1 // Copyright 2013 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.weblayer_private; 6 7 import android.animation.Animator; 8 import android.animation.AnimatorListenerAdapter; 9 import android.content.Context; 10 import android.view.Gravity; 11 import android.view.View; 12 import android.view.ViewGroup; 13 import android.widget.FrameLayout; 14 import android.widget.RelativeLayout; 15 16 import androidx.annotation.NonNull; 17 import androidx.annotation.VisibleForTesting; 18 19 import org.chromium.base.MathUtils; 20 import org.chromium.components.browser_ui.banners.SwipableOverlayView; 21 import org.chromium.components.infobars.InfoBar; 22 import org.chromium.components.infobars.InfoBarAnimationListener; 23 import org.chromium.components.infobars.InfoBarContainerLayout; 24 import org.chromium.components.infobars.InfoBarUiItem; 25 import org.chromium.ui.display.DisplayAndroid; 26 import org.chromium.ui.display.DisplayUtil; 27 28 /** 29 * The {@link View} for the {@link InfoBarContainer}. 30 */ 31 public class InfoBarContainerView extends SwipableOverlayView { 32 /** 33 * Observes container view changes. 34 */ 35 public interface ContainerViewObserver extends InfoBarAnimationListener { 36 /** 37 * Called when the height of shown content changed. 38 * @param shownFraction The ratio of height of shown content to the height of the container 39 * view. 40 */ onShownRatioChanged(float shownFraction)41 void onShownRatioChanged(float shownFraction); 42 } 43 44 /** Top margin, including the toolbar and tabstrip height and 48dp of web contents. */ 45 private static final int TOP_MARGIN_PHONE_DP = 104; 46 private static final int TOP_MARGIN_TABLET_DP = 144; 47 48 /** Length of the animation to fade the InfoBarContainer back into View. */ 49 private static final long REATTACH_FADE_IN_MS = 250; 50 51 /** Whether or not the InfoBarContainer is allowed to hide when the user scrolls. */ 52 private static boolean sIsAllowedToAutoHide = true; 53 54 private final ContainerViewObserver mContainerViewObserver; 55 private final InfoBarContainerLayout mLayout; 56 57 /** Parent view that contains the InfoBarContainerLayout. */ 58 private ViewGroup mParentView; 59 60 private TabImpl mTab; 61 62 /** Animation used to snap the container to the nearest state if scroll direction changes. */ 63 private Animator mScrollDirectionChangeAnimation; 64 65 /** Whether or not the current scroll is downward. */ 66 private boolean mIsScrollingDownward; 67 68 /** Tracks the previous event's scroll offset to determine if a scroll is up or down. */ 69 private int mLastScrollOffsetY; 70 71 /** 72 * @param context The {@link Context} that this view is attached to. 73 * @param containerViewObserver The {@link ContainerViewObserver} that gets notified on 74 * container view changes. 75 * @param isTablet Whether this view is displayed on tablet or not. 76 */ InfoBarContainerView(@onNull Context context, @NonNull ContainerViewObserver containerViewObserver, TabImpl tab, boolean isTablet)77 InfoBarContainerView(@NonNull Context context, 78 @NonNull ContainerViewObserver containerViewObserver, TabImpl tab, boolean isTablet) { 79 super(context, null); 80 mTab = tab; 81 mContainerViewObserver = containerViewObserver; 82 83 // TODO(newt): move this workaround into the infobar views if/when they're scrollable. 84 // Workaround for http://crbug.com/407149. See explanation in onMeasure() below. 85 setVerticalScrollBarEnabled(false); 86 87 updateLayoutParams(context, isTablet); 88 89 Runnable makeContainerVisibleRunnable = () -> runUpEventAnimation(true); 90 mLayout = new InfoBarContainerLayout( 91 context, makeContainerVisibleRunnable, new InfoBarAnimationListener() { 92 @Override 93 public void notifyAnimationFinished(int animationType) { 94 mContainerViewObserver.notifyAnimationFinished(animationType); 95 } 96 97 @Override 98 public void notifyAllAnimationsFinished(InfoBarUiItem frontInfoBar) { 99 mContainerViewObserver.notifyAllAnimationsFinished(frontInfoBar); 100 } 101 }); 102 103 addView(mLayout, 104 new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, 105 Gravity.CENTER_HORIZONTAL)); 106 } 107 destroy()108 void destroy() { 109 removeFromParentView(); 110 mTab = null; 111 } 112 113 // SwipableOverlayView implementation. 114 @Override 115 @VisibleForTesting isAllowedToAutoHide()116 public boolean isAllowedToAutoHide() { 117 return sIsAllowedToAutoHide; 118 } 119 120 @Override onAttachedToWindow()121 protected void onAttachedToWindow() { 122 super.onAttachedToWindow(); 123 if (getVisibility() != View.GONE) { 124 setVisibility(VISIBLE); 125 setAlpha(0f); 126 animate().alpha(1f).setDuration(REATTACH_FADE_IN_MS); 127 } 128 } 129 130 @Override runUpEventAnimation(boolean visible)131 protected void runUpEventAnimation(boolean visible) { 132 if (mScrollDirectionChangeAnimation != null) mScrollDirectionChangeAnimation.cancel(); 133 super.runUpEventAnimation(visible); 134 } 135 136 @Override isIndependentlyAnimating()137 protected boolean isIndependentlyAnimating() { 138 return mScrollDirectionChangeAnimation != null; 139 } 140 141 // View implementation. 142 @Override setTranslationY(float translationY)143 public void setTranslationY(float translationY) { 144 int contentHeightDelta = mTab != null 145 ? mTab.getBrowser().getViewController().getBottomContentHeightDelta() 146 : 0; 147 148 // Push the infobar container up by any delta caused by the bottom toolbar while ensuring 149 // that it does not ascend beyond the top of the bottom toolbar nor descend beyond its own 150 // height. 151 float newTranslationY = MathUtils.clamp( 152 translationY - contentHeightDelta, -contentHeightDelta, getHeight()); 153 154 super.setTranslationY(newTranslationY); 155 156 float shownFraction = 0; 157 if (getHeight() > 0) { 158 shownFraction = contentHeightDelta > 0 ? 1f : 1f - (translationY / getHeight()); 159 } 160 mContainerViewObserver.onShownRatioChanged(shownFraction); 161 } 162 163 /** 164 * Sets whether the InfoBarContainer is allowed to auto-hide when the user scrolls the page. 165 * Expected to be called when Touch Exploration is enabled. 166 * @param isAllowed Whether auto-hiding is allowed. 167 */ setIsAllowedToAutoHide(boolean isAllowed)168 public static void setIsAllowedToAutoHide(boolean isAllowed) { 169 sIsAllowedToAutoHide = isAllowed; 170 } 171 172 /** 173 * Notifies that an infobar's View ({@link InfoBar#getView}) has changed. If the infobar is 174 * visible, a view swapping animation will be run. 175 */ notifyInfoBarViewChanged()176 void notifyInfoBarViewChanged() { 177 mLayout.notifyInfoBarViewChanged(); 178 } 179 180 /** 181 * Sets the parent {@link ViewGroup} that contains the {@link InfoBarContainer}. 182 */ setParentView(ViewGroup parent)183 void setParentView(ViewGroup parent) { 184 mParentView = parent; 185 // Don't attach the container to the new parent if it is not previously attached. 186 if (removeFromParentView()) addToParentView(); 187 } 188 189 /** 190 * Adds this class to the parent view {@link #mParentView}. 191 */ addToParentView()192 void addToParentView() { 193 // If mTab is null, destroy() was called. This should not be added after destroyed. 194 assert mTab != null; 195 super.addToParentViewAtIndex(mParentView, 196 mTab.getBrowser().getViewController().getDesiredInfoBarContainerViewIndex()); 197 } 198 199 /** 200 * Adds an {@link InfoBar} to the layout. 201 * @param infoBar The {@link InfoBar} to be added. 202 */ addInfoBar(InfoBar infoBar)203 void addInfoBar(InfoBar infoBar) { 204 infoBar.createView(); 205 mLayout.addInfoBar(infoBar); 206 } 207 208 /** 209 * Removes an {@link InfoBar} from the layout. 210 * @param infoBar The {@link InfoBar} to be removed. 211 */ removeInfoBar(InfoBar infoBar)212 void removeInfoBar(InfoBar infoBar) { 213 mLayout.removeInfoBar(infoBar); 214 } 215 216 /** 217 * Hides or stops hiding this View. 218 * @param isHidden Whether this View is should be hidden. 219 */ setHidden(boolean isHidden)220 void setHidden(boolean isHidden) { 221 setVisibility(isHidden ? View.GONE : View.VISIBLE); 222 } 223 224 /** 225 * Run an animation when the scrolling direction of a gesture has changed (this does not mean 226 * the gesture has ended). 227 * @param visible Whether or not the view should be visible. 228 */ runDirectionChangeAnimation(boolean visible)229 private void runDirectionChangeAnimation(boolean visible) { 230 mScrollDirectionChangeAnimation = createVerticalSnapAnimation(visible); 231 mScrollDirectionChangeAnimation.addListener(new AnimatorListenerAdapter() { 232 @Override 233 public void onAnimationEnd(Animator animation) { 234 mScrollDirectionChangeAnimation = null; 235 } 236 }); 237 mScrollDirectionChangeAnimation.start(); 238 } 239 240 @Override 241 // Ensure that this view's custom layout params are passed when adding it to its parent. createLayoutParams()242 public ViewGroup.MarginLayoutParams createLayoutParams() { 243 return (ViewGroup.MarginLayoutParams) getLayoutParams(); 244 } 245 updateLayoutParams(Context context, boolean isTablet)246 private void updateLayoutParams(Context context, boolean isTablet) { 247 RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams( 248 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 249 lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 250 int topMarginDp = isTablet ? TOP_MARGIN_TABLET_DP : TOP_MARGIN_PHONE_DP; 251 lp.topMargin = DisplayUtil.dpToPx(DisplayAndroid.getNonMultiDisplay(context), topMarginDp); 252 setLayoutParams(lp); 253 } 254 255 /** 256 * Returns true if any animations are pending or in progress. 257 */ 258 @VisibleForTesting isAnimating()259 public boolean isAnimating() { 260 return mLayout.isAnimating(); 261 } 262 } 263