1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.leanback.widget; 18 19 import android.animation.ArgbEvaluator; 20 import android.animation.ValueAnimator; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.Color; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.graphics.drawable.GradientDrawable; 28 import android.util.AttributeSet; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.widget.FrameLayout; 32 import android.widget.ImageView; 33 34 import androidx.annotation.ColorInt; 35 import androidx.core.view.ViewCompat; 36 import androidx.leanback.R; 37 38 /** 39 * <p>A widget that draws a search affordance, represented by a round background and an icon.</p> 40 * 41 * The background color and icon can be customized. 42 */ 43 public class SearchOrbView extends FrameLayout implements View.OnClickListener { 44 private OnClickListener mListener; 45 private View mRootView; 46 private View mSearchOrbView; 47 private ImageView mIcon; 48 private Drawable mIconDrawable; 49 private Colors mColors; 50 private final float mFocusedZoom; 51 private final int mPulseDurationMs; 52 private final int mScaleDurationMs; 53 private final float mUnfocusedZ; 54 private final float mFocusedZ; 55 private ValueAnimator mColorAnimator; 56 private boolean mColorAnimationEnabled; 57 private boolean mAttachedToWindow; 58 59 /** 60 * A set of colors used to display the search orb. 61 */ 62 public static class Colors { 63 private static final float BRIGHTNESS_ALPHA = 0.15f; 64 65 /** 66 * Constructs a color set using the given color for the search orb. 67 * Other colors are provided by the framework. 68 * 69 * @param color The main search orb color. 70 */ Colors(@olorInt int color)71 public Colors(@ColorInt int color) { 72 this(color, color); 73 } 74 75 /** 76 * Constructs a color set using the given colors for the search orb. 77 * Other colors are provided by the framework. 78 * 79 * @param color The main search orb color. 80 * @param brightColor A brighter version of the search orb used for animation. 81 */ Colors(@olorInt int color, @ColorInt int brightColor)82 public Colors(@ColorInt int color, @ColorInt int brightColor) { 83 this(color, brightColor, Color.TRANSPARENT); 84 } 85 86 /** 87 * Constructs a color set using the given colors. 88 * 89 * @param color The main search orb color. 90 * @param brightColor A brighter version of the search orb used for animation. 91 * @param iconColor A color used to tint the search orb icon. 92 */ Colors(@olorInt int color, @ColorInt int brightColor, @ColorInt int iconColor)93 public Colors(@ColorInt int color, @ColorInt int brightColor, @ColorInt int iconColor) { 94 this.color = color; 95 this.brightColor = brightColor == color ? getBrightColor(color) : brightColor; 96 this.iconColor = iconColor; 97 } 98 99 /** 100 * The main color of the search orb. 101 */ 102 @ColorInt 103 public int color; 104 105 /** 106 * A brighter version of the search orb used for animation. 107 */ 108 @ColorInt 109 public int brightColor; 110 111 /** 112 * A color used to tint the search orb icon. 113 */ 114 @ColorInt 115 public int iconColor; 116 117 /** 118 * Computes a default brighter version of the given color. 119 */ getBrightColor(int color)120 public static int getBrightColor(int color) { 121 final float brightnessValue = 0xff * BRIGHTNESS_ALPHA; 122 int red = (int)(Color.red(color) * (1 - BRIGHTNESS_ALPHA) + brightnessValue); 123 int green = (int)(Color.green(color) * (1 - BRIGHTNESS_ALPHA) + brightnessValue); 124 int blue = (int)(Color.blue(color) * (1 - BRIGHTNESS_ALPHA) + brightnessValue); 125 int alpha = (int)(Color.alpha(color) * (1 - BRIGHTNESS_ALPHA) + brightnessValue); 126 return Color.argb(alpha, red, green, blue); 127 } 128 } 129 130 private final ArgbEvaluator mColorEvaluator = new ArgbEvaluator(); 131 132 private final ValueAnimator.AnimatorUpdateListener mUpdateListener = 133 new ValueAnimator.AnimatorUpdateListener() { 134 @Override 135 public void onAnimationUpdate(ValueAnimator animator) { 136 Integer color = (Integer) animator.getAnimatedValue(); 137 setOrbViewColor(color.intValue()); 138 } 139 }; 140 141 private ValueAnimator mShadowFocusAnimator; 142 143 private final ValueAnimator.AnimatorUpdateListener mFocusUpdateListener = 144 new ValueAnimator.AnimatorUpdateListener() { 145 @Override 146 public void onAnimationUpdate(ValueAnimator animation) { 147 setSearchOrbZ(animation.getAnimatedFraction()); 148 } 149 }; 150 setSearchOrbZ(float fraction)151 void setSearchOrbZ(float fraction) { 152 ViewCompat.setZ(mSearchOrbView, mUnfocusedZ + fraction * (mFocusedZ - mUnfocusedZ)); 153 } 154 SearchOrbView(Context context)155 public SearchOrbView(Context context) { 156 this(context, null); 157 } 158 SearchOrbView(Context context, AttributeSet attrs)159 public SearchOrbView(Context context, AttributeSet attrs) { 160 this(context, attrs, R.attr.searchOrbViewStyle); 161 } 162 SearchOrbView(Context context, AttributeSet attrs, int defStyleAttr)163 public SearchOrbView(Context context, AttributeSet attrs, int defStyleAttr) { 164 super(context, attrs, defStyleAttr); 165 166 final Resources res = context.getResources(); 167 168 LayoutInflater inflater = (LayoutInflater) context 169 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 170 mRootView = inflater.inflate(getLayoutResourceId(), this, true); 171 mSearchOrbView = mRootView.findViewById(R.id.search_orb); 172 mIcon = (ImageView) mRootView.findViewById(R.id.icon); 173 174 mFocusedZoom = context.getResources().getFraction( 175 R.fraction.lb_search_orb_focused_zoom, 1, 1); 176 mPulseDurationMs = context.getResources().getInteger( 177 R.integer.lb_search_orb_pulse_duration_ms); 178 mScaleDurationMs = context.getResources().getInteger( 179 R.integer.lb_search_orb_scale_duration_ms); 180 mFocusedZ = context.getResources().getDimensionPixelSize( 181 R.dimen.lb_search_orb_focused_z); 182 mUnfocusedZ = context.getResources().getDimensionPixelSize( 183 R.dimen.lb_search_orb_unfocused_z); 184 185 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbSearchOrbView, 186 defStyleAttr, 0); 187 188 Drawable img = a.getDrawable(R.styleable.lbSearchOrbView_searchOrbIcon); 189 if (img == null) { 190 img = res.getDrawable(R.drawable.lb_ic_in_app_search); 191 } 192 setOrbIcon(img); 193 194 int defColor = res.getColor(R.color.lb_default_search_color); 195 int color = a.getColor(R.styleable.lbSearchOrbView_searchOrbColor, defColor); 196 int brightColor = a.getColor( 197 R.styleable.lbSearchOrbView_searchOrbBrightColor, color); 198 int iconColor = a.getColor(R.styleable.lbSearchOrbView_searchOrbIconColor, Color.TRANSPARENT); 199 setOrbColors(new Colors(color, brightColor, iconColor)); 200 a.recycle(); 201 202 setFocusable(true); 203 setClipChildren(false); 204 setOnClickListener(this); 205 setSoundEffectsEnabled(false); 206 setSearchOrbZ(0); 207 208 // Icon has no background, but must be on top of the search orb view 209 ViewCompat.setZ(mIcon, mFocusedZ); 210 } 211 getLayoutResourceId()212 int getLayoutResourceId() { 213 return R.layout.lb_search_orb; 214 } 215 scaleOrbViewOnly(float scale)216 void scaleOrbViewOnly(float scale) { 217 mSearchOrbView.setScaleX(scale); 218 mSearchOrbView.setScaleY(scale); 219 } 220 getFocusedZoom()221 float getFocusedZoom() { 222 return mFocusedZoom; 223 } 224 225 @Override onClick(View view)226 public void onClick(View view) { 227 if (null != mListener) { 228 mListener.onClick(view); 229 } 230 } 231 startShadowFocusAnimation(boolean gainFocus, int duration)232 private void startShadowFocusAnimation(boolean gainFocus, int duration) { 233 if (mShadowFocusAnimator == null) { 234 mShadowFocusAnimator = ValueAnimator.ofFloat(0f, 1f); 235 mShadowFocusAnimator.addUpdateListener(mFocusUpdateListener); 236 } 237 if (gainFocus) { 238 mShadowFocusAnimator.start(); 239 } else { 240 mShadowFocusAnimator.reverse(); 241 } 242 mShadowFocusAnimator.setDuration(duration); 243 } 244 245 @Override onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)246 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 247 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 248 animateOnFocus(gainFocus); 249 } 250 animateOnFocus(boolean hasFocus)251 void animateOnFocus(boolean hasFocus) { 252 final float zoom = hasFocus ? mFocusedZoom : 1f; 253 mRootView.animate().scaleX(zoom).scaleY(zoom).setDuration(mScaleDurationMs).start(); 254 startShadowFocusAnimation(hasFocus, mScaleDurationMs); 255 enableOrbColorAnimation(hasFocus); 256 } 257 258 /** 259 * Sets the orb icon. 260 * @param icon the drawable to be used as the icon 261 */ setOrbIcon(Drawable icon)262 public void setOrbIcon(Drawable icon) { 263 mIconDrawable = icon; 264 mIcon.setImageDrawable(mIconDrawable); 265 } 266 267 /** 268 * Returns the orb icon 269 * @return the drawable used as the icon 270 */ getOrbIcon()271 public Drawable getOrbIcon() { 272 return mIconDrawable; 273 } 274 275 /** 276 * Sets the on click listener for the orb. 277 * @param listener The listener. 278 */ setOnOrbClickedListener(OnClickListener listener)279 public void setOnOrbClickedListener(OnClickListener listener) { 280 mListener = listener; 281 } 282 283 /** 284 * Sets the background color of the search orb. 285 * Other colors will be provided by the framework. 286 * 287 * @param color the RGBA color 288 */ setOrbColor(int color)289 public void setOrbColor(int color) { 290 setOrbColors(new Colors(color, color, Color.TRANSPARENT)); 291 } 292 293 /** 294 * Sets the search orb colors. 295 * Other colors are provided by the framework. 296 * @deprecated Use {@link #setOrbColors(Colors)} instead. 297 */ 298 @Deprecated setOrbColor(@olorInt int color, @ColorInt int brightColor)299 public void setOrbColor(@ColorInt int color, @ColorInt int brightColor) { 300 setOrbColors(new Colors(color, brightColor, Color.TRANSPARENT)); 301 } 302 303 /** 304 * Returns the orb color 305 * @return the RGBA color 306 */ 307 @ColorInt getOrbColor()308 public int getOrbColor() { 309 return mColors.color; 310 } 311 312 /** 313 * Sets the {@link Colors} used to display the search orb. 314 */ setOrbColors(Colors colors)315 public void setOrbColors(Colors colors) { 316 mColors = colors; 317 mIcon.setColorFilter(mColors.iconColor); 318 319 if (mColorAnimator == null) { 320 setOrbViewColor(mColors.color); 321 } else { 322 enableOrbColorAnimation(true); 323 } 324 } 325 326 /** 327 * Returns the {@link Colors} used to display the search orb. 328 */ getOrbColors()329 public Colors getOrbColors() { 330 return mColors; 331 } 332 333 /** 334 * Enables or disables the orb color animation. 335 * 336 * <p> 337 * Orb color animation is handled automatically when the orb is focused/unfocused, 338 * however, an app may choose to override the current animation state, for example 339 * when an activity is paused. 340 * </p> 341 */ enableOrbColorAnimation(boolean enable)342 public void enableOrbColorAnimation(boolean enable) { 343 mColorAnimationEnabled = enable; 344 updateColorAnimator(); 345 } 346 updateColorAnimator()347 private void updateColorAnimator() { 348 if (mColorAnimator != null) { 349 mColorAnimator.end(); 350 mColorAnimator = null; 351 } 352 if (mColorAnimationEnabled && mAttachedToWindow) { 353 // TODO: set interpolator (material if available) 354 mColorAnimator = ValueAnimator.ofObject(mColorEvaluator, 355 mColors.color, mColors.brightColor, mColors.color); 356 mColorAnimator.setRepeatCount(ValueAnimator.INFINITE); 357 mColorAnimator.setDuration(mPulseDurationMs * 2); 358 mColorAnimator.addUpdateListener(mUpdateListener); 359 mColorAnimator.start(); 360 } 361 } 362 setOrbViewColor(int color)363 void setOrbViewColor(int color) { 364 if (mSearchOrbView.getBackground() instanceof GradientDrawable) { 365 ((GradientDrawable) mSearchOrbView.getBackground()).setColor(color); 366 } 367 } 368 369 @Override onAttachedToWindow()370 protected void onAttachedToWindow() { 371 super.onAttachedToWindow(); 372 mAttachedToWindow = true; 373 updateColorAnimator(); 374 } 375 376 @Override onDetachedFromWindow()377 protected void onDetachedFromWindow() { 378 mAttachedToWindow = false; 379 // Must stop infinite animation to prevent activity leak 380 updateColorAnimator(); 381 super.onDetachedFromWindow(); 382 } 383 } 384