// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.snackbar;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.CoordinatorLayout.LayoutParams;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.compositor.CompositorViewHolderBehavior;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.interpolators.BakedBezierInterpolator;
/**
* Visual representation of a snackbar. On phone it matches the width of the activity; on tablet it
* has a fixed width and is anchored at the start-bottom corner of the current window.
*/
class SnackbarView {
private static final int MAX_LINES = 5;
private final Activity mActivity;
private final ViewGroup mView;
private final TemplatePreservingTextView mMessageView;
private final TextView mActionButtonView;
private final ImageView mProfileImageView;
private final int mAnimationDuration;
private final boolean mIsTablet;
private ViewGroup mOriginalParent;
private ViewGroup mParent;
private Snackbar mSnackbar;
// Variables used to calculate the virtual keyboard's height.
private Rect mCurrentVisibleRect = new Rect();
private Rect mPreviousVisibleRect = new Rect();
private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
adjustViewPosition();
}
};
/**
* Creates an instance of the {@link SnackbarView}.
* @param activity The activity that displays the snackbar.
* @param listener An {@link OnClickListener} that will be called when the action button is
* clicked.
* @param snackbar The snackbar to be displayed.
*/
SnackbarView(Activity activity, OnClickListener listener, Snackbar snackbar) {
mActivity = activity;
mIsTablet = DeviceFormFactor.isTablet(activity);
mOriginalParent = findParentView(activity);
mParent = mOriginalParent;
mView = (ViewGroup) LayoutInflater.from(activity).inflate(
R.layout.snackbar, mParent, false);
mAnimationDuration = mView.getResources()
.getInteger(android.R.integer.config_mediumAnimTime);
mMessageView = (TemplatePreservingTextView) mView.findViewById(R.id.snackbar_message);
mActionButtonView = (TextView) mView.findViewById(R.id.snackbar_button);
mActionButtonView.setOnClickListener(listener);
mProfileImageView = (ImageView) mView.findViewById(R.id.snackbar_profile_image);
updateInternal(snackbar, false);
}
void show() {
addToParent();
mView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
mView.removeOnLayoutChangeListener(this);
mView.setTranslationY(mView.getHeight() + getLayoutParams().bottomMargin);
Animator animator = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, 0);
animator.setInterpolator(new DecelerateInterpolator());
animator.setDuration(mAnimationDuration);
startAnimatorOnSurfaceView(animator);
}
});
}
void dismiss() {
// Disable action button during animation.
mActionButtonView.setEnabled(false);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(mAnimationDuration);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mParent.removeOnLayoutChangeListener(mLayoutListener);
mParent.removeView(mView);
}
});
Animator moveDown = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y,
mView.getHeight() + getLayoutParams().bottomMargin);
moveDown.setInterpolator(new DecelerateInterpolator());
Animator fadeOut = ObjectAnimator.ofFloat(mView, View.ALPHA, 0f);
fadeOut.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);
animatorSet.playTogether(fadeOut, moveDown);
startAnimatorOnSurfaceView(animatorSet);
}
void adjustViewPosition() {
mParent.getWindowVisibleDisplayFrame(mCurrentVisibleRect);
// Only update if the visible frame has changed, otherwise there will be a layout loop.
if (!mCurrentVisibleRect.equals(mPreviousVisibleRect)) {
mPreviousVisibleRect.set(mCurrentVisibleRect);
int keyboardHeight = mParent.getHeight() - mCurrentVisibleRect.bottom
+ mCurrentVisibleRect.top;
MarginLayoutParams lp = getLayoutParams();
lp.bottomMargin = keyboardHeight;
if (mIsTablet) {
int margin = mParent.getResources()
.getDimensionPixelSize(R.dimen.snackbar_margin_tablet);
ApiCompatibilityUtils.setMarginStart(lp, margin);
lp.bottomMargin += margin;
int width = mParent.getResources()
.getDimensionPixelSize(R.dimen.snackbar_width_tablet);
lp.width = Math.min(width, mParent.getWidth() - 2 * margin);
}
mView.setLayoutParams(lp);
}
}
/**
* @see SnackbarManager#overrideParent(ViewGroup)
*/
void overrideParent(ViewGroup overridingParent) {
mParent.removeOnLayoutChangeListener(mLayoutListener);
mParent = overridingParent == null ? mOriginalParent : overridingParent;
if (isShowing()) {
((ViewGroup) mView.getParent()).removeView(mView);
}
addToParent();
}
boolean isShowing() {
return mView.isShown();
}
/**
* Sends an accessibility event to mMessageView announcing that this window was added so that
* the mMessageView content description is read aloud if accessibility is enabled.
*/
void announceforAccessibility() {
mMessageView.announceForAccessibility(mMessageView.getContentDescription()
+ mView.getResources().getString(R.string.bottom_bar_screen_position));
}
/**
* Updates the view to display data from the given snackbar. No-op if the view is already
* showing the given snackbar.
* @param snackbar The snackbar to display
* @return Whether update has actually been executed.
*/
boolean update(Snackbar snackbar) {
return updateInternal(snackbar, true);
}
private void addToParent() {
// LayoutParams in CoordinatorLayout and FrameLayout cannot be used interchangeably. Thus
// we create new LayoutParams every time.
if (mParent instanceof CoordinatorLayout) {
CoordinatorLayout.LayoutParams lp = new LayoutParams(getLayoutParams());
lp.gravity = Gravity.BOTTOM | Gravity.START;
lp.setBehavior(new CompositorViewHolderBehavior());
mParent.addView(mView, lp);
} else if (mParent instanceof FrameLayout) {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getLayoutParams());
lp.gravity = Gravity.BOTTOM | Gravity.START;
mParent.addView(mView, lp);
} else {
assert false : "Only FrameLayout and CoordinatorLayout are supported to show snackbars";
}
// Why setting listener on parent? It turns out that if we force a relayout in the layout
// change listener of the view itself, the force layout flag will be reset to 0 when
// layout() returns. Therefore we have to do request layout on one level above the requested
// view.
mParent.addOnLayoutChangeListener(mLayoutListener);
}
private boolean updateInternal(Snackbar snackbar, boolean animate) {
if (mSnackbar == snackbar) return false;
mSnackbar = snackbar;
mMessageView.setMaxLines(snackbar.getSingleLine() ? 1 : MAX_LINES);
mMessageView.setTemplate(snackbar.getTemplateText());
setViewText(mMessageView, snackbar.getText(), animate);
String actionText = snackbar.getActionText();
int backgroundColor = snackbar.getBackgroundColor();
if (backgroundColor == 0) {
backgroundColor = ApiCompatibilityUtils.getColor(mView.getResources(),
R.color.snackbar_background_color);
}
if (mIsTablet) {
// On tablet, snackbars have rounded corners.
mView.setBackgroundResource(R.drawable.snackbar_background_tablet);
GradientDrawable backgroundDrawable = (GradientDrawable) mView.getBackground().mutate();
backgroundDrawable.setColor(backgroundColor);
} else {
mView.setBackgroundColor(backgroundColor);
}
if (actionText != null) {
mActionButtonView.setVisibility(View.VISIBLE);
setViewText(mActionButtonView, snackbar.getActionText(), animate);
} else {
mActionButtonView.setVisibility(View.GONE);
}
Bitmap profileImage = snackbar.getProfileImage();
if (profileImage != null) {
mProfileImageView.setVisibility(View.VISIBLE);
mProfileImageView.setImageBitmap(profileImage);
} else {
mProfileImageView.setVisibility(View.GONE);
}
return true;
}
/**
* @return The parent {@link ViewGroup} that {@link #mView} will be added to.
*/
private ViewGroup findParentView(Activity activity) {
if (activity instanceof ChromeActivity) {
return ((ChromeActivity) activity).getCompositorViewHolder();
} else {
return (ViewGroup) activity.findViewById(android.R.id.content);
}
}
/**
* Starts the {@link Animator} with {@link SurfaceView} optimization disabled. If a
* {@link SurfaceView} is not present in the given {@link Activity}, start the {@link Animator}
* in the normal way.
*/
private void startAnimatorOnSurfaceView(Animator animator) {
if (mActivity instanceof ChromeActivity) {
((ChromeActivity) mActivity).getWindowAndroid().startAnimationOverContent(animator);
} else {
animator.start();
}
}
private MarginLayoutParams getLayoutParams() {
return (MarginLayoutParams) mView.getLayoutParams();
}
private void setViewText(TextView view, CharSequence text, boolean animate) {
if (view.getText().toString().equals(text)) return;
view.animate().cancel();
if (animate) {
view.setAlpha(0.0f);
view.setText(text);
view.animate().alpha(1.f).setDuration(mAnimationDuration).setListener(null);
} else {
view.setText(text);
}
}
}