blob: 03c89c60360f09205a969aa64db9b7e7e7cd3e5a [file] [log] [blame]
Jorim Jaggi40db0292016-06-27 17:58:03 -07001/*
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
17package com.android.systemui.statusbar.policy;
18
Matthew Ngd6865ba2018-08-27 17:58:41 -070019import android.animation.ArgbEvaluator;
20import android.annotation.ColorInt;
21import android.annotation.DrawableRes;
Matthew Ng25593cc2018-09-12 16:05:41 -070022import android.annotation.NonNull;
Matthew Ngeb5ce832018-05-15 17:50:37 -070023import android.content.Context;
24import android.content.res.Resources;
Matthew Ngd6865ba2018-08-27 17:58:41 -070025import android.graphics.Bitmap;
26import android.graphics.BlurMaskFilter;
27import android.graphics.BlurMaskFilter.Blur;
28import android.graphics.Canvas;
Matthew Ngeb5ce832018-05-15 17:50:37 -070029import android.graphics.Color;
Matthew Ngd6865ba2018-08-27 17:58:41 -070030import android.graphics.ColorFilter;
31import android.graphics.Paint;
32import android.graphics.PixelFormat;
33import android.graphics.PorterDuff.Mode;
34import android.graphics.PorterDuffColorFilter;
35import android.graphics.Rect;
36import android.graphics.drawable.AnimatedVectorDrawable;
Jorim Jaggi40db0292016-06-27 17:58:03 -070037import android.graphics.drawable.Drawable;
Matthew Ngca4592b2018-08-06 14:12:37 -070038import android.util.FloatProperty;
Matthew Ng25593cc2018-09-12 16:05:41 -070039import android.view.ContextThemeWrapper;
Gus Prevasab336792018-11-14 13:52:20 -050040
Matthew Ngd6865ba2018-08-27 17:58:41 -070041import com.android.settingslib.Utils;
Matthew Ngeb5ce832018-05-15 17:50:37 -070042import com.android.systemui.R;
Matthew Ngeb5ce832018-05-15 17:50:37 -070043
Jorim Jaggi40db0292016-06-27 17:58:03 -070044/**
Matthew Ngd6865ba2018-08-27 17:58:41 -070045 * Drawable for {@link KeyButtonView}s that supports tinting between two colors, rotation and shows
46 * a shadow. AnimatedVectorDrawable will only support tinting from intensities but has no support
47 * for shadows nor rotations.
Jorim Jaggi40db0292016-06-27 17:58:03 -070048 */
Matthew Ngd6865ba2018-08-27 17:58:41 -070049public class KeyButtonDrawable extends Drawable {
Jorim Jaggi40db0292016-06-27 17:58:03 -070050
Matthew Ngca4592b2018-08-06 14:12:37 -070051 public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_ROTATE =
52 new FloatProperty<KeyButtonDrawable>("KeyButtonRotation") {
53 @Override
54 public void setValue(KeyButtonDrawable drawable, float degree) {
55 drawable.setRotation(degree);
56 }
57
58 @Override
59 public Float get(KeyButtonDrawable drawable) {
60 return drawable.getRotation();
61 }
62 };
63
64 public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_TRANSLATE_Y =
65 new FloatProperty<KeyButtonDrawable>("KeyButtonTranslateY") {
66 @Override
67 public void setValue(KeyButtonDrawable drawable, float y) {
68 drawable.setTranslationY(y);
69 }
70
71 @Override
72 public Float get(KeyButtonDrawable drawable) {
73 return drawable.getTranslationY();
74 }
75 };
76
Matthew Ngd6865ba2018-08-27 17:58:41 -070077 private final Paint mIconPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
78 private final Paint mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
79 private final ShadowDrawableState mState;
80 private AnimatedVectorDrawable mAnimatedDrawable;
Jorim Jaggi40db0292016-06-27 17:58:03 -070081
Matthew Ngd6865ba2018-08-27 17:58:41 -070082 public KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor) {
83 this(d, new ShadowDrawableState(lightColor, darkColor,
84 d instanceof AnimatedVectorDrawable));
Jorim Jaggi40db0292016-06-27 17:58:03 -070085 }
86
Matthew Ngd6865ba2018-08-27 17:58:41 -070087 private KeyButtonDrawable(Drawable d, ShadowDrawableState state) {
88 mState = state;
89 if (d != null) {
90 mState.mBaseHeight = d.getIntrinsicHeight();
91 mState.mBaseWidth = d.getIntrinsicWidth();
92 mState.mChangingConfigurations = d.getChangingConfigurations();
93 mState.mChildState = d.getConstantState();
Jason Monk5b3b4852017-03-27 17:40:56 -040094 }
Matthew Ngd6865ba2018-08-27 17:58:41 -070095 if (canAnimate()) {
96 mAnimatedDrawable = (AnimatedVectorDrawable) mState.mChildState.newDrawable().mutate();
97 setDrawableBounds(mAnimatedDrawable);
98 }
Jorim Jaggi40db0292016-06-27 17:58:03 -070099 }
100
101 public void setDarkIntensity(float intensity) {
Matthew Ngd6865ba2018-08-27 17:58:41 -0700102 mState.mDarkIntensity = intensity;
103 final int color = (int) ArgbEvaluator.getInstance()
104 .evaluate(intensity, mState.mLightColor, mState.mDarkColor);
105 updateShadowAlpha();
106 setColorFilter(new PorterDuffColorFilter(color, Mode.SRC_ATOP));
Jorim Jaggi40db0292016-06-27 17:58:03 -0700107 }
Matthew Ngeb5ce832018-05-15 17:50:37 -0700108
109 public void setRotation(float degrees) {
Matthew Ngd6865ba2018-08-27 17:58:41 -0700110 if (canAnimate()) {
111 // AnimatedVectorDrawables will not support rotation
112 return;
Matthew Ngeb5ce832018-05-15 17:50:37 -0700113 }
Matthew Ngd6865ba2018-08-27 17:58:41 -0700114 if (mState.mRotateDegrees != degrees) {
115 mState.mRotateDegrees = degrees;
116 invalidateSelf();
Matthew Ngeb5ce832018-05-15 17:50:37 -0700117 }
118 }
Matthew Ngca4592b2018-08-06 14:12:37 -0700119
Matthew Ngd6865ba2018-08-27 17:58:41 -0700120 public void setTranslationX(float x) {
121 setTranslation(x, mState.mTranslationY);
122 }
123
Matthew Ngca4592b2018-08-06 14:12:37 -0700124 public void setTranslationY(float y) {
Matthew Ngd6865ba2018-08-27 17:58:41 -0700125 setTranslation(mState.mTranslationX, y);
126 }
127
128 public void setTranslation(float x, float y) {
129 if (mState.mTranslationX != x || mState.mTranslationY != y) {
130 mState.mTranslationX = x;
131 mState.mTranslationY = y;
132 invalidateSelf();
Matthew Ngca4592b2018-08-06 14:12:37 -0700133 }
Matthew Ngd6865ba2018-08-27 17:58:41 -0700134 }
135
136 public void setShadowProperties(int x, int y, int size, int color) {
137 if (canAnimate()) {
138 // AnimatedVectorDrawables will not support shadows
139 return;
Matthew Ngca4592b2018-08-06 14:12:37 -0700140 }
Matthew Ngd6865ba2018-08-27 17:58:41 -0700141 if (mState.mShadowOffsetX != x || mState.mShadowOffsetY != y
142 || mState.mShadowSize != size || mState.mShadowColor != color) {
143 mState.mShadowOffsetX = x;
144 mState.mShadowOffsetY = y;
145 mState.mShadowSize = size;
146 mState.mShadowColor = color;
147 mShadowPaint.setColorFilter(
148 new PorterDuffColorFilter(mState.mShadowColor, Mode.SRC_ATOP));
149 updateShadowAlpha();
150 invalidateSelf();
151 }
152 }
153
154 @Override
155 public void setAlpha(int alpha) {
156 mState.mAlpha = alpha;
157 mIconPaint.setAlpha(alpha);
158 updateShadowAlpha();
159 invalidateSelf();
160 }
161
162 @Override
163 public void setColorFilter(ColorFilter colorFilter) {
164 mIconPaint.setColorFilter(colorFilter);
165 if (mAnimatedDrawable != null) {
166 mAnimatedDrawable.setColorFilter(colorFilter);
167 }
168 invalidateSelf();
169 }
170
171 public float getDarkIntensity() {
172 return mState.mDarkIntensity;
Matthew Ngca4592b2018-08-06 14:12:37 -0700173 }
174
175 public float getRotation() {
Matthew Ngd6865ba2018-08-27 17:58:41 -0700176 return mState.mRotateDegrees;
177 }
178
179 public float getTranslationX() {
180 return mState.mTranslationX;
Matthew Ngca4592b2018-08-06 14:12:37 -0700181 }
182
183 public float getTranslationY() {
Matthew Ngd6865ba2018-08-27 17:58:41 -0700184 return mState.mTranslationY;
185 }
186
187 @Override
188 public ConstantState getConstantState() {
189 return mState;
190 }
191
192 @Override
193 public int getOpacity() {
194 return PixelFormat.TRANSLUCENT;
195 }
196
197 @Override
198 public int getIntrinsicHeight() {
199 return mState.mBaseHeight + (mState.mShadowSize + Math.abs(mState.mShadowOffsetY)) * 2;
200 }
201
202 @Override
203 public int getIntrinsicWidth() {
204 return mState.mBaseWidth + (mState.mShadowSize + Math.abs(mState.mShadowOffsetX)) * 2;
205 }
206
207 public boolean canAnimate() {
208 return mState.mSupportsAnimation;
209 }
210
211 public void startAnimation() {
212 if (mAnimatedDrawable != null) {
213 mAnimatedDrawable.start();
Matthew Ngca4592b2018-08-06 14:12:37 -0700214 }
Matthew Ngd6865ba2018-08-27 17:58:41 -0700215 }
216
217 public void resetAnimation() {
218 if (mAnimatedDrawable != null) {
219 mAnimatedDrawable.reset();
Matthew Ngca4592b2018-08-06 14:12:37 -0700220 }
Matthew Ngd6865ba2018-08-27 17:58:41 -0700221 }
222
223 public void clearAnimationCallbacks() {
224 if (mAnimatedDrawable != null) {
225 mAnimatedDrawable.clearAnimationCallbacks();
226 }
227 }
228
229 @Override
230 public void draw(Canvas canvas) {
231 Rect bounds = getBounds();
232 if (bounds.isEmpty()) {
233 return;
234 }
235
236 if (mAnimatedDrawable != null) {
237 mAnimatedDrawable.draw(canvas);
238 } else {
239 // If no cache or previous cached bitmap is hardware/software acceleration does not
240 // match the current canvas on draw then regenerate
241 boolean hwBitmapChanged = mState.mIsHardwareBitmap != canvas.isHardwareAccelerated();
242 if (hwBitmapChanged) {
243 mState.mIsHardwareBitmap = canvas.isHardwareAccelerated();
244 }
245 if (mState.mLastDrawnIcon == null || hwBitmapChanged) {
246 regenerateBitmapIconCache();
247 }
248 canvas.save();
249 canvas.translate(mState.mTranslationX, mState.mTranslationY);
250 canvas.rotate(mState.mRotateDegrees, getIntrinsicWidth() / 2, getIntrinsicHeight() / 2);
251
252 if (mState.mShadowSize > 0) {
253 if (mState.mLastDrawnShadow == null || hwBitmapChanged) {
254 regenerateBitmapShadowCache();
255 }
256
257 // Translate (with rotation offset) before drawing the shadow
258 final float radians = (float) (mState.mRotateDegrees * Math.PI / 180);
259 final float shadowOffsetX = (float) (Math.sin(radians) * mState.mShadowOffsetY
260 + Math.cos(radians) * mState.mShadowOffsetX) - mState.mTranslationX;
261 final float shadowOffsetY = (float) (Math.cos(radians) * mState.mShadowOffsetY
262 - Math.sin(radians) * mState.mShadowOffsetX) - mState.mTranslationY;
263 canvas.drawBitmap(mState.mLastDrawnShadow, shadowOffsetX, shadowOffsetY,
264 mShadowPaint);
265 }
266 canvas.drawBitmap(mState.mLastDrawnIcon, null, bounds, mIconPaint);
267 canvas.restore();
268 }
269 }
270
271 @Override
272 public boolean canApplyTheme() {
273 return mState.canApplyTheme();
274 }
275
276 private void regenerateBitmapIconCache() {
277 final int width = getIntrinsicWidth();
278 final int height = getIntrinsicHeight();
279 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
280 final Canvas canvas = new Canvas(bitmap);
281
282 // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
283 final Drawable d = mState.mChildState.newDrawable().mutate();
284 setDrawableBounds(d);
285 d.draw(canvas);
286
287 if (mState.mIsHardwareBitmap) {
288 bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
289 }
290 mState.mLastDrawnIcon = bitmap;
291 }
292
293 private void regenerateBitmapShadowCache() {
294 if (mState.mShadowSize == 0) {
295 // No shadow
296 mState.mLastDrawnIcon = null;
297 return;
298 }
299
300 final int width = getIntrinsicWidth();
301 final int height = getIntrinsicHeight();
302 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
303 Canvas canvas = new Canvas(bitmap);
304
305 // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
306 final Drawable d = mState.mChildState.newDrawable().mutate();
307 setDrawableBounds(d);
308 d.draw(canvas);
309
310 // Draws the shadow from original drawable
311 Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
312 paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, Blur.NORMAL));
313 int[] offset = new int[2];
314 final Bitmap shadow = bitmap.extractAlpha(paint, offset);
315 paint.setMaskFilter(null);
316 bitmap.eraseColor(Color.TRANSPARENT);
317 canvas.drawBitmap(shadow, offset[0], offset[1], paint);
318
319 if (mState.mIsHardwareBitmap) {
320 bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
321 }
322 mState.mLastDrawnShadow = bitmap;
323 }
324
325 /**
326 * Set the alpha of the shadow. As dark intensity increases, drop the alpha of the shadow since
327 * dark color and shadow should not be visible at the same time.
328 */
329 private void updateShadowAlpha() {
330 // Update the color from the original color's alpha as the max
331 int alpha = Color.alpha(mState.mShadowColor);
332 mShadowPaint.setAlpha(
333 Math.round(alpha * (mState.mAlpha / 255f) * (1 - mState.mDarkIntensity)));
334 }
335
336 /**
337 * Prevent shadow clipping by offsetting the drawable bounds by the shadow and its offset
338 * @param d the drawable to set the bounds
339 */
340 private void setDrawableBounds(Drawable d) {
341 final int offsetX = mState.mShadowSize + Math.abs(mState.mShadowOffsetX);
342 final int offsetY = mState.mShadowSize + Math.abs(mState.mShadowOffsetY);
343 d.setBounds(offsetX, offsetY, getIntrinsicWidth() - offsetX,
344 getIntrinsicHeight() - offsetY);
345 }
346
347 private static class ShadowDrawableState extends ConstantState {
348 int mChangingConfigurations;
349 int mBaseWidth;
350 int mBaseHeight;
351 float mRotateDegrees;
352 float mTranslationX;
353 float mTranslationY;
354 int mShadowOffsetX;
355 int mShadowOffsetY;
356 int mShadowSize;
357 int mShadowColor;
358 float mDarkIntensity;
359 int mAlpha;
360
361 boolean mIsHardwareBitmap;
362 Bitmap mLastDrawnIcon;
363 Bitmap mLastDrawnShadow;
364 ConstantState mChildState;
365
366 final int mLightColor;
367 final int mDarkColor;
368 final boolean mSupportsAnimation;
369
370 public ShadowDrawableState(@ColorInt int lightColor, @ColorInt int darkColor,
371 boolean animated) {
372 mLightColor = lightColor;
373 mDarkColor = darkColor;
374 mSupportsAnimation = animated;
375 mAlpha = 255;
376 }
377
378 @Override
379 public Drawable newDrawable() {
380 return new KeyButtonDrawable(null, this);
381 }
382
383 @Override
384 public int getChangingConfigurations() {
385 return mChangingConfigurations;
386 }
387
388 @Override
389 public boolean canApplyTheme() {
390 return true;
391 }
392 }
393
Matthew Ng25593cc2018-09-12 16:05:41 -0700394 /**
395 * Creates a KeyButtonDrawable with a shadow given its icon. The tint applied to the drawable
396 * is determined by the dark and light theme given by the context.
397 * @param ctx Context to get the drawable and determine the dark and light theme
398 * @param icon the icon resource id
399 * @param hasShadow if a shadow will appear with the drawable
400 * @return KeyButtonDrawable
401 */
402 public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon,
403 boolean hasShadow) {
404 final int dualToneDarkTheme = Utils.getThemeAttr(ctx, R.attr.darkIconTheme);
405 final int dualToneLightTheme = Utils.getThemeAttr(ctx, R.attr.lightIconTheme);
406 Context lightContext = new ContextThemeWrapper(ctx, dualToneLightTheme);
407 Context darkContext = new ContextThemeWrapper(ctx, dualToneDarkTheme);
408 return KeyButtonDrawable.create(lightContext, darkContext, icon, hasShadow);
409 }
410
Matthew Ngd6865ba2018-08-27 17:58:41 -0700411 public static KeyButtonDrawable create(Context lightContext, Context darkContext,
412 @DrawableRes int iconResId, boolean hasShadow) {
413 return create(lightContext,
414 Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor),
415 Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor),
416 iconResId, hasShadow);
417 }
418
419 public static KeyButtonDrawable create(Context context, @ColorInt int lightColor,
420 @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow) {
421 final KeyButtonDrawable drawable = new KeyButtonDrawable(context.getDrawable(iconResId),
422 lightColor, darkColor);
423 if (hasShadow) {
424 final Resources res = context.getResources();
425 int offsetX = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_x);
426 int offsetY = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_y);
427 int radius = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_radius);
428 int color = context.getColor(R.color.nav_key_button_shadow_color);
429 drawable.setShadowProperties(offsetX, offsetY, radius, color);
430 }
431 return drawable;
Matthew Ngca4592b2018-08-06 14:12:37 -0700432 }
Jorim Jaggi40db0292016-06-27 17:58:03 -0700433}