/* * 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 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 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; } } }