Home | History | Annotate | Download | only in ui
      1 /**
      2  * Copyright (c) 2011, Google Inc.
      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.mail.ui;
     17 
     18 import android.animation.Animator;
     19 import android.animation.AnimatorListenerAdapter;
     20 import android.animation.TimeInterpolator;
     21 import android.annotation.TargetApi;
     22 import android.content.Context;
     23 import android.os.Handler;
     24 import android.support.annotation.StringRes;
     25 import android.text.TextUtils;
     26 import android.util.AttributeSet;
     27 import android.view.MotionEvent;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.view.animation.LinearInterpolator;
     31 import android.view.animation.PathInterpolator;
     32 import android.widget.FrameLayout;
     33 import android.widget.TextView;
     34 
     35 import com.android.mail.R;
     36 import com.android.mail.utils.Utils;
     37 import com.android.mail.utils.ViewUtils;
     38 
     39 /**
     40  * A custom {@link View} that exposes an action to the user.
     41  */
     42 public class ActionableToastBar extends FrameLayout {
     43 
     44     private boolean mHidden = true;
     45     private final Runnable mHideToastBarRunnable;
     46     private final Handler mHideToastBarHandler;
     47 
     48     /**
     49      * The floating action button if it must be animated with the toast bar; <code>null</code>
     50      * otherwise.
     51      */
     52     private View mFloatingActionButton;
     53 
     54     /**
     55      * <tt>true</tt> while animation is occurring; false otherwise; It is used to block attempts to
     56      * hide the toast bar while it is being animated
     57      */
     58     private boolean mAnimating = false;
     59 
     60     /** The interpolator that produces position values during animation. */
     61     private TimeInterpolator mAnimationInterpolator;
     62 
     63     /** The length of time (in milliseconds) that the popup / push down animation run over */
     64     private int mAnimationDuration;
     65 
     66     /**
     67      * The time at which the toast popup completed. This is used to ensure the toast remains
     68      * visible for a minimum duration before it is removed.
     69      */
     70     private long mAnimationCompleteTimestamp;
     71 
     72     /** The min time duration for which the toast must remain visible and cannot be dismissed. */
     73     private long mMinToastDuration;
     74 
     75     /** The max time duration for which the toast can remain visible and must be dismissed. */
     76     private long mMaxToastDuration;
     77 
     78     /** The view that contains the description when laid out as a single line. */
     79     private TextView mSingleLineDescriptionView;
     80 
     81     /** The view that contains the text for the action button when laid out as a single line. */
     82     private TextView mSingleLineActionView;
     83 
     84     /** The view that contains the description when laid out as a multiple lines;
     85      * always <tt>null</tt> in two-pane layouts. */
     86     private TextView mMultiLineDescriptionView;
     87 
     88     /** The view that contains the text for the action button when laid out as a multiple lines;
     89      * always <tt>null</tt> in two-pane layouts. */
     90     private TextView mMultiLineActionView;
     91 
     92     /** The minimum width of this view; applicable when description text is very short. */
     93     private int mMinWidth;
     94 
     95     /** The maximum width of this view; applicable when description text is long enough to wrap. */
     96     private int mMaxWidth;
     97 
     98     private ToastBarOperation mOperation;
     99 
    100     public ActionableToastBar(Context context) {
    101         this(context, null);
    102     }
    103 
    104     public ActionableToastBar(Context context, AttributeSet attrs) {
    105         this(context, attrs, 0);
    106     }
    107 
    108     public ActionableToastBar(Context context, AttributeSet attrs, int defStyle) {
    109         super(context, attrs, defStyle);
    110         mAnimationInterpolator = createTimeInterpolator();
    111         mAnimationDuration = getResources().getInteger(R.integer.toast_bar_animation_duration_ms);
    112         mMinToastDuration = getResources().getInteger(R.integer.toast_bar_min_duration_ms);
    113         mMaxToastDuration = getResources().getInteger(R.integer.toast_bar_max_duration_ms);
    114         mMinWidth = getResources().getDimensionPixelOffset(R.dimen.snack_bar_min_width);
    115         mMaxWidth = getResources().getDimensionPixelOffset(R.dimen.snack_bar_max_width);
    116         mHideToastBarHandler = new Handler();
    117         mHideToastBarRunnable = new Runnable() {
    118             @Override
    119             public void run() {
    120                 if (!mHidden) {
    121                     hide(true, false /* actionClicked */);
    122                 }
    123             }
    124         };
    125     }
    126 
    127     private TimeInterpolator createTimeInterpolator() {
    128         // L and beyond we can use the new PathInterpolator
    129         if (Utils.isRunningLOrLater()) {
    130             return createPathInterpolator();
    131         }
    132 
    133         // fall back to basic LinearInterpolator
    134         return new LinearInterpolator();
    135     }
    136 
    137     @TargetApi(21)
    138     private TimeInterpolator createPathInterpolator() {
    139         return new PathInterpolator(0.4f, 0f, 0.2f, 1f);
    140     }
    141 
    142     @Override
    143     protected void onFinishInflate() {
    144         super.onFinishInflate();
    145 
    146         mSingleLineDescriptionView = (TextView) findViewById(R.id.description_text);
    147         mSingleLineActionView = (TextView) findViewById(R.id.action_text);
    148         mMultiLineDescriptionView = (TextView) findViewById(R.id.multiline_description_text);
    149         mMultiLineActionView = (TextView) findViewById(R.id.multiline_action_text);
    150     }
    151 
    152     @Override
    153     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    154         final boolean showAction = !TextUtils.isEmpty(mSingleLineActionView.getText());
    155 
    156         // configure the UI assuming the description fits on a single line
    157         setVisibility(false /* multiLine */, showAction);
    158 
    159         // measure the view and its content
    160         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    161 
    162         // if specific views exist to handle the multiline case
    163         if (mMultiLineDescriptionView != null) {
    164             // if the description does not fit on a single line
    165             if (mSingleLineDescriptionView.getLineCount() > 1) {
    166                 //switch to multi line display views
    167                 setVisibility(true /* multiLine */, showAction);
    168 
    169                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    170             }
    171         // if width constraints were given explicitly, honor them; otherwise use the natural width
    172         } else if (mMinWidth >= 0 && mMaxWidth >= 0) {
    173             // otherwise, adjust the the single line view so wrapping occurs at the desired width
    174             // (the total width of the toast bar must always fall between the given min and max
    175             // width; if max width cannot accommodate all of the description text, it wraps)
    176             if (getMeasuredWidth() < mMinWidth) {
    177                 widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mMinWidth, MeasureSpec.EXACTLY);
    178                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    179             } else if (getMeasuredWidth() > mMaxWidth) {
    180                 widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
    181                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    182             }
    183         }
    184     }
    185 
    186     /**
    187      * Displays the toast bar and makes it visible. Allows the setting of
    188      * parameters to customize the display.
    189      * @param listener Performs some action when the action button is clicked.
    190      *                 If the {@link ToastBarOperation} overrides
    191      *                 {@link ToastBarOperation#shouldTakeOnActionClickedPrecedence()}
    192      *                 to return <code>true</code>, the
    193      *                 {@link ToastBarOperation#onActionClicked(android.content.Context)}
    194      *                 will override this listener and be called instead.
    195      * @param descriptionText a description text to show in the toast bar
    196      * @param actionTextResourceId resource ID for the text to show in the action button
    197      * @param replaceVisibleToast if true, this toast should replace any currently visible toast.
    198      *                            Otherwise, skip showing this toast.
    199      * @param autohide <tt>true</tt> indicates the toast will be automatically hidden after a time
    200      *                 delay; <tt>false</tt> indicate the toast will remain visible until the user
    201      *                 dismisses it
    202      * @param op the operation that corresponds to the specific toast being shown
    203      */
    204     public void show(final ActionClickedListener listener, final CharSequence descriptionText,
    205                      @StringRes final int actionTextResourceId, final boolean replaceVisibleToast,
    206                      final boolean autohide, final ToastBarOperation op) {
    207         if (!mHidden && !replaceVisibleToast) {
    208             return;
    209         }
    210 
    211         // Remove any running delayed animations first
    212         mHideToastBarHandler.removeCallbacks(mHideToastBarRunnable);
    213 
    214         mOperation = op;
    215 
    216         setActionClickListener(new OnClickListener() {
    217             @Override
    218             public void onClick(View widget) {
    219                 if (op != null && op.shouldTakeOnActionClickedPrecedence()) {
    220                     op.onActionClicked(getContext());
    221                 } else {
    222                     listener.onActionClicked(getContext());
    223                 }
    224                 hide(true /* animate */, true /* actionClicked */);
    225             }
    226         });
    227 
    228         setDescriptionText(descriptionText);
    229         ViewUtils.announceForAccessibility(this, descriptionText);
    230         setActionText(actionTextResourceId);
    231 
    232         // if this toast bar is not yet hidden, animate it in place; otherwise we just update the
    233         // text that it displays
    234         if (mHidden) {
    235             mHidden = false;
    236             popupToast();
    237         }
    238 
    239         if (autohide) {
    240             // Set up runnable to execute hide toast once delay is completed
    241             mHideToastBarHandler.postDelayed(mHideToastBarRunnable, mMaxToastDuration);
    242         }
    243     }
    244 
    245     public ToastBarOperation getOperation() {
    246         return mOperation;
    247     }
    248 
    249     /**
    250      * Hides the view and resets the state.
    251      */
    252     public void hide(boolean animate, boolean actionClicked) {
    253         mHidden = true;
    254         mAnimationCompleteTimestamp = 0;
    255         mHideToastBarHandler.removeCallbacks(mHideToastBarRunnable);
    256         if (getVisibility() == View.VISIBLE) {
    257             setActionClickListener(null);
    258             // Hide view once it's clicked.
    259             if (animate) {
    260                 pushDownToast();
    261             } else {
    262                 // immediate hiding implies no position adjustment of the FAB and hide the toast bar
    263                 if (mFloatingActionButton != null) {
    264                     mFloatingActionButton.setTranslationY(0);
    265                 }
    266                 setVisibility(View.GONE);
    267             }
    268 
    269             if (!actionClicked && mOperation != null) {
    270                 mOperation.onToastBarTimeout(getContext());
    271             }
    272         }
    273     }
    274 
    275     /**
    276      * @return <tt>true</tt> while the toast bar animation is popping up or pushing down the toast;
    277      *      <tt>false</tt> otherwise
    278      */
    279     public boolean isAnimating() {
    280         return mAnimating;
    281     }
    282 
    283     /**
    284      * @return <tt>true</tt> if this toast bar has not yet been displayed for a long enough period
    285      *      of time to be dismissed; <tt>false</tt> otherwise
    286      */
    287     public boolean cannotBeHidden() {
    288         return System.currentTimeMillis() - mAnimationCompleteTimestamp < mMinToastDuration;
    289     }
    290 
    291     @Override
    292     public void onDetachedFromWindow() {
    293         mHideToastBarHandler.removeCallbacks(mHideToastBarRunnable);
    294         super.onDetachedFromWindow();
    295     }
    296 
    297     public boolean isEventInToastBar(MotionEvent event) {
    298         if (!isShown()) {
    299             return false;
    300         }
    301         int[] xy = new int[2];
    302         float x = event.getX();
    303         float y = event.getY();
    304         getLocationOnScreen(xy);
    305         return (x > xy[0] && x < (xy[0] + getWidth()) && y > xy[1] && y < xy[1] + getHeight());
    306     }
    307 
    308     /**
    309      * Indicates that the given view should be animated with this toast bar as it pops up and pushes
    310      * down. In some layouts, the floating action button appears above the toast bar and thus must
    311      * be pushed up as the toast pops up and fall down as the toast is pushed down.
    312      *
    313      * @param floatingActionButton a the floating action button to be animated with the toast bar as
    314      *                             it pops up and pushes down
    315      */
    316     public void setFloatingActionButton(View floatingActionButton) {
    317         mFloatingActionButton = floatingActionButton;
    318     }
    319 
    320     /**
    321      * If the View requires multiple lines to fully display the toast description then make the
    322      * multi-line view visible and hide the single line view; otherwise vice versa. If the action
    323      * text is present, display it, otherwise hide it.
    324      *
    325      * @param multiLine <tt>true</tt> if the View requires multiple lines to display the toast
    326      * @param showAction <tt>true</tt> if the action text is present and should be shown
    327      */
    328     private void setVisibility(boolean multiLine, boolean showAction) {
    329         mSingleLineDescriptionView.setVisibility(!multiLine ? View.VISIBLE : View.GONE);
    330         mSingleLineActionView.setVisibility(!multiLine && showAction ? View.VISIBLE : View.GONE);
    331         if (mMultiLineDescriptionView != null) {
    332             mMultiLineDescriptionView.setVisibility(multiLine ? View.VISIBLE : View.GONE);
    333         }
    334         if (mMultiLineActionView != null) {
    335             mMultiLineActionView.setVisibility(multiLine && showAction ? View.VISIBLE : View.GONE);
    336         }
    337     }
    338 
    339     private void setDescriptionText(CharSequence description) {
    340         mSingleLineDescriptionView.setText(description);
    341         if (mMultiLineDescriptionView != null) {
    342             mMultiLineDescriptionView.setText(description);
    343         }
    344     }
    345 
    346     private void setActionText(@StringRes int actionTextResourceId) {
    347         if (actionTextResourceId == 0) {
    348             mSingleLineActionView.setText("");
    349             if (mMultiLineActionView != null) {
    350                 mMultiLineActionView.setText("");
    351             }
    352         } else {
    353             mSingleLineActionView.setText(actionTextResourceId);
    354             if (mMultiLineActionView != null) {
    355                 mMultiLineActionView.setText(actionTextResourceId);
    356             }
    357         }
    358     }
    359 
    360     private void setActionClickListener(OnClickListener listener) {
    361         mSingleLineActionView.setOnClickListener(listener);
    362 
    363         if (mMultiLineActionView != null) {
    364             mMultiLineActionView.setOnClickListener(listener);
    365         }
    366     }
    367 
    368     /**
    369      * Pops up the toast (and optionally the floating action button) into view via an animation.
    370      */
    371     private void popupToast() {
    372         final float animationDistance = getAnimationDistance();
    373 
    374         setVisibility(View.VISIBLE);
    375         setTranslationY(animationDistance);
    376         animate()
    377                 .setDuration(mAnimationDuration)
    378                 .setInterpolator(mAnimationInterpolator)
    379                 .translationYBy(-animationDistance)
    380                 .setListener(new AnimatorListenerAdapter() {
    381                     @Override
    382                     public void onAnimationStart(Animator animation) {
    383                         mAnimating = true;
    384                     }
    385                     @Override
    386                     public void onAnimationEnd(Animator animation) {
    387                         mAnimating = false;
    388                         mAnimationCompleteTimestamp = System.currentTimeMillis();
    389                     }
    390                 });
    391 
    392         if (mFloatingActionButton != null) {
    393             mFloatingActionButton.setTranslationY(animationDistance);
    394             mFloatingActionButton.animate()
    395                     .setDuration(mAnimationDuration)
    396                     .setInterpolator(mAnimationInterpolator)
    397                     .translationYBy(-animationDistance);
    398         }
    399     }
    400 
    401     /**
    402      * Pushes down the toast (and optionally the floating action button) out of view via an
    403      * animation.
    404      */
    405     private void pushDownToast() {
    406         final float animationDistance = getAnimationDistance();
    407 
    408         setTranslationY(0);
    409         animate()
    410                 .setDuration(mAnimationDuration)
    411                 .setInterpolator(mAnimationInterpolator)
    412                 .translationYBy(animationDistance)
    413                 .setListener(new AnimatorListenerAdapter() {
    414                     @Override
    415                     public void onAnimationStart(Animator animation) {
    416                         mAnimating = true;
    417                     }
    418                     @Override
    419                     public void onAnimationEnd(Animator animation) {
    420                         mAnimating = false;
    421                         // on push down animation completion the toast bar is no longer present
    422                         setVisibility(View.GONE);
    423                     }
    424                 });
    425 
    426         if (mFloatingActionButton != null) {
    427             mFloatingActionButton.setTranslationY(0);
    428             mFloatingActionButton.animate()
    429                     .setDuration(mAnimationDuration)
    430                     .setInterpolator(mAnimationInterpolator)
    431                     .translationYBy(animationDistance)
    432                     .setListener(new AnimatorListenerAdapter() {
    433                         @Override
    434                         public void onAnimationEnd(Animator animation) {
    435                             // on push down animation completion the FAB no longer needs translation
    436                             mFloatingActionButton.setTranslationY(0);
    437                         }
    438                     });
    439         }
    440     }
    441 
    442     /**
    443      * The toast bar is assumed to be positioned at the bottom of the display, so the distance over
    444      * which to animate is the height of the toast bar + any margin beneath the toast bar.
    445      *
    446      * @return the distance to move the toast bar to make it appear to pop up / push down from the
    447      *      bottom of the display
    448      */
    449     private int getAnimationDistance() {
    450         // total height over which the animation takes place is the toast bar height + bottom margin
    451         final ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
    452         return getHeight() + params.bottomMargin;
    453     }
    454 
    455     /**
    456      * Classes that wish to perform some action when the action button is clicked
    457      * should implement this interface.
    458      */
    459     public interface ActionClickedListener {
    460         public void onActionClicked(Context context);
    461     }
    462 }