1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.ui.messages.snackbar; 6 7 import android.animation.Animator; 8 import android.animation.AnimatorListenerAdapter; 9 import android.animation.AnimatorSet; 10 import android.animation.ObjectAnimator; 11 import android.app.Activity; 12 import android.graphics.Rect; 13 import android.graphics.drawable.Drawable; 14 import android.graphics.drawable.GradientDrawable; 15 import android.text.TextUtils; 16 import android.view.Gravity; 17 import android.view.LayoutInflater; 18 import android.view.SurfaceView; 19 import android.view.View; 20 import android.view.View.OnClickListener; 21 import android.view.View.OnLayoutChangeListener; 22 import android.view.ViewGroup; 23 import android.widget.FrameLayout; 24 import android.widget.ImageView; 25 import android.widget.TextView; 26 27 import androidx.annotation.Nullable; 28 29 import org.chromium.base.ApiCompatibilityUtils; 30 import org.chromium.chrome.ui.messages.R; 31 import org.chromium.components.browser_ui.widget.animation.Interpolators; 32 import org.chromium.components.browser_ui.widget.text.TemplatePreservingTextView; 33 import org.chromium.ui.base.DeviceFormFactor; 34 import org.chromium.ui.base.WindowAndroid; 35 import org.chromium.ui.interpolators.BakedBezierInterpolator; 36 37 /** 38 * Visual representation of a snackbar. On phone it matches the width of the activity; on tablet it 39 * has a fixed width and is anchored at the start-bottom corner of the current window. 40 */ 41 // TODO (jianli): Change this class and its methods back to package protected after the offline 42 // indicator experiment is done. 43 public class SnackbarView { 44 private static final int MAX_LINES = 5; 45 46 private final WindowAndroid mWindowAndroid; 47 protected final ViewGroup mContainerView; 48 protected final ViewGroup mSnackbarView; 49 protected final TemplatePreservingTextView mMessageView; 50 private final TextView mActionButtonView; 51 private final ImageView mProfileImageView; 52 private final int mAnimationDuration; 53 private final boolean mIsTablet; 54 private ViewGroup mOriginalParent; 55 protected ViewGroup mParent; 56 protected Snackbar mSnackbar; 57 private View mRootContentView; 58 59 // Variables used to calculate the virtual keyboard's height. 60 private Rect mCurrentVisibleRect = new Rect(); 61 private Rect mPreviousVisibleRect = new Rect(); 62 private int[] mTempLocation = new int[2]; 63 64 private OnLayoutChangeListener mLayoutListener = new OnLayoutChangeListener() { 65 @Override 66 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 67 int oldTop, int oldRight, int oldBottom) { 68 adjustViewPosition(); 69 } 70 }; 71 72 /** 73 * Creates an instance of the {@link SnackbarView}. 74 * @param activity The activity that displays the snackbar. 75 * @param listener An {@link OnClickListener} that will be called when the action button is 76 * clicked. 77 * @param snackbar The snackbar to be displayed. 78 * @param parentView The ViewGroup used to display this snackbar. 79 * @param windowAndroid The WindowAndroid used for starting animation. If it is null, 80 * Animator#start is called instead. 81 */ SnackbarView(Activity activity, OnClickListener listener, Snackbar snackbar, ViewGroup parentView, @Nullable WindowAndroid windowAndroid)82 public SnackbarView(Activity activity, OnClickListener listener, Snackbar snackbar, 83 ViewGroup parentView, @Nullable WindowAndroid windowAndroid) { 84 mIsTablet = DeviceFormFactor.isNonMultiDisplayContextOnTablet(activity); 85 mOriginalParent = parentView; 86 mWindowAndroid = windowAndroid; 87 88 mRootContentView = activity.findViewById(android.R.id.content); 89 mParent = mOriginalParent; 90 mContainerView = (ViewGroup) LayoutInflater.from(activity).inflate( 91 R.layout.snackbar, mParent, false); 92 mSnackbarView = mContainerView.findViewById(R.id.snackbar); 93 mAnimationDuration = 94 mContainerView.getResources().getInteger(android.R.integer.config_mediumAnimTime); 95 mMessageView = 96 (TemplatePreservingTextView) mContainerView.findViewById(R.id.snackbar_message); 97 mActionButtonView = (TextView) mContainerView.findViewById(R.id.snackbar_button); 98 mActionButtonView.setOnClickListener(listener); 99 mProfileImageView = (ImageView) mContainerView.findViewById(R.id.snackbar_profile_image); 100 101 updateInternal(snackbar, false); 102 } 103 show()104 public void show() { 105 addToParent(); 106 mContainerView.addOnLayoutChangeListener(new OnLayoutChangeListener() { 107 @Override 108 public void onLayoutChange(View v, int left, int top, int right, int bottom, 109 int oldLeft, int oldTop, int oldRight, int oldBottom) { 110 mContainerView.removeOnLayoutChangeListener(this); 111 mContainerView.setTranslationY(getYPositionForMoveAnimation()); 112 Animator animator = ObjectAnimator.ofFloat(mContainerView, View.TRANSLATION_Y, 0); 113 animator.setInterpolator(Interpolators.DECELERATE_INTERPOLATOR); 114 animator.setDuration(mAnimationDuration); 115 startAnimatorOnSurfaceView(animator); 116 } 117 }); 118 } 119 dismiss()120 public void dismiss() { 121 // Disable action button during animation. 122 mActionButtonView.setEnabled(false); 123 AnimatorSet animatorSet = new AnimatorSet(); 124 animatorSet.setDuration(mAnimationDuration); 125 animatorSet.addListener(new AnimatorListenerAdapter() { 126 @Override 127 public void onAnimationEnd(Animator animation) { 128 mRootContentView.removeOnLayoutChangeListener(mLayoutListener); 129 mParent.removeView(mContainerView); 130 } 131 }); 132 Animator moveAnimator = ObjectAnimator.ofFloat( 133 mContainerView, View.TRANSLATION_Y, getYPositionForMoveAnimation()); 134 moveAnimator.setInterpolator(Interpolators.DECELERATE_INTERPOLATOR); 135 Animator fadeOut = ObjectAnimator.ofFloat(mContainerView, View.ALPHA, 0f); 136 fadeOut.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE); 137 138 animatorSet.playTogether(fadeOut, moveAnimator); 139 startAnimatorOnSurfaceView(animatorSet); 140 } 141 142 /** 143 * Adjusts the position of the snackbar on top of the soft keyboard, if any. 144 */ adjustViewPosition()145 void adjustViewPosition() { 146 mParent.getWindowVisibleDisplayFrame(mCurrentVisibleRect); 147 // Only update if the visible frame has changed, otherwise there will be a layout loop. 148 if (!mCurrentVisibleRect.equals(mPreviousVisibleRect)) { 149 mPreviousVisibleRect.set(mCurrentVisibleRect); 150 151 FrameLayout.LayoutParams lp = getLayoutParams(); 152 153 int prevBottomMargin = lp.bottomMargin; 154 int prevWidth = lp.width; 155 int prevGravity = lp.gravity; 156 157 lp.bottomMargin = getBottomMarginForLayout(); 158 if (mIsTablet) { 159 int margin = mParent.getResources().getDimensionPixelSize( 160 R.dimen.snackbar_margin_tablet); 161 int width = 162 mParent.getResources().getDimensionPixelSize(R.dimen.snackbar_width_tablet); 163 lp.width = Math.min(width, mParent.getWidth() - 2 * margin); 164 lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 165 } 166 167 if (prevBottomMargin != lp.bottomMargin || prevWidth != lp.width 168 || prevGravity != lp.gravity) { 169 mContainerView.setLayoutParams(lp); 170 } 171 } 172 } 173 getYPositionForMoveAnimation()174 protected int getYPositionForMoveAnimation() { 175 return mContainerView.getHeight() + getLayoutParams().bottomMargin; 176 } 177 getBottomMarginForLayout()178 protected int getBottomMarginForLayout() { 179 mParent.getLocationInWindow(mTempLocation); 180 int keyboardHeight = mParent.getHeight() + mTempLocation[1] - mCurrentVisibleRect.bottom; 181 return Math.max(0, keyboardHeight); 182 } 183 184 /** 185 * @see SnackbarManager#overrideParent(ViewGroup) 186 */ overrideParent(ViewGroup overridingParent)187 void overrideParent(ViewGroup overridingParent) { 188 mRootContentView.removeOnLayoutChangeListener(mLayoutListener); 189 mParent = overridingParent == null ? mOriginalParent : overridingParent; 190 if (isShowing()) { 191 ((ViewGroup) mContainerView.getParent()).removeView(mContainerView); 192 } 193 addToParent(); 194 } 195 isShowing()196 boolean isShowing() { 197 return mContainerView.isShown(); 198 } 199 bringToFront()200 void bringToFront() { 201 mContainerView.bringToFront(); 202 } 203 204 /** 205 * Sends an accessibility event to mMessageView announcing that this window was added so that 206 * the mMessageView content description is read aloud if accessibility is enabled. 207 */ announceforAccessibility()208 public void announceforAccessibility() { 209 StringBuilder accessibilityText = new StringBuilder(mMessageView.getContentDescription()); 210 if (mActionButtonView.getContentDescription() != null) { 211 accessibilityText.append(". ") 212 .append(mActionButtonView.getContentDescription()) 213 .append(". ") 214 .append(mContainerView.getResources().getString( 215 R.string.bottom_bar_screen_position)); 216 } 217 218 mMessageView.announceForAccessibility(accessibilityText); 219 } 220 221 /** 222 * Sends an accessibility event to mContainerView announcing that an action was taken based on 223 * the action button being pressed. May do nothing if no announcement was specified. 224 */ announceActionForAccessibility()225 public void announceActionForAccessibility() { 226 if (TextUtils.isEmpty(mSnackbar.getActionAccessibilityAnnouncement())) return; 227 mContainerView.announceForAccessibility(mSnackbar.getActionAccessibilityAnnouncement()); 228 } 229 230 /** 231 * Updates the view to display data from the given snackbar. No-op if the view is already 232 * showing the given snackbar. 233 * @param snackbar The snackbar to display 234 * @return Whether update has actually been executed. 235 */ update(Snackbar snackbar)236 boolean update(Snackbar snackbar) { 237 return updateInternal(snackbar, true); 238 } 239 addToParent()240 private void addToParent() { 241 mParent.addView(mContainerView); 242 243 // Why setting listener on parent? It turns out that if we force a relayout in the layout 244 // change listener of the view itself, the force layout flag will be reset to 0 when 245 // layout() returns. Therefore we have to do request layout on one level above the requested 246 // view. 247 mRootContentView.addOnLayoutChangeListener(mLayoutListener); 248 } 249 250 // TODO(fgorski): Start using color ID, to remove the view from arguments. getBackgroundColor(View view, Snackbar snackbar)251 private static int getBackgroundColor(View view, Snackbar snackbar) { 252 // Themes are used first. 253 if (snackbar.getTheme() == Snackbar.Theme.GOOGLE) { 254 return ApiCompatibilityUtils.getColor( 255 view.getResources(), R.color.default_control_color_active); 256 } 257 258 assert snackbar.getTheme() == Snackbar.Theme.BASIC; 259 if (snackbar.getBackgroundColor() != 0) { 260 return snackbar.getBackgroundColor(); 261 } 262 263 return ApiCompatibilityUtils.getColor( 264 view.getResources(), R.color.snackbar_background_color); 265 } 266 getTextAppearance(Snackbar snackbar)267 private static int getTextAppearance(Snackbar snackbar) { 268 if (snackbar.getTheme() == Snackbar.Theme.GOOGLE) { 269 return R.style.TextAppearance_TextMedium_Primary_Inverse; 270 } 271 272 assert snackbar.getTheme() == Snackbar.Theme.BASIC; 273 if (snackbar.getTextAppearance() != 0) { 274 return snackbar.getTextAppearance(); 275 } 276 277 return R.style.TextAppearance_TextMedium_Primary; 278 } 279 getButtonTextAppearance(Snackbar snackbar)280 private static int getButtonTextAppearance(Snackbar snackbar) { 281 if (snackbar.getTheme() == Snackbar.Theme.GOOGLE) { 282 return R.style.TextAppearance_Button_Text_Filled; 283 } 284 285 assert snackbar.getTheme() == Snackbar.Theme.BASIC; 286 return R.style.TextButton; 287 } 288 updateInternal(Snackbar snackbar, boolean animate)289 private boolean updateInternal(Snackbar snackbar, boolean animate) { 290 if (mSnackbar == snackbar) return false; 291 mSnackbar = snackbar; 292 mMessageView.setMaxLines(snackbar.getSingleLine() ? 1 : MAX_LINES); 293 mMessageView.setTemplate(snackbar.getTemplateText()); 294 setViewText(mMessageView, snackbar.getText(), animate); 295 296 ApiCompatibilityUtils.setTextAppearance(mMessageView, getTextAppearance(snackbar)); 297 ApiCompatibilityUtils.setTextAppearance( 298 mActionButtonView, getButtonTextAppearance(snackbar)); 299 300 int backgroundColor = getBackgroundColor(mContainerView, snackbar); 301 if (mIsTablet) { 302 // On tablet, snackbars have rounded corners. 303 mSnackbarView.setBackgroundResource(R.drawable.snackbar_background_tablet); 304 GradientDrawable backgroundDrawable = 305 (GradientDrawable) mSnackbarView.getBackground().mutate(); 306 backgroundDrawable.setColor(backgroundColor); 307 } else { 308 mSnackbarView.setBackgroundColor(backgroundColor); 309 } 310 311 if (snackbar.getActionText() != null) { 312 mActionButtonView.setVisibility(View.VISIBLE); 313 mActionButtonView.setContentDescription(snackbar.getActionText()); 314 setViewText(mActionButtonView, snackbar.getActionText(), animate); 315 } else { 316 mActionButtonView.setVisibility(View.GONE); 317 } 318 Drawable profileImage = snackbar.getProfileImage(); 319 if (profileImage != null) { 320 mProfileImageView.setVisibility(View.VISIBLE); 321 mProfileImageView.setImageDrawable(profileImage); 322 } else { 323 mProfileImageView.setVisibility(View.GONE); 324 } 325 326 if (mIsTablet) { 327 mContainerView.findViewById(R.id.snackbar_shadow_left).setVisibility(View.VISIBLE); 328 mContainerView.findViewById(R.id.snackbar_shadow_right).setVisibility(View.VISIBLE); 329 } 330 331 return true; 332 } 333 334 /** 335 * Starts the {@link Animator} with {@link SurfaceView} optimization disabled. If a 336 * {@link SurfaceView} is not present (mWindowAndroid is null), start the {@link Animator} 337 * in the normal way. 338 */ startAnimatorOnSurfaceView(Animator animator)339 private void startAnimatorOnSurfaceView(Animator animator) { 340 if (mWindowAndroid != null) { 341 mWindowAndroid.startAnimationOverContent(animator); 342 } else { 343 animator.start(); 344 } 345 } 346 getLayoutParams()347 private FrameLayout.LayoutParams getLayoutParams() { 348 return (FrameLayout.LayoutParams) mContainerView.getLayoutParams(); 349 } 350 setViewText(TextView view, CharSequence text, boolean animate)351 private void setViewText(TextView view, CharSequence text, boolean animate) { 352 if (view.getText().toString().equals(text)) return; 353 view.animate().cancel(); 354 if (animate) { 355 view.setAlpha(0.0f); 356 view.setText(text); 357 view.animate().alpha(1.f).setDuration(mAnimationDuration).setListener(null); 358 } else { 359 view.setText(text); 360 } 361 } 362 } 363