diff options
author | Mike Dodd <mdodd@google.com> | 2015-08-11 11:16:59 -0700 |
---|---|---|
committer | Mike Dodd <mdodd@google.com> | 2015-08-12 08:58:28 -0700 |
commit | 461a34b466cb4b13dbbc2ec6330b31e217b2ac4e (patch) | |
tree | bc4b489af52d0e2521e21167d2ad76a47256f348 /src/com/android/messaging/ui/SnackBarManager.java | |
parent | 8b3e2b9c1b0a09423a7ba5d1091b9192106502f8 (diff) | |
download | android_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.java | 365 |
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; + } + } +} |