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