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