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