1 /* 2 * Copyright (C) 2016 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 com.android.systemui.statusbar.policy; 18 19 import android.animation.ArgbEvaluator; 20 import android.annotation.ColorInt; 21 import android.annotation.DrawableRes; 22 import android.annotation.NonNull; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Bitmap; 26 import android.graphics.BlurMaskFilter; 27 import android.graphics.BlurMaskFilter.Blur; 28 import android.graphics.Canvas; 29 import android.graphics.Color; 30 import android.graphics.ColorFilter; 31 import android.graphics.Paint; 32 import android.graphics.PixelFormat; 33 import android.graphics.PorterDuff; 34 import android.graphics.PorterDuff.Mode; 35 import android.graphics.PorterDuffColorFilter; 36 import android.graphics.Rect; 37 import android.graphics.drawable.AnimatedVectorDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.util.FloatProperty; 40 import android.view.ContextThemeWrapper; 41 import android.view.View; 42 43 import com.android.settingslib.Utils; 44 import com.android.systemui.R; 45 46 /** 47 * Drawable for {@link KeyButtonView}s that supports tinting between two colors, rotation and shows 48 * a shadow. AnimatedVectorDrawable will only support tinting from intensities but has no support 49 * for shadows nor rotations. 50 */ 51 public class KeyButtonDrawable extends Drawable { 52 53 public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_ROTATE = 54 new FloatProperty<KeyButtonDrawable>("KeyButtonRotation") { 55 @Override 56 public void setValue(KeyButtonDrawable drawable, float degree) { 57 drawable.setRotation(degree); 58 } 59 60 @Override 61 public Float get(KeyButtonDrawable drawable) { 62 return drawable.getRotation(); 63 } 64 }; 65 66 public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_TRANSLATE_Y = 67 new FloatProperty<KeyButtonDrawable>("KeyButtonTranslateY") { 68 @Override 69 public void setValue(KeyButtonDrawable drawable, float y) { 70 drawable.setTranslationY(y); 71 } 72 73 @Override 74 public Float get(KeyButtonDrawable drawable) { 75 return drawable.getTranslationY(); 76 } 77 }; 78 79 private final Paint mIconPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 80 private final Paint mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 81 private final ShadowDrawableState mState; 82 private AnimatedVectorDrawable mAnimatedDrawable; 83 private final Callback mAnimatedDrawableCallback = new Callback() { 84 @Override 85 public void invalidateDrawable(@NonNull Drawable who) { 86 invalidateSelf(); 87 } 88 89 @Override 90 public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { 91 scheduleSelf(what, when); 92 } 93 94 @Override 95 public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { 96 unscheduleSelf(what); 97 } 98 }; 99 KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor, boolean horizontalFlip, Color ovalBackgroundColor)100 public KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor, 101 boolean horizontalFlip, Color ovalBackgroundColor) { 102 this(d, new ShadowDrawableState(lightColor, darkColor, 103 d instanceof AnimatedVectorDrawable, horizontalFlip, ovalBackgroundColor)); 104 } 105 KeyButtonDrawable(Drawable d, ShadowDrawableState state)106 private KeyButtonDrawable(Drawable d, ShadowDrawableState state) { 107 mState = state; 108 if (d != null) { 109 mState.mBaseHeight = d.getIntrinsicHeight(); 110 mState.mBaseWidth = d.getIntrinsicWidth(); 111 mState.mChangingConfigurations = d.getChangingConfigurations(); 112 mState.mChildState = d.getConstantState(); 113 } 114 if (canAnimate()) { 115 mAnimatedDrawable = (AnimatedVectorDrawable) mState.mChildState.newDrawable().mutate(); 116 mAnimatedDrawable.setCallback(mAnimatedDrawableCallback); 117 setDrawableBounds(mAnimatedDrawable); 118 } 119 } 120 setDarkIntensity(float intensity)121 public void setDarkIntensity(float intensity) { 122 mState.mDarkIntensity = intensity; 123 final int color = (int) ArgbEvaluator.getInstance() 124 .evaluate(intensity, mState.mLightColor, mState.mDarkColor); 125 updateShadowAlpha(); 126 setColorFilter(new PorterDuffColorFilter(color, Mode.SRC_ATOP)); 127 } 128 setRotation(float degrees)129 public void setRotation(float degrees) { 130 if (canAnimate()) { 131 // AnimatedVectorDrawables will not support rotation 132 return; 133 } 134 if (mState.mRotateDegrees != degrees) { 135 mState.mRotateDegrees = degrees; 136 invalidateSelf(); 137 } 138 } 139 setTranslationX(float x)140 public void setTranslationX(float x) { 141 setTranslation(x, mState.mTranslationY); 142 } 143 setTranslationY(float y)144 public void setTranslationY(float y) { 145 setTranslation(mState.mTranslationX, y); 146 } 147 setTranslation(float x, float y)148 public void setTranslation(float x, float y) { 149 if (mState.mTranslationX != x || mState.mTranslationY != y) { 150 mState.mTranslationX = x; 151 mState.mTranslationY = y; 152 invalidateSelf(); 153 } 154 } 155 setShadowProperties(int x, int y, int size, int color)156 public void setShadowProperties(int x, int y, int size, int color) { 157 if (canAnimate()) { 158 // AnimatedVectorDrawables will not support shadows 159 return; 160 } 161 if (mState.mShadowOffsetX != x || mState.mShadowOffsetY != y 162 || mState.mShadowSize != size || mState.mShadowColor != color) { 163 mState.mShadowOffsetX = x; 164 mState.mShadowOffsetY = y; 165 mState.mShadowSize = size; 166 mState.mShadowColor = color; 167 mShadowPaint.setColorFilter( 168 new PorterDuffColorFilter(mState.mShadowColor, Mode.SRC_ATOP)); 169 updateShadowAlpha(); 170 invalidateSelf(); 171 } 172 } 173 174 @Override setAlpha(int alpha)175 public void setAlpha(int alpha) { 176 mState.mAlpha = alpha; 177 mIconPaint.setAlpha(alpha); 178 updateShadowAlpha(); 179 invalidateSelf(); 180 } 181 182 @Override setColorFilter(ColorFilter colorFilter)183 public void setColorFilter(ColorFilter colorFilter) { 184 mIconPaint.setColorFilter(colorFilter); 185 if (mAnimatedDrawable != null) { 186 if (hasOvalBg()) { 187 mAnimatedDrawable.setColorFilter( 188 new PorterDuffColorFilter(mState.mLightColor, PorterDuff.Mode.SRC_IN)); 189 } else { 190 mAnimatedDrawable.setColorFilter(colorFilter); 191 } 192 } 193 invalidateSelf(); 194 } 195 getDarkIntensity()196 public float getDarkIntensity() { 197 return mState.mDarkIntensity; 198 } 199 getRotation()200 public float getRotation() { 201 return mState.mRotateDegrees; 202 } 203 getTranslationX()204 public float getTranslationX() { 205 return mState.mTranslationX; 206 } 207 getTranslationY()208 public float getTranslationY() { 209 return mState.mTranslationY; 210 } 211 212 @Override getConstantState()213 public ConstantState getConstantState() { 214 return mState; 215 } 216 217 @Override getOpacity()218 public int getOpacity() { 219 return PixelFormat.TRANSLUCENT; 220 } 221 222 @Override getIntrinsicHeight()223 public int getIntrinsicHeight() { 224 return mState.mBaseHeight + (mState.mShadowSize + Math.abs(mState.mShadowOffsetY)) * 2; 225 } 226 227 @Override getIntrinsicWidth()228 public int getIntrinsicWidth() { 229 return mState.mBaseWidth + (mState.mShadowSize + Math.abs(mState.mShadowOffsetX)) * 2; 230 } 231 canAnimate()232 public boolean canAnimate() { 233 return mState.mSupportsAnimation; 234 } 235 startAnimation()236 public void startAnimation() { 237 if (mAnimatedDrawable != null) { 238 mAnimatedDrawable.start(); 239 } 240 } 241 resetAnimation()242 public void resetAnimation() { 243 if (mAnimatedDrawable != null) { 244 mAnimatedDrawable.reset(); 245 } 246 } 247 clearAnimationCallbacks()248 public void clearAnimationCallbacks() { 249 if (mAnimatedDrawable != null) { 250 mAnimatedDrawable.clearAnimationCallbacks(); 251 } 252 } 253 254 @Override draw(Canvas canvas)255 public void draw(Canvas canvas) { 256 Rect bounds = getBounds(); 257 if (bounds.isEmpty()) { 258 return; 259 } 260 261 if (mAnimatedDrawable != null) { 262 mAnimatedDrawable.draw(canvas); 263 } else { 264 // If no cache or previous cached bitmap is hardware/software acceleration does not 265 // match the current canvas on draw then regenerate 266 boolean hwBitmapChanged = mState.mIsHardwareBitmap != canvas.isHardwareAccelerated(); 267 if (hwBitmapChanged) { 268 mState.mIsHardwareBitmap = canvas.isHardwareAccelerated(); 269 } 270 if (mState.mLastDrawnIcon == null || hwBitmapChanged) { 271 regenerateBitmapIconCache(); 272 } 273 canvas.save(); 274 canvas.translate(mState.mTranslationX, mState.mTranslationY); 275 canvas.rotate(mState.mRotateDegrees, getIntrinsicWidth() / 2, getIntrinsicHeight() / 2); 276 277 if (mState.mShadowSize > 0) { 278 if (mState.mLastDrawnShadow == null || hwBitmapChanged) { 279 regenerateBitmapShadowCache(); 280 } 281 282 // Translate (with rotation offset) before drawing the shadow 283 final float radians = (float) (mState.mRotateDegrees * Math.PI / 180); 284 final float shadowOffsetX = (float) (Math.sin(radians) * mState.mShadowOffsetY 285 + Math.cos(radians) * mState.mShadowOffsetX) - mState.mTranslationX; 286 final float shadowOffsetY = (float) (Math.cos(radians) * mState.mShadowOffsetY 287 - Math.sin(radians) * mState.mShadowOffsetX) - mState.mTranslationY; 288 canvas.drawBitmap(mState.mLastDrawnShadow, shadowOffsetX, shadowOffsetY, 289 mShadowPaint); 290 } 291 canvas.drawBitmap(mState.mLastDrawnIcon, null, bounds, mIconPaint); 292 canvas.restore(); 293 } 294 } 295 296 @Override canApplyTheme()297 public boolean canApplyTheme() { 298 return mState.canApplyTheme(); 299 } 300 getDrawableBackgroundColor()301 @ColorInt int getDrawableBackgroundColor() { 302 return mState.mOvalBackgroundColor.toArgb(); 303 } 304 hasOvalBg()305 boolean hasOvalBg() { 306 return mState.mOvalBackgroundColor != null; 307 } 308 regenerateBitmapIconCache()309 private void regenerateBitmapIconCache() { 310 final int width = getIntrinsicWidth(); 311 final int height = getIntrinsicHeight(); 312 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 313 final Canvas canvas = new Canvas(bitmap); 314 315 // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared. 316 final Drawable d = mState.mChildState.newDrawable().mutate(); 317 setDrawableBounds(d); 318 canvas.save(); 319 if (mState.mHorizontalFlip) { 320 canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f); 321 } 322 d.draw(canvas); 323 canvas.restore(); 324 325 if (mState.mIsHardwareBitmap) { 326 bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false); 327 } 328 mState.mLastDrawnIcon = bitmap; 329 } 330 regenerateBitmapShadowCache()331 private void regenerateBitmapShadowCache() { 332 if (mState.mShadowSize == 0) { 333 // No shadow 334 mState.mLastDrawnIcon = null; 335 return; 336 } 337 338 final int width = getIntrinsicWidth(); 339 final int height = getIntrinsicHeight(); 340 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 341 Canvas canvas = new Canvas(bitmap); 342 343 // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared. 344 final Drawable d = mState.mChildState.newDrawable().mutate(); 345 setDrawableBounds(d); 346 canvas.save(); 347 if (mState.mHorizontalFlip) { 348 canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f); 349 } 350 d.draw(canvas); 351 canvas.restore(); 352 353 // Draws the shadow from original drawable 354 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 355 paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, Blur.NORMAL)); 356 int[] offset = new int[2]; 357 final Bitmap shadow = bitmap.extractAlpha(paint, offset); 358 paint.setMaskFilter(null); 359 bitmap.eraseColor(Color.TRANSPARENT); 360 canvas.drawBitmap(shadow, offset[0], offset[1], paint); 361 362 if (mState.mIsHardwareBitmap) { 363 bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false); 364 } 365 mState.mLastDrawnShadow = bitmap; 366 } 367 368 /** 369 * Set the alpha of the shadow. As dark intensity increases, drop the alpha of the shadow since 370 * dark color and shadow should not be visible at the same time. 371 */ updateShadowAlpha()372 private void updateShadowAlpha() { 373 // Update the color from the original color's alpha as the max 374 int alpha = Color.alpha(mState.mShadowColor); 375 mShadowPaint.setAlpha( 376 Math.round(alpha * (mState.mAlpha / 255f) * (1 - mState.mDarkIntensity))); 377 } 378 379 /** 380 * Prevent shadow clipping by offsetting the drawable bounds by the shadow and its offset 381 * @param d the drawable to set the bounds 382 */ setDrawableBounds(Drawable d)383 private void setDrawableBounds(Drawable d) { 384 final int offsetX = mState.mShadowSize + Math.abs(mState.mShadowOffsetX); 385 final int offsetY = mState.mShadowSize + Math.abs(mState.mShadowOffsetY); 386 d.setBounds(offsetX, offsetY, getIntrinsicWidth() - offsetX, 387 getIntrinsicHeight() - offsetY); 388 } 389 390 private static class ShadowDrawableState extends ConstantState { 391 int mChangingConfigurations; 392 int mBaseWidth; 393 int mBaseHeight; 394 float mRotateDegrees; 395 float mTranslationX; 396 float mTranslationY; 397 int mShadowOffsetX; 398 int mShadowOffsetY; 399 int mShadowSize; 400 int mShadowColor; 401 float mDarkIntensity; 402 int mAlpha; 403 boolean mHorizontalFlip; 404 405 boolean mIsHardwareBitmap; 406 Bitmap mLastDrawnIcon; 407 Bitmap mLastDrawnShadow; 408 ConstantState mChildState; 409 410 final int mLightColor; 411 final int mDarkColor; 412 final boolean mSupportsAnimation; 413 final Color mOvalBackgroundColor; 414 ShadowDrawableState(@olorInt int lightColor, @ColorInt int darkColor, boolean animated, boolean horizontalFlip, Color ovalBackgroundColor)415 public ShadowDrawableState(@ColorInt int lightColor, @ColorInt int darkColor, 416 boolean animated, boolean horizontalFlip, Color ovalBackgroundColor) { 417 mLightColor = lightColor; 418 mDarkColor = darkColor; 419 mSupportsAnimation = animated; 420 mAlpha = 255; 421 mHorizontalFlip = horizontalFlip; 422 mOvalBackgroundColor = ovalBackgroundColor; 423 } 424 425 @Override newDrawable()426 public Drawable newDrawable() { 427 return new KeyButtonDrawable(null, this); 428 } 429 430 @Override getChangingConfigurations()431 public int getChangingConfigurations() { 432 return mChangingConfigurations; 433 } 434 435 @Override canApplyTheme()436 public boolean canApplyTheme() { 437 return true; 438 } 439 } 440 441 /** 442 * Creates a KeyButtonDrawable with a shadow given its icon. The tint applied to the drawable 443 * is determined by the dark and light theme given by the context. 444 * @param ctx Context to get the drawable and determine the dark and light theme 445 * @param icon the icon resource id 446 * @param hasShadow if a shadow will appear with the drawable 447 * @param ovalBackgroundColor the color of the oval bg that will be drawn 448 * @return KeyButtonDrawable 449 */ create(@onNull Context ctx, @DrawableRes int icon, boolean hasShadow, Color ovalBackgroundColor)450 public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon, 451 boolean hasShadow, Color ovalBackgroundColor) { 452 final int dualToneDarkTheme = Utils.getThemeAttr(ctx, R.attr.darkIconTheme); 453 final int dualToneLightTheme = Utils.getThemeAttr(ctx, R.attr.lightIconTheme); 454 Context lightContext = new ContextThemeWrapper(ctx, dualToneLightTheme); 455 Context darkContext = new ContextThemeWrapper(ctx, dualToneDarkTheme); 456 return KeyButtonDrawable.create(lightContext, darkContext, icon, hasShadow, 457 ovalBackgroundColor); 458 } 459 460 /** 461 * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see 462 * {@link #create(Context, int, boolean, boolean)}. 463 */ create(@onNull Context ctx, @DrawableRes int icon, boolean hasShadow)464 public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon, 465 boolean hasShadow) { 466 return create(ctx, icon, hasShadow, null /* ovalBackgroundColor */); 467 } 468 469 /** 470 * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see 471 * {@link #create(Context, int, boolean, boolean)}. 472 */ create(Context lightContext, Context darkContext, @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor)473 public static KeyButtonDrawable create(Context lightContext, Context darkContext, 474 @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor) { 475 return create(lightContext, 476 Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor), 477 Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor), 478 iconResId, hasShadow, ovalBackgroundColor); 479 } 480 481 /** 482 * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see 483 * {@link #create(Context, int, boolean, boolean)}. 484 */ create(Context context, @ColorInt int lightColor, @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor)485 public static KeyButtonDrawable create(Context context, @ColorInt int lightColor, 486 @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow, 487 Color ovalBackgroundColor) { 488 final Resources res = context.getResources(); 489 boolean isRtl = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 490 Drawable d = context.getDrawable(iconResId); 491 final KeyButtonDrawable drawable = new KeyButtonDrawable(d, lightColor, darkColor, 492 isRtl && d.isAutoMirrored(), ovalBackgroundColor); 493 if (hasShadow) { 494 int offsetX = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_x); 495 int offsetY = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_y); 496 int radius = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_radius); 497 int color = context.getColor(R.color.nav_key_button_shadow_color); 498 drawable.setShadowProperties(offsetX, offsetY, radius, color); 499 } 500 return drawable; 501 } 502 } 503