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