1 // Copyright 2019 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 package org.chromium.chrome.browser.gesturenav;
5 
6 import android.animation.ValueAnimator;
7 import android.content.Context;
8 import android.content.res.ColorStateList;
9 import android.graphics.PorterDuff.Mode;
10 import android.util.AttributeSet;
11 import android.view.View;
12 import android.view.ViewGroup;
13 import android.view.animation.Animation.AnimationListener;
14 import android.widget.ImageView;
15 import android.widget.LinearLayout;
16 import android.widget.TextView;
17 
18 import androidx.annotation.DrawableRes;
19 import androidx.annotation.IntDef;
20 
21 import org.chromium.base.ApiCompatibilityUtils;
22 import org.chromium.chrome.R;
23 import org.chromium.ui.util.ColorUtils;
24 
25 import java.lang.annotation.Retention;
26 import java.lang.annotation.RetentionPolicy;
27 
28 /**
29  * View class for a bubble used in gesture navigation UI that consists of an icon
30  * and an optional text.
31  */
32 public class NavigationBubble extends LinearLayout {
33     /**
34      * Target to close when gesture navigation takes place on the beginning
35      * of the navigation history. It can close either the current tab or
36      * chrome itself (putting it background).
37      */
38     @IntDef({CloseTarget.NONE, CloseTarget.TAB, CloseTarget.APP})
39     @Retention(RetentionPolicy.SOURCE)
40     @interface CloseTarget {
41         int NONE = 0;
42         int TAB = 1;
43         int APP = 2;
44     }
45 
46     private static final int COLOR_TRANSITION_DURATION_MS = 250;
47 
48     private static final float FADE_ALPHA = 0.5f;
49 
50     private static final int FADE_DURATION_MS = 400;
51 
52     private final ValueAnimator mColorAnimator;
53     private final int mBlue;
54     private final int mBlack;
55     private final String mCloseApp;
56     private final String mCloseTab;
57 
58     private class ColorUpdateListener implements ValueAnimator.AnimatorUpdateListener {
59         private int mStart;
60         private int mEnd;
61 
setTransitionColors(int start, int end)62         private void setTransitionColors(int start, int end) {
63             mStart = start;
64             mEnd = end;
65         }
66 
67         @Override
onAnimationUpdate(ValueAnimator animation)68         public void onAnimationUpdate(ValueAnimator animation) {
69             float fraction = (float) animation.getAnimatedValue();
70             ApiCompatibilityUtils.setImageTintList(mIcon,
71                     ColorStateList.valueOf(ColorUtils.getColorWithOverlay(mStart, mEnd, fraction)));
72         }
73     }
74 
75     private final ColorUpdateListener mColorUpdateListener;
76 
77     private TextView mText;
78     private ImageView mIcon;
79     private AnimationListener mListener;
80 
81     // True if arrow bubble is faded out.
82     private boolean mArrowFaded;
83 
84     private @CloseTarget int mCloseTarget;
85 
86     /**
87      * Constructor for inflating from XML.
88      */
NavigationBubble(Context context)89     public NavigationBubble(Context context) {
90         this(context, null);
91     }
92 
NavigationBubble(Context context, AttributeSet attrs)93     public NavigationBubble(Context context, AttributeSet attrs) {
94         super(context, attrs);
95 
96         mBlack = ApiCompatibilityUtils.getColor(getResources(), R.color.navigation_bubble_arrow);
97         mBlue = ApiCompatibilityUtils.getColor(getResources(), R.color.default_icon_color_blue);
98 
99         mColorUpdateListener = new ColorUpdateListener();
100         mColorAnimator = ValueAnimator.ofFloat(0, 1).setDuration(COLOR_TRANSITION_DURATION_MS);
101         mColorAnimator.addUpdateListener(mColorUpdateListener);
102         getBackground().setColorFilter(ApiCompatibilityUtils.getColor(getResources(),
103                                                R.color.navigation_bubble_background_color),
104                 Mode.MULTIPLY);
105         mCloseApp = getResources().getString(R.string.overscroll_navigation_close_chrome,
106                 getContext().getString(R.string.app_name));
107         mCloseTab = getResources().getString(R.string.overscroll_navigation_close_tab);
108         mCloseTarget = CloseTarget.NONE;
109     }
110 
111     @Override
onFinishInflate()112     protected void onFinishInflate() {
113         super.onFinishInflate();
114         mIcon = findViewById(R.id.navigation_bubble_arrow);
115         mText = findViewById(R.id.navigation_bubble_text);
116     }
117 
118     /**
119      * Sets {@link AnimationListener} used when the widget disappears at the end of
120      * user gesture.
121      * @param listener Listener object.
122      */
setAnimationListener(AnimationListener listener)123     public void setAnimationListener(AnimationListener listener) {
124         mListener = listener;
125     }
126 
127     /**
128      * @return {@code true} if the widget is showing the close chrome indicator text.
129      */
isShowingCaption()130     public boolean isShowingCaption() {
131         return getTextView().getVisibility() == View.VISIBLE;
132     }
133 
134     /**
135      * Shows or hides the close indicator.
136      * @param target Target to close. if {@code NONE}, hide the indicator.
137      */
showCaption(@loseTarget int target)138     public void showCaption(@CloseTarget int target) {
139         if (target != CloseTarget.NONE && !isShowingCaption()) {
140             setCloseIndicator(target);
141             getTextView().setVisibility(View.VISIBLE);
142             // Measure the width again after the indicator text becomes visible.
143             measure(0, 0);
144         } else if (target == CloseTarget.NONE && isShowingCaption()) {
145             getTextView().setVisibility(View.GONE);
146         }
147     }
148 
setCloseIndicator(@loseTarget int target)149     private void setCloseIndicator(@CloseTarget int target) {
150         assert target == CloseTarget.APP || target == CloseTarget.TAB;
151         if (mCloseTarget == target) return;
152         mCloseTarget = target;
153         getTextView().setText(target == CloseTarget.APP ? mCloseApp : mCloseTab);
154     }
155 
156     @Override
onAnimationStart()157     public void onAnimationStart() {
158         super.onAnimationStart();
159         if (mListener != null) {
160             mListener.onAnimationStart(getAnimation());
161         }
162     }
163 
164     @Override
onAnimationEnd()165     public void onAnimationEnd() {
166         super.onAnimationEnd();
167         if (mListener != null) {
168             mListener.onAnimationEnd(getAnimation());
169         }
170     }
171 
172     /**
173      * Sets the icon at the start of the icon view.
174      * @param icon The resource id pointing to the icon.
175      */
setIcon(@rawableRes int icon)176     public void setIcon(@DrawableRes int icon) {
177         mIcon.setVisibility(ViewGroup.VISIBLE);
178         mIcon.setImageResource(icon);
179         setImageTint(false);
180     }
181 
182     /**
183      * Sets the correct tinting on the arrow icon.
184      */
setImageTint(boolean navigate)185     public void setImageTint(boolean navigate) {
186         assert mIcon != null;
187         mColorUpdateListener.setTransitionColors(
188                 navigate ? mBlack : mBlue, navigate ? mBlue : mBlack);
189         mColorAnimator.start();
190     }
191 
192     /**
193      * Returns the {@link TextView} that contains the label of the widget.
194      * @return A {@link TextView}.
195      */
getTextView()196     public TextView getTextView() {
197         return mText;
198     }
199 
200     /**
201      * Fade out the arrow bubble.
202      * @param faded {@code true} if the bubble should be faded.
203      * @param animate {@code true} if animation is needed.
204      */
setFaded(boolean faded, boolean animate)205     public void setFaded(boolean faded, boolean animate) {
206         if (faded == mArrowFaded) return;
207         assert mIcon != null;
208         animate().alpha(faded ? FADE_ALPHA : 1.f).setDuration(animate ? FADE_DURATION_MS : 0);
209         mArrowFaded = faded;
210     }
211 }
212