summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/SnackBarManager.java
diff options
context:
space:
mode:
authorMike Dodd <mdodd@google.com>2015-08-11 11:16:59 -0700
committerMike Dodd <mdodd@google.com>2015-08-12 08:58:28 -0700
commit461a34b466cb4b13dbbc2ec6330b31e217b2ac4e (patch)
treebc4b489af52d0e2521e21167d2ad76a47256f348 /src/com/android/messaging/ui/SnackBarManager.java
parent8b3e2b9c1b0a09423a7ba5d1091b9192106502f8 (diff)
downloadandroid_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.tar.gz
android_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.tar.bz2
android_packages_apps_Messaging-461a34b466cb4b13dbbc2ec6330b31e217b2ac4e.zip
Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
Diffstat (limited to 'src/com/android/messaging/ui/SnackBarManager.java')
-rw-r--r--src/com/android/messaging/ui/SnackBarManager.java365
1 files changed, 365 insertions, 0 deletions
diff --git a/src/com/android/messaging/ui/SnackBarManager.java b/src/com/android/messaging/ui/SnackBarManager.java
new file mode 100644
index 0000000..e107999
--- /dev/null
+++ b/src/com/android/messaging/ui/SnackBarManager.java
@@ -0,0 +1,365 @@
+/*
+ * 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.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 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));
+ }
+
+
+ // 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
+ }
+
+ 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;
+ }
+ }
+}