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