| /** |
| * Copyright (c) 2011, Google Inc. |
| * |
| * 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.mail.ui; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.TimeInterpolator; |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.os.Handler; |
| import android.support.annotation.StringRes; |
| import android.text.TextUtils; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.animation.LinearInterpolator; |
| import android.view.animation.PathInterpolator; |
| import android.widget.FrameLayout; |
| import android.widget.TextView; |
| |
| import com.android.mail.R; |
| import com.android.mail.utils.Utils; |
| import com.android.mail.utils.ViewUtils; |
| |
| /** |
| * A custom {@link View} that exposes an action to the user. |
| */ |
| public class ActionableToastBar extends FrameLayout { |
| |
| private boolean mHidden = true; |
| private final Runnable mHideToastBarRunnable; |
| private final Handler mHideToastBarHandler; |
| |
| /** |
| * The floating action button if it must be animated with the toast bar; <code>null</code> |
| * otherwise. |
| */ |
| private View mFloatingActionButton; |
| |
| /** |
| * <tt>true</tt> while animation is occurring; false otherwise; It is used to block attempts to |
| * hide the toast bar while it is being animated |
| */ |
| private boolean mAnimating = false; |
| |
| /** The interpolator that produces position values during animation. */ |
| private TimeInterpolator mAnimationInterpolator; |
| |
| /** The length of time (in milliseconds) that the popup / push down animation run over */ |
| private int mAnimationDuration; |
| |
| /** |
| * The time at which the toast popup completed. This is used to ensure the toast remains |
| * visible for a minimum duration before it is removed. |
| */ |
| private long mAnimationCompleteTimestamp; |
| |
| /** The min time duration for which the toast must remain visible and cannot be dismissed. */ |
| private long mMinToastDuration; |
| |
| /** The max time duration for which the toast can remain visible and must be dismissed. */ |
| private long mMaxToastDuration; |
| |
| /** The view that contains the description when laid out as a single line. */ |
| private TextView mSingleLineDescriptionView; |
| |
| /** The view that contains the text for the action button when laid out as a single line. */ |
| private TextView mSingleLineActionView; |
| |
| /** The view that contains the description when laid out as a multiple lines; |
| * always <tt>null</tt> in two-pane layouts. */ |
| private TextView mMultiLineDescriptionView; |
| |
| /** The view that contains the text for the action button when laid out as a multiple lines; |
| * always <tt>null</tt> in two-pane layouts. */ |
| private TextView mMultiLineActionView; |
| |
| /** The minimum width of this view; applicable when description text is very short. */ |
| private int mMinWidth; |
| |
| /** The maximum width of this view; applicable when description text is long enough to wrap. */ |
| private int mMaxWidth; |
| |
| private ToastBarOperation mOperation; |
| |
| public ActionableToastBar(Context context) { |
| this(context, null); |
| } |
| |
| public ActionableToastBar(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public ActionableToastBar(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| mAnimationInterpolator = createTimeInterpolator(); |
| mAnimationDuration = getResources().getInteger(R.integer.toast_bar_animation_duration_ms); |
| mMinToastDuration = getResources().getInteger(R.integer.toast_bar_min_duration_ms); |
| mMaxToastDuration = getResources().getInteger(R.integer.toast_bar_max_duration_ms); |
| mMinWidth = getResources().getDimensionPixelOffset(R.dimen.snack_bar_min_width); |
| mMaxWidth = getResources().getDimensionPixelOffset(R.dimen.snack_bar_max_width); |
| mHideToastBarHandler = new Handler(); |
| mHideToastBarRunnable = new Runnable() { |
| @Override |
| public void run() { |
| if (!mHidden) { |
| hide(true, false /* actionClicked */); |
| } |
| } |
| }; |
| } |
| |
| private TimeInterpolator createTimeInterpolator() { |
| // L and beyond we can use the new PathInterpolator |
| if (Utils.isRunningLOrLater()) { |
| return createPathInterpolator(); |
| } |
| |
| // fall back to basic LinearInterpolator |
| return new LinearInterpolator(); |
| } |
| |
| @TargetApi(21) |
| private TimeInterpolator createPathInterpolator() { |
| return new PathInterpolator(0.4f, 0f, 0.2f, 1f); |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| |
| mSingleLineDescriptionView = (TextView) findViewById(R.id.description_text); |
| mSingleLineActionView = (TextView) findViewById(R.id.action_text); |
| mMultiLineDescriptionView = (TextView) findViewById(R.id.multiline_description_text); |
| mMultiLineActionView = (TextView) findViewById(R.id.multiline_action_text); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| final boolean showAction = !TextUtils.isEmpty(mSingleLineActionView.getText()); |
| |
| // configure the UI assuming the description fits on a single line |
| setVisibility(false /* multiLine */, showAction); |
| |
| // measure the view and its content |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| |
| // if specific views exist to handle the multiline case |
| if (mMultiLineDescriptionView != null) { |
| // if the description does not fit on a single line |
| if (mSingleLineDescriptionView.getLineCount() > 1) { |
| //switch to multi line display views |
| setVisibility(true /* multiLine */, showAction); |
| |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| // if width constraints were given explicitly, honor them; otherwise use the natural width |
| } else if (mMinWidth >= 0 && mMaxWidth >= 0) { |
| // otherwise, adjust the the single line view so wrapping occurs at the desired width |
| // (the total width of the toast bar must always fall between the given min and max |
| // width; if max width cannot accommodate all of the description text, it wraps) |
| if (getMeasuredWidth() < mMinWidth) { |
| widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mMinWidth, MeasureSpec.EXACTLY); |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } else if (getMeasuredWidth() > mMaxWidth) { |
| widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY); |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| } |
| } |
| |
| /** |
| * Displays the toast bar and makes it visible. Allows the setting of |
| * parameters to customize the display. |
| * @param listener Performs some action when the action button is clicked. |
| * If the {@link ToastBarOperation} overrides |
| * {@link ToastBarOperation#shouldTakeOnActionClickedPrecedence()} |
| * to return <code>true</code>, the |
| * {@link ToastBarOperation#onActionClicked(android.content.Context)} |
| * will override this listener and be called instead. |
| * @param descriptionText a description text to show in the toast bar |
| * @param actionTextResourceId resource ID for the text to show in the action button |
| * @param replaceVisibleToast if true, this toast should replace any currently visible toast. |
| * Otherwise, skip showing this toast. |
| * @param autohide <tt>true</tt> indicates the toast will be automatically hidden after a time |
| * delay; <tt>false</tt> indicate the toast will remain visible until the user |
| * dismisses it |
| * @param op the operation that corresponds to the specific toast being shown |
| */ |
| public void show(final ActionClickedListener listener, final CharSequence descriptionText, |
| @StringRes final int actionTextResourceId, final boolean replaceVisibleToast, |
| final boolean autohide, final ToastBarOperation op) { |
| if (!mHidden && !replaceVisibleToast) { |
| return; |
| } |
| |
| // Remove any running delayed animations first |
| mHideToastBarHandler.removeCallbacks(mHideToastBarRunnable); |
| |
| mOperation = op; |
| |
| setActionClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View widget) { |
| if (op != null && op.shouldTakeOnActionClickedPrecedence()) { |
| op.onActionClicked(getContext()); |
| } else { |
| listener.onActionClicked(getContext()); |
| } |
| hide(true /* animate */, true /* actionClicked */); |
| } |
| }); |
| |
| setDescriptionText(descriptionText); |
| ViewUtils.announceForAccessibility(this, descriptionText); |
| setActionText(actionTextResourceId); |
| |
| // if this toast bar is not yet hidden, animate it in place; otherwise we just update the |
| // text that it displays |
| if (mHidden) { |
| mHidden = false; |
| popupToast(); |
| } |
| |
| if (autohide) { |
| // Set up runnable to execute hide toast once delay is completed |
| mHideToastBarHandler.postDelayed(mHideToastBarRunnable, mMaxToastDuration); |
| } |
| } |
| |
| public ToastBarOperation getOperation() { |
| return mOperation; |
| } |
| |
| /** |
| * Hides the view and resets the state. |
| */ |
| public void hide(boolean animate, boolean actionClicked) { |
| mHidden = true; |
| mAnimationCompleteTimestamp = 0; |
| mHideToastBarHandler.removeCallbacks(mHideToastBarRunnable); |
| if (getVisibility() == View.VISIBLE) { |
| setActionClickListener(null); |
| // Hide view once it's clicked. |
| if (animate) { |
| pushDownToast(); |
| } else { |
| // immediate hiding implies no position adjustment of the FAB and hide the toast bar |
| if (mFloatingActionButton != null) { |
| mFloatingActionButton.setTranslationY(0); |
| } |
| setVisibility(View.GONE); |
| } |
| |
| if (!actionClicked && mOperation != null) { |
| mOperation.onToastBarTimeout(getContext()); |
| } |
| } |
| } |
| |
| /** |
| * @return <tt>true</tt> while the toast bar animation is popping up or pushing down the toast; |
| * <tt>false</tt> otherwise |
| */ |
| public boolean isAnimating() { |
| return mAnimating; |
| } |
| |
| /** |
| * @return <tt>true</tt> if this toast bar has not yet been displayed for a long enough period |
| * of time to be dismissed; <tt>false</tt> otherwise |
| */ |
| public boolean cannotBeHidden() { |
| return System.currentTimeMillis() - mAnimationCompleteTimestamp < mMinToastDuration; |
| } |
| |
| @Override |
| public void onDetachedFromWindow() { |
| mHideToastBarHandler.removeCallbacks(mHideToastBarRunnable); |
| super.onDetachedFromWindow(); |
| } |
| |
| public boolean isEventInToastBar(MotionEvent event) { |
| if (!isShown()) { |
| return false; |
| } |
| int[] xy = new int[2]; |
| float x = event.getX(); |
| float y = event.getY(); |
| getLocationOnScreen(xy); |
| return (x > xy[0] && x < (xy[0] + getWidth()) && y > xy[1] && y < xy[1] + getHeight()); |
| } |
| |
| /** |
| * Indicates that the given view should be animated with this toast bar as it pops up and pushes |
| * down. In some layouts, the floating action button appears above the toast bar and thus must |
| * be pushed up as the toast pops up and fall down as the toast is pushed down. |
| * |
| * @param floatingActionButton a the floating action button to be animated with the toast bar as |
| * it pops up and pushes down |
| */ |
| public void setFloatingActionButton(View floatingActionButton) { |
| mFloatingActionButton = floatingActionButton; |
| } |
| |
| /** |
| * If the View requires multiple lines to fully display the toast description then make the |
| * multi-line view visible and hide the single line view; otherwise vice versa. If the action |
| * text is present, display it, otherwise hide it. |
| * |
| * @param multiLine <tt>true</tt> if the View requires multiple lines to display the toast |
| * @param showAction <tt>true</tt> if the action text is present and should be shown |
| */ |
| private void setVisibility(boolean multiLine, boolean showAction) { |
| mSingleLineDescriptionView.setVisibility(!multiLine ? View.VISIBLE : View.GONE); |
| mSingleLineActionView.setVisibility(!multiLine && showAction ? View.VISIBLE : View.GONE); |
| if (mMultiLineDescriptionView != null) { |
| mMultiLineDescriptionView.setVisibility(multiLine ? View.VISIBLE : View.GONE); |
| } |
| if (mMultiLineActionView != null) { |
| mMultiLineActionView.setVisibility(multiLine && showAction ? View.VISIBLE : View.GONE); |
| } |
| } |
| |
| private void setDescriptionText(CharSequence description) { |
| mSingleLineDescriptionView.setText(description); |
| if (mMultiLineDescriptionView != null) { |
| mMultiLineDescriptionView.setText(description); |
| } |
| } |
| |
| private void setActionText(@StringRes int actionTextResourceId) { |
| if (actionTextResourceId == 0) { |
| mSingleLineActionView.setText(""); |
| if (mMultiLineActionView != null) { |
| mMultiLineActionView.setText(""); |
| } |
| } else { |
| mSingleLineActionView.setText(actionTextResourceId); |
| if (mMultiLineActionView != null) { |
| mMultiLineActionView.setText(actionTextResourceId); |
| } |
| } |
| } |
| |
| private void setActionClickListener(OnClickListener listener) { |
| mSingleLineActionView.setOnClickListener(listener); |
| |
| if (mMultiLineActionView != null) { |
| mMultiLineActionView.setOnClickListener(listener); |
| } |
| } |
| |
| /** |
| * Pops up the toast (and optionally the floating action button) into view via an animation. |
| */ |
| private void popupToast() { |
| final float animationDistance = getAnimationDistance(); |
| |
| setVisibility(View.VISIBLE); |
| setTranslationY(animationDistance); |
| animate() |
| .setDuration(mAnimationDuration) |
| .setInterpolator(mAnimationInterpolator) |
| .translationYBy(-animationDistance) |
| .setListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mAnimating = true; |
| } |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mAnimating = false; |
| mAnimationCompleteTimestamp = System.currentTimeMillis(); |
| } |
| }); |
| |
| if (mFloatingActionButton != null) { |
| mFloatingActionButton.setTranslationY(animationDistance); |
| mFloatingActionButton.animate() |
| .setDuration(mAnimationDuration) |
| .setInterpolator(mAnimationInterpolator) |
| .translationYBy(-animationDistance); |
| } |
| } |
| |
| /** |
| * Pushes down the toast (and optionally the floating action button) out of view via an |
| * animation. |
| */ |
| private void pushDownToast() { |
| final float animationDistance = getAnimationDistance(); |
| |
| setTranslationY(0); |
| animate() |
| .setDuration(mAnimationDuration) |
| .setInterpolator(mAnimationInterpolator) |
| .translationYBy(animationDistance) |
| .setListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| mAnimating = true; |
| } |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mAnimating = false; |
| // on push down animation completion the toast bar is no longer present |
| setVisibility(View.GONE); |
| } |
| }); |
| |
| if (mFloatingActionButton != null) { |
| mFloatingActionButton.setTranslationY(0); |
| mFloatingActionButton.animate() |
| .setDuration(mAnimationDuration) |
| .setInterpolator(mAnimationInterpolator) |
| .translationYBy(animationDistance) |
| .setListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| // on push down animation completion the FAB no longer needs translation |
| mFloatingActionButton.setTranslationY(0); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * The toast bar is assumed to be positioned at the bottom of the display, so the distance over |
| * which to animate is the height of the toast bar + any margin beneath the toast bar. |
| * |
| * @return the distance to move the toast bar to make it appear to pop up / push down from the |
| * bottom of the display |
| */ |
| private int getAnimationDistance() { |
| // total height over which the animation takes place is the toast bar height + bottom margin |
| final ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams(); |
| return getHeight() + params.bottomMargin; |
| } |
| |
| /** |
| * Classes that wish to perform some action when the action button is clicked |
| * should implement this interface. |
| */ |
| public interface ActionClickedListener { |
| public void onActionClicked(Context context); |
| } |
| } |