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 }