SearchOrbView.java revision 77b750bc2094fbe921058d8748fe26f830fbc6c8
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 if (null != listener) { 281 setVisibility(View.VISIBLE); 282 } else { 283 setVisibility(View.INVISIBLE); 284 } 285 } 286 287 /** 288 * Sets the background color of the search orb. 289 * Other colors will be provided by the framework. 290 * 291 * @param color the RGBA color 292 */ 293 public void setOrbColor(int color) { 294 setOrbColors(new Colors(color, color, Color.TRANSPARENT)); 295 } 296 297 /** 298 * Sets the search orb colors. 299 * Other colors are provided by the framework. 300 * @deprecated Use {@link #setOrbColors(Colors)} instead. 301 */ 302 @Deprecated 303 public void setOrbColor(@ColorInt int color, @ColorInt int brightColor) { 304 setOrbColors(new Colors(color, brightColor, Color.TRANSPARENT)); 305 } 306 307 /** 308 * Returns the orb color 309 * @return the RGBA color 310 */ 311 @ColorInt 312 public int getOrbColor() { 313 return mColors.color; 314 } 315 316 /** 317 * Sets the {@link Colors} used to display the search orb. 318 */ 319 public void setOrbColors(Colors colors) { 320 mColors = colors; 321 mIcon.setColorFilter(mColors.iconColor); 322 323 if (mColorAnimator == null) { 324 setOrbViewColor(mColors.color); 325 } else { 326 enableOrbColorAnimation(true); 327 } 328 } 329 330 /** 331 * Returns the {@link Colors} used to display the search orb. 332 */ 333 public Colors getOrbColors() { 334 return mColors; 335 } 336 337 /** 338 * Enables or disables the orb color animation. 339 * 340 * <p> 341 * Orb color animation is handled automatically when the orb is focused/unfocused, 342 * however, an app may choose to override the current animation state, for example 343 * when an activity is paused. 344 * </p> 345 */ 346 public void enableOrbColorAnimation(boolean enable) { 347 mColorAnimationEnabled = enable; 348 updateColorAnimator(); 349 } 350 351 private void updateColorAnimator() { 352 if (mColorAnimator != null) { 353 mColorAnimator.end(); 354 mColorAnimator = null; 355 } 356 if (mColorAnimationEnabled && mAttachedToWindow) { 357 // TODO: set interpolator (material if available) 358 mColorAnimator = ValueAnimator.ofObject(mColorEvaluator, 359 mColors.color, mColors.brightColor, mColors.color); 360 mColorAnimator.setRepeatCount(ValueAnimator.INFINITE); 361 mColorAnimator.setDuration(mPulseDurationMs * 2); 362 mColorAnimator.addUpdateListener(mUpdateListener); 363 mColorAnimator.start(); 364 } 365 } 366 367 private void setOrbViewColor(int color) { 368 if (mSearchOrbView.getBackground() instanceof GradientDrawable) { 369 ((GradientDrawable) mSearchOrbView.getBackground()).setColor(color); 370 } 371 } 372 373 @Override 374 protected void onAttachedToWindow() { 375 super.onAttachedToWindow(); 376 mAttachedToWindow = true; 377 updateColorAnimator(); 378 } 379 380 @Override 381 protected void onDetachedFromWindow() { 382 mAttachedToWindow = false; 383 // Must stop infinite animation to prevent activity leak 384 updateColorAnimator(); 385 super.onDetachedFromWindow(); 386 } 387} 388