blob: d5ca87098bcc1de44b2e802e418f465a1987ac4f [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnAttachStateChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewPropertyAnimator;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager;
import android.widget.PopupWindow;
import android.widget.PopupWindow.OnDismissListener;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.ui.SnackBar.Placement;
import com.android.messaging.ui.SnackBar.SnackBarListener;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.TextUtil;
import com.android.messaging.util.UiUtils;
import com.google.common.base.Joiner;
import java.util.List;
public class SnackBarManager {
private static SnackBarManager sInstance;
public static SnackBarManager get() {
if (sInstance == null) {
synchronized (SnackBarManager.class) {
if (sInstance == null) {
sInstance = new SnackBarManager();
}
}
}
return sInstance;
}
private final Runnable mDismissRunnable = new Runnable() {
@Override
public void run() {
dismiss();
}
};
private final OnTouchListener mDismissOnTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(final View view, final MotionEvent event) {
// Dismiss the {@link SnackBar} but don't consume the event.
dismiss();
return false;
}
};
private final SnackBarListener mDismissOnUserTapListener = new SnackBarListener() {
@Override
public void onActionClick() {
dismiss();
}
};
private final OnAttachStateChangeListener mAttachStateChangeListener =
new OnAttachStateChangeListener() {
@Override
public void onViewDetachedFromWindow(View v) {
// Dismiss the PopupWindow and clear SnackBarManager state.
mHideHandler.removeCallbacks(mDismissRunnable);
mPopupWindow.dismiss();
mCurrentSnackBar = null;
mNextSnackBar = null;
mIsCurrentlyDismissing = false;
}
@Override
public void onViewAttachedToWindow(View v) {}
};
private final int mTranslationDurationMs;
private final Handler mHideHandler;
private SnackBar mCurrentSnackBar;
private SnackBar mLatestSnackBar;
private SnackBar mNextSnackBar;
private boolean mIsCurrentlyDismissing;
private PopupWindow mPopupWindow;
private SnackBarManager() {
mTranslationDurationMs = Factory.get().getApplicationContext().getResources().getInteger(
R.integer.snackbar_translation_duration_ms);
mHideHandler = new Handler();
}
public SnackBar getLatestSnackBar() {
return mLatestSnackBar;
}
public SnackBar.Builder newBuilder(final View parentView) {
return new SnackBar.Builder(this, parentView);
}
/**
* The given snackBar is not guaranteed to be shown. If the previous snackBar is animating away,
* and another snackBar is requested to show after this one, this snackBar will be skipped.
*/
public void show(final SnackBar snackBar) {
Assert.notNull(snackBar);
if (mCurrentSnackBar != null) {
LogUtil.d(LogUtil.BUGLE_TAG, "Showing snack bar, but currentSnackBar was not null.");
// Dismiss the current snack bar. That will cause the next snack bar to be shown on
// completion.
mNextSnackBar = snackBar;
mLatestSnackBar = snackBar;
dismiss();
return;
}
mCurrentSnackBar = snackBar;
mLatestSnackBar = snackBar;
// We want to know when either button was tapped so we can dismiss.
snackBar.setListener(mDismissOnUserTapListener);
// Cancel previous dismisses & set dismiss for the delay time.
mHideHandler.removeCallbacks(mDismissRunnable);
mHideHandler.postDelayed(mDismissRunnable, snackBar.getDuration());
snackBar.setEnabled(false);
// For some reason, the addView function does not respect layoutParams.
// We need to explicitly set it first here.
final View rootView = snackBar.getRootView();
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
LogUtil.d(LogUtil.BUGLE_TAG, "Showing snack bar: " + snackBar);
}
// Measure the snack bar root view so we know how much to translate by.
measureSnackBar(snackBar);
mPopupWindow = new PopupWindow(snackBar.getContext());
mPopupWindow.setWidth(LayoutParams.MATCH_PARENT);
mPopupWindow.setHeight(LayoutParams.WRAP_CONTENT);
mPopupWindow.setBackgroundDrawable(null);
mPopupWindow.setContentView(rootView);
final Placement placement = snackBar.getPlacement();
if (placement == null) {
mPopupWindow.showAtLocation(
snackBar.getParentView(), Gravity.BOTTOM | Gravity.START,
0, getScreenBottomOffset(snackBar));
} else {
final View anchorView = placement.getAnchorView();
// You'd expect PopupWindow.showAsDropDown to ensure the popup moves with the anchor
// view, which it does for scrolling, but not layout changes, so we have to manually
// update while the snackbar is showing
final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mPopupWindow.update(anchorView, 0, getRelativeOffset(snackBar),
anchorView.getWidth(), LayoutParams.WRAP_CONTENT);
}
};
anchorView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
mPopupWindow.setOnDismissListener(new OnDismissListener() {
@Override
public void onDismiss() {
anchorView.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
}
});
mPopupWindow.showAsDropDown(anchorView, 0, getRelativeOffset(snackBar));
}
snackBar.getParentView().addOnAttachStateChangeListener(mAttachStateChangeListener);
// Animate the toast bar into view.
placeSnackBarOffScreen(snackBar);
animateSnackBarOnScreen(snackBar).withEndAction(new Runnable() {
@Override
public void run() {
mCurrentSnackBar.setEnabled(true);
makeCurrentSnackBarDismissibleOnTouch();
// Fire an accessibility event as needed
String snackBarText = snackBar.getMessageText();
if (!TextUtils.isEmpty(snackBarText) &&
TextUtils.getTrimmedLength(snackBarText) > 0) {
snackBarText = snackBarText.trim();
final String snackBarActionText = snackBar.getActionLabel();
if (!TextUtil.isAllWhitespace(snackBarActionText)) {
snackBarText = Joiner.on(", ").join(snackBarText, snackBarActionText);
}
AccessibilityUtil.announceForAccessibilityCompat(snackBar.getSnackBarView(),
null /*accessibilityManager*/, snackBarText);
}
}
});
// Animate any interaction views out of the way.
animateInteractionsOnShow(snackBar);
}
/**
* Dismisses the current toast that is showing. If there is a toast waiting to be shown, that
* toast will be shown when the current one has been dismissed.
*/
public void dismiss() {
mHideHandler.removeCallbacks(mDismissRunnable);
if (mCurrentSnackBar == null || mIsCurrentlyDismissing) {
return;
}
final SnackBar snackBar = mCurrentSnackBar;
LogUtil.d(LogUtil.BUGLE_TAG, "Dismissing snack bar.");
mIsCurrentlyDismissing = true;
snackBar.setEnabled(false);
// Animate the toast bar down.
final View rootView = snackBar.getRootView();
animateSnackBarOffScreen(snackBar).withEndAction(new Runnable() {
@Override
public void run() {
rootView.setVisibility(View.GONE);
try {
mPopupWindow.dismiss();
} catch (IllegalArgumentException e) {
// PopupWindow.dismiss() will fire an IllegalArgumentException if the activity
// has already ended while we were animating
}
snackBar.getParentView()
.removeOnAttachStateChangeListener(mAttachStateChangeListener);
mCurrentSnackBar = null;
mIsCurrentlyDismissing = false;
// Show the next toast if one is waiting.
if (mNextSnackBar != null) {
final SnackBar localNextSnackBar = mNextSnackBar;
mNextSnackBar = null;
show(localNextSnackBar);
}
}
});
// Animate any interaction views back.
animateInteractionsOnDismiss(snackBar);
}
private void makeCurrentSnackBarDismissibleOnTouch() {
// Set touching on the entire view, the {@link SnackBar} itself, as
// well as the button's dismiss the toast.
mCurrentSnackBar.getRootView().setOnTouchListener(mDismissOnTouchListener);
mCurrentSnackBar.getSnackBarView().setOnTouchListener(mDismissOnTouchListener);
}
private void measureSnackBar(final SnackBar snackBar) {
final View rootView = snackBar.getRootView();
final Point displaySize = new Point();
getWindowManager(snackBar.getContext()).getDefaultDisplay().getSize(displaySize);
final int widthSpec = ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(displaySize.x, MeasureSpec.EXACTLY),
0, LayoutParams.MATCH_PARENT);
final int heightSpec = ViewGroup.getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(displaySize.y, MeasureSpec.EXACTLY),
0, LayoutParams.WRAP_CONTENT);
rootView.measure(widthSpec, heightSpec);
}
private void placeSnackBarOffScreen(final SnackBar snackBar) {
final View rootView = snackBar.getRootView();
final View snackBarView = snackBar.getSnackBarView();
snackBarView.setTranslationY(rootView.getMeasuredHeight());
}
private ViewPropertyAnimator animateSnackBarOnScreen(final SnackBar snackBar) {
final View snackBarView = snackBar.getSnackBarView();
return normalizeAnimator(snackBarView.animate()).translationX(0).translationY(0);
}
private ViewPropertyAnimator animateSnackBarOffScreen(final SnackBar snackBar) {
final View rootView = snackBar.getRootView();
final View snackBarView = snackBar.getSnackBarView();
return normalizeAnimator(snackBarView.animate()).translationY(rootView.getHeight());
}
private void animateInteractionsOnShow(final SnackBar snackBar) {
final List<SnackBarInteraction> interactions = snackBar.getInteractions();
for (final SnackBarInteraction interaction : interactions) {
if (interaction != null) {
final ViewPropertyAnimator animator = interaction.animateOnSnackBarShow(snackBar);
if (animator != null) {
normalizeAnimator(animator);
}
}
}
}
private void animateInteractionsOnDismiss(final SnackBar snackBar) {
final List<SnackBarInteraction> interactions = snackBar.getInteractions();
for (final SnackBarInteraction interaction : interactions) {
if (interaction != null) {
final ViewPropertyAnimator animator =
interaction.animateOnSnackBarDismiss(snackBar);
if (animator != null) {
normalizeAnimator(animator);
}
}
}
}
private ViewPropertyAnimator normalizeAnimator(final ViewPropertyAnimator animator) {
return animator
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
.setDuration(mTranslationDurationMs);
}
private WindowManager getWindowManager(final Context context) {
return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
}
/**
* Get the offset from the bottom of the screen where the snack bar should be placed.
*/
private int getScreenBottomOffset(final SnackBar snackBar) {
final WindowManager windowManager = getWindowManager(snackBar.getContext());
final DisplayMetrics displayMetrics = new DisplayMetrics();
if (OsUtil.isAtLeastL()) {
windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
} else {
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
}
final int screenHeight = displayMetrics.heightPixels;
if (OsUtil.isAtLeastL()) {
// In L, the navigation bar is included in the space for the popup window, so we have to
// offset by the size of the navigation bar
final Rect displayRect = new Rect();
snackBar.getParentView().getRootView().getWindowVisibleDisplayFrame(displayRect);
return screenHeight - displayRect.bottom;
}
return 0;
}
private int getRelativeOffset(final SnackBar snackBar) {
final Placement placement = snackBar.getPlacement();
Assert.notNull(placement);
final View anchorView = placement.getAnchorView();
if (placement.getAnchorAbove()) {
return -snackBar.getRootView().getMeasuredHeight() - anchorView.getHeight();
} else {
// Use the default dropdown positioning
return 0;
}
}
}