Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.messaging.ui;
     17 
     18 import android.content.Context;
     19 import android.graphics.Point;
     20 import android.graphics.Rect;
     21 import android.os.Handler;
     22 import android.text.TextUtils;
     23 import android.util.DisplayMetrics;
     24 import android.view.Gravity;
     25 import android.view.MotionEvent;
     26 import android.view.View;
     27 import android.view.View.MeasureSpec;
     28 import android.view.View.OnTouchListener;
     29 import android.view.ViewGroup;
     30 import android.view.ViewGroup.LayoutParams;
     31 import android.view.ViewPropertyAnimator;
     32 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     33 import android.view.WindowManager;
     34 import android.widget.PopupWindow;
     35 import android.widget.PopupWindow.OnDismissListener;
     36 
     37 import com.android.messaging.Factory;
     38 import com.android.messaging.R;
     39 import com.android.messaging.ui.SnackBar.Placement;
     40 import com.android.messaging.ui.SnackBar.SnackBarListener;
     41 import com.android.messaging.util.AccessibilityUtil;
     42 import com.android.messaging.util.Assert;
     43 import com.android.messaging.util.LogUtil;
     44 import com.android.messaging.util.OsUtil;
     45 import com.android.messaging.util.TextUtil;
     46 import com.android.messaging.util.UiUtils;
     47 import com.google.common.base.Joiner;
     48 
     49 import java.util.List;
     50 
     51 public class SnackBarManager {
     52 
     53     private static SnackBarManager sInstance;
     54 
     55     public static SnackBarManager get() {
     56         if (sInstance == null) {
     57             synchronized (SnackBarManager.class) {
     58                 if (sInstance == null) {
     59                     sInstance = new SnackBarManager();
     60                 }
     61             }
     62         }
     63         return sInstance;
     64     }
     65 
     66     private final Runnable mDismissRunnable = new Runnable() {
     67         @Override
     68         public void run() {
     69             dismiss();
     70         }
     71     };
     72 
     73     private final OnTouchListener mDismissOnTouchListener = new OnTouchListener() {
     74         @Override
     75         public boolean onTouch(final View view, final MotionEvent event) {
     76             // Dismiss the {@link SnackBar} but don't consume the event.
     77             dismiss();
     78             return false;
     79         }
     80     };
     81 
     82     private final SnackBarListener mDismissOnUserTapListener = new SnackBarListener() {
     83         @Override
     84         public void onActionClick() {
     85             dismiss();
     86         }
     87     };
     88 
     89     private final int mTranslationDurationMs;
     90     private final Handler mHideHandler;
     91 
     92     private SnackBar mCurrentSnackBar;
     93     private SnackBar mLatestSnackBar;
     94     private SnackBar mNextSnackBar;
     95     private boolean mIsCurrentlyDismissing;
     96     private PopupWindow mPopupWindow;
     97 
     98     private SnackBarManager() {
     99         mTranslationDurationMs = Factory.get().getApplicationContext().getResources().getInteger(
    100                 R.integer.snackbar_translation_duration_ms);
    101         mHideHandler = new Handler();
    102     }
    103 
    104     public SnackBar getLatestSnackBar() {
    105         return mLatestSnackBar;
    106     }
    107 
    108     public SnackBar.Builder newBuilder(final View parentView) {
    109         return new SnackBar.Builder(this, parentView);
    110     }
    111 
    112     /**
    113      * The given snackBar is not guaranteed to be shown. If the previous snackBar is animating away,
    114      * and another snackBar is requested to show after this one, this snackBar will be skipped.
    115      */
    116     public void show(final SnackBar snackBar) {
    117         Assert.notNull(snackBar);
    118 
    119         if (mCurrentSnackBar != null) {
    120             LogUtil.d(LogUtil.BUGLE_TAG, "Showing snack bar, but currentSnackBar was not null.");
    121 
    122             // Dismiss the current snack bar. That will cause the next snack bar to be shown on
    123             // completion.
    124             mNextSnackBar = snackBar;
    125             mLatestSnackBar = snackBar;
    126             dismiss();
    127             return;
    128         }
    129 
    130         mCurrentSnackBar = snackBar;
    131         mLatestSnackBar = snackBar;
    132 
    133         // We want to know when either button was tapped so we can dismiss.
    134         snackBar.setListener(mDismissOnUserTapListener);
    135 
    136         // Cancel previous dismisses & set dismiss for the delay time.
    137         mHideHandler.removeCallbacks(mDismissRunnable);
    138         mHideHandler.postDelayed(mDismissRunnable, snackBar.getDuration());
    139 
    140         snackBar.setEnabled(false);
    141 
    142         // For some reason, the addView function does not respect layoutParams.
    143         // We need to explicitly set it first here.
    144         final View rootView = snackBar.getRootView();
    145 
    146         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
    147             LogUtil.d(LogUtil.BUGLE_TAG, "Showing snack bar: " + snackBar);
    148         }
    149         // Measure the snack bar root view so we know how much to translate by.
    150         measureSnackBar(snackBar);
    151         mPopupWindow = new PopupWindow(snackBar.getContext());
    152         mPopupWindow.setWidth(LayoutParams.MATCH_PARENT);
    153         mPopupWindow.setHeight(LayoutParams.WRAP_CONTENT);
    154         mPopupWindow.setBackgroundDrawable(null);
    155         mPopupWindow.setContentView(rootView);
    156         final Placement placement = snackBar.getPlacement();
    157         if (placement == null) {
    158             mPopupWindow.showAtLocation(
    159                     snackBar.getParentView(), Gravity.BOTTOM | Gravity.START,
    160                     0, getScreenBottomOffset(snackBar));
    161         } else {
    162             final View anchorView = placement.getAnchorView();
    163 
    164             // You'd expect PopupWindow.showAsDropDown to ensure the popup moves with the anchor
    165             // view, which it does for scrolling, but not layout changes, so we have to manually
    166             // update while the snackbar is showing
    167             final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
    168                 @Override
    169                 public void onGlobalLayout() {
    170                     mPopupWindow.update(anchorView, 0, getRelativeOffset(snackBar),
    171                             anchorView.getWidth(), LayoutParams.WRAP_CONTENT);
    172                 }
    173             };
    174             anchorView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
    175             mPopupWindow.setOnDismissListener(new OnDismissListener() {
    176                 @Override
    177                 public void onDismiss() {
    178                     anchorView.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
    179                 }
    180             });
    181             mPopupWindow.showAsDropDown(anchorView, 0, getRelativeOffset(snackBar));
    182         }
    183 
    184 
    185         // Animate the toast bar into view.
    186         placeSnackBarOffScreen(snackBar);
    187         animateSnackBarOnScreen(snackBar).withEndAction(new Runnable() {
    188             @Override
    189             public void run() {
    190                 mCurrentSnackBar.setEnabled(true);
    191                 makeCurrentSnackBarDismissibleOnTouch();
    192                 // Fire an accessibility event as needed
    193                 String snackBarText = snackBar.getMessageText();
    194                 if (!TextUtils.isEmpty(snackBarText) &&
    195                         TextUtils.getTrimmedLength(snackBarText) > 0) {
    196                     snackBarText = snackBarText.trim();
    197                     final String snackBarActionText = snackBar.getActionLabel();
    198                     if (!TextUtil.isAllWhitespace(snackBarActionText)) {
    199                         snackBarText = Joiner.on(", ").join(snackBarText, snackBarActionText);
    200                     }
    201                     AccessibilityUtil.announceForAccessibilityCompat(snackBar.getSnackBarView(),
    202                             null /*accessibilityManager*/, snackBarText);
    203                 }
    204             }
    205         });
    206 
    207         // Animate any interaction views out of the way.
    208         animateInteractionsOnShow(snackBar);
    209     }
    210 
    211     /**
    212      * Dismisses the current toast that is showing. If there is a toast waiting to be shown, that
    213      * toast will be shown when the current one has been dismissed.
    214      */
    215     public void dismiss() {
    216         mHideHandler.removeCallbacks(mDismissRunnable);
    217 
    218         if (mCurrentSnackBar == null || mIsCurrentlyDismissing) {
    219             return;
    220         }
    221 
    222         final SnackBar snackBar = mCurrentSnackBar;
    223 
    224         LogUtil.d(LogUtil.BUGLE_TAG, "Dismissing snack bar.");
    225         mIsCurrentlyDismissing = true;
    226 
    227         snackBar.setEnabled(false);
    228 
    229         // Animate the toast bar down.
    230         final View rootView = snackBar.getRootView();
    231         animateSnackBarOffScreen(snackBar).withEndAction(new Runnable() {
    232             @Override
    233             public void run() {
    234                 rootView.setVisibility(View.GONE);
    235                 try {
    236                     mPopupWindow.dismiss();
    237                 } catch (IllegalArgumentException e) {
    238                     // PopupWindow.dismiss() will fire an IllegalArgumentException if the activity
    239                     // has already ended while we were animating
    240                 }
    241 
    242                 mCurrentSnackBar = null;
    243                 mIsCurrentlyDismissing = false;
    244 
    245                 // Show the next toast if one is waiting.
    246                 if (mNextSnackBar != null) {
    247                     final SnackBar localNextSnackBar = mNextSnackBar;
    248                     mNextSnackBar = null;
    249                     show(localNextSnackBar);
    250                 }
    251             }
    252         });
    253 
    254         // Animate any interaction views back.
    255         animateInteractionsOnDismiss(snackBar);
    256     }
    257 
    258     private void makeCurrentSnackBarDismissibleOnTouch() {
    259         // Set touching on the entire view, the {@link SnackBar} itself, as
    260         // well as the button's dismiss the toast.
    261         mCurrentSnackBar.getRootView().setOnTouchListener(mDismissOnTouchListener);
    262         mCurrentSnackBar.getSnackBarView().setOnTouchListener(mDismissOnTouchListener);
    263     }
    264 
    265     private void measureSnackBar(final SnackBar snackBar) {
    266         final View rootView = snackBar.getRootView();
    267         final Point displaySize = new Point();
    268         getWindowManager(snackBar.getContext()).getDefaultDisplay().getSize(displaySize);
    269         final int widthSpec = ViewGroup.getChildMeasureSpec(
    270                 MeasureSpec.makeMeasureSpec(displaySize.x, MeasureSpec.EXACTLY),
    271                 0, LayoutParams.MATCH_PARENT);
    272         final int heightSpec = ViewGroup.getChildMeasureSpec(
    273                 MeasureSpec.makeMeasureSpec(displaySize.y, MeasureSpec.EXACTLY),
    274                 0, LayoutParams.WRAP_CONTENT);
    275         rootView.measure(widthSpec, heightSpec);
    276     }
    277 
    278     private void placeSnackBarOffScreen(final SnackBar snackBar) {
    279         final View rootView = snackBar.getRootView();
    280         final View snackBarView = snackBar.getSnackBarView();
    281         snackBarView.setTranslationY(rootView.getMeasuredHeight());
    282     }
    283 
    284     private ViewPropertyAnimator animateSnackBarOnScreen(final SnackBar snackBar) {
    285         final View snackBarView = snackBar.getSnackBarView();
    286         return normalizeAnimator(snackBarView.animate()).translationX(0).translationY(0);
    287     }
    288 
    289     private ViewPropertyAnimator animateSnackBarOffScreen(final SnackBar snackBar) {
    290         final View rootView = snackBar.getRootView();
    291         final View snackBarView = snackBar.getSnackBarView();
    292         return normalizeAnimator(snackBarView.animate()).translationY(rootView.getHeight());
    293     }
    294 
    295     private void animateInteractionsOnShow(final SnackBar snackBar) {
    296         final List<SnackBarInteraction> interactions = snackBar.getInteractions();
    297         for (final SnackBarInteraction interaction : interactions) {
    298             if (interaction != null) {
    299                 final ViewPropertyAnimator animator = interaction.animateOnSnackBarShow(snackBar);
    300                 if (animator != null) {
    301                     normalizeAnimator(animator);
    302                 }
    303             }
    304         }
    305     }
    306 
    307     private void animateInteractionsOnDismiss(final SnackBar snackBar) {
    308         final List<SnackBarInteraction> interactions = snackBar.getInteractions();
    309         for (final SnackBarInteraction interaction : interactions) {
    310             if (interaction != null) {
    311                 final ViewPropertyAnimator animator =
    312                         interaction.animateOnSnackBarDismiss(snackBar);
    313                 if (animator != null) {
    314                     normalizeAnimator(animator);
    315                 }
    316             }
    317         }
    318     }
    319 
    320     private ViewPropertyAnimator normalizeAnimator(final ViewPropertyAnimator animator) {
    321         return animator
    322                 .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
    323                 .setDuration(mTranslationDurationMs);
    324     }
    325 
    326     private WindowManager getWindowManager(final Context context) {
    327         return (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    328     }
    329 
    330     /**
    331      * Get the offset from the bottom of the screen where the snack bar should be placed.
    332      */
    333     private int getScreenBottomOffset(final SnackBar snackBar) {
    334         final WindowManager windowManager = getWindowManager(snackBar.getContext());
    335         final DisplayMetrics displayMetrics = new DisplayMetrics();
    336         if (OsUtil.isAtLeastL()) {
    337             windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
    338         } else {
    339             windowManager.getDefaultDisplay().getMetrics(displayMetrics);
    340         }
    341         final int screenHeight = displayMetrics.heightPixels;
    342 
    343         if (OsUtil.isAtLeastL()) {
    344             // In L, the navigation bar is included in the space for the popup window, so we have to
    345             // offset by the size of the navigation bar
    346             final Rect displayRect = new Rect();
    347             snackBar.getParentView().getRootView().getWindowVisibleDisplayFrame(displayRect);
    348             return screenHeight - displayRect.bottom;
    349         }
    350 
    351         return 0;
    352     }
    353 
    354     private int getRelativeOffset(final SnackBar snackBar) {
    355         final Placement placement = snackBar.getPlacement();
    356         Assert.notNull(placement);
    357         final View anchorView = placement.getAnchorView();
    358         if (placement.getAnchorAbove()) {
    359             return -snackBar.getRootView().getMeasuredHeight() - anchorView.getHeight();
    360         } else {
    361             // Use the default dropdown positioning
    362             return 0;
    363         }
    364     }
    365 }
    366