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