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