blob: c6bf99090f17277ce34e543ea0eb297a9f9a57c1 [file] [log] [blame]
/**
* 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);
}
}