Merged KeyButtonDrawable with tint drawable and shadow drawable (1/2)

KeyButtonDrawable can now draw its own shadow and tint based on its dark
intensity, no more shadow drawables wrapped by dual layer drawable and
cross fading to change their intensities. Refactored and simplified code
to get the nav bar drawables. AnimatedVectorDrawables can be used however
only dark intensity tinting will work on it. Fixed the clipped shadow in
landscape.

Change-Id: I6e234857f7972974aae34d92c7047086782124f0
Fixes: 112105450
Test: look at nav bar and tap rotate suggestion button
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
index 8e31f31..945d9b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
@@ -16,23 +16,34 @@
 
 package com.android.systemui.statusbar.policy;
 
-import android.annotation.Nullable;
+import android.animation.ArgbEvaluator;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.BlurMaskFilter.Blur;
+import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
 import android.util.FloatProperty;
-import android.view.Gravity;
-
+import com.android.settingslib.Utils;
 import com.android.systemui.R;
-import com.android.systemui.statusbar.phone.ShadowKeyDrawable;
 
 /**
- * Drawable for {@link KeyButtonView}s which contains an asset for both normal mode and light
- * navigation bar mode.
+ * Drawable for {@link KeyButtonView}s that supports tinting between two colors, rotation and shows
+ * a shadow. AnimatedVectorDrawable will only support tinting from intensities but has no support
+ * for shadows nor rotations.
  */
-public class KeyButtonDrawable extends LayerDrawable {
+public class KeyButtonDrawable extends Drawable {
 
     public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_ROTATE =
         new FloatProperty<KeyButtonDrawable>("KeyButtonRotation") {
@@ -60,83 +71,343 @@
             }
         };
 
-    private final boolean mHasDarkDrawable;
+    private final Paint mIconPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+    private final Paint mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+    private final ShadowDrawableState mState;
+    private AnimatedVectorDrawable mAnimatedDrawable;
 
-    public static KeyButtonDrawable create(Context lightContext, Drawable lightDrawable,
-            @Nullable Drawable darkDrawable, boolean hasShadow) {
-        if (darkDrawable != null) {
-            ShadowKeyDrawable light = new ShadowKeyDrawable(lightDrawable.mutate());
-            ShadowKeyDrawable dark = new ShadowKeyDrawable(darkDrawable.mutate());
-            if (hasShadow) {
-                // Only apply the shadow on the light drawable
-                Resources res = lightContext.getResources();
-                int offsetX = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_x);
-                int offsetY = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_y);
-                int radius = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_radius);
-                int color = lightContext.getColor(R.color.nav_key_button_shadow_color);
-                light.setShadowProperties(offsetX, offsetY, radius, color);
-            }
-            return new KeyButtonDrawable(new Drawable[] { light, dark });
-        } else {
-            return new KeyButtonDrawable(new Drawable[] {
-                    new ShadowKeyDrawable(lightDrawable.mutate()) });
-        }
+    public KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor) {
+        this(d, new ShadowDrawableState(lightColor, darkColor,
+                d instanceof AnimatedVectorDrawable));
     }
 
-    protected KeyButtonDrawable(Drawable[] drawables) {
-        super(drawables);
-        for (int i = 0; i < drawables.length; i++) {
-            setLayerGravity(i, Gravity.CENTER);
+    private KeyButtonDrawable(Drawable d, ShadowDrawableState state) {
+        mState = state;
+        if (d != null) {
+            mState.mBaseHeight = d.getIntrinsicHeight();
+            mState.mBaseWidth = d.getIntrinsicWidth();
+            mState.mChangingConfigurations = d.getChangingConfigurations();
+            mState.mChildState = d.getConstantState();
         }
-        mutate();
-        mHasDarkDrawable = drawables.length > 1;
-        setDarkIntensity(0f);
+        if (canAnimate()) {
+            mAnimatedDrawable = (AnimatedVectorDrawable) mState.mChildState.newDrawable().mutate();
+            setDrawableBounds(mAnimatedDrawable);
+        }
     }
 
     public void setDarkIntensity(float intensity) {
-        if (!mHasDarkDrawable) {
-            return;
-        }
-        getDrawable(0).setAlpha((int) ((1 - intensity) * 255f));
-        getDrawable(1).setAlpha((int) (intensity * 255f));
-        invalidateSelf();
+        mState.mDarkIntensity = intensity;
+        final int color = (int) ArgbEvaluator.getInstance()
+                .evaluate(intensity, mState.mLightColor, mState.mDarkColor);
+        updateShadowAlpha();
+        setColorFilter(new PorterDuffColorFilter(color, Mode.SRC_ATOP));
     }
 
     public void setRotation(float degrees) {
-        if (getDrawable(0) instanceof ShadowKeyDrawable) {
-            ((ShadowKeyDrawable) getDrawable(0)).setRotation(degrees);
+        if (canAnimate()) {
+            // AnimatedVectorDrawables will not support rotation
+            return;
         }
-        if (mHasDarkDrawable && getDrawable(1) instanceof ShadowKeyDrawable) {
-            ((ShadowKeyDrawable) getDrawable(1)).setRotation(degrees);
+        if (mState.mRotateDegrees != degrees) {
+            mState.mRotateDegrees = degrees;
+            invalidateSelf();
         }
     }
 
+    public void setTranslationX(float x) {
+        setTranslation(x, mState.mTranslationY);
+    }
+
     public void setTranslationY(float y) {
-        if (getDrawable(0) instanceof ShadowKeyDrawable) {
-            ((ShadowKeyDrawable) getDrawable(0)).setTranslationY(y);
+        setTranslation(mState.mTranslationX, y);
+    }
+
+    public void setTranslation(float x, float y) {
+        if (mState.mTranslationX != x || mState.mTranslationY != y) {
+            mState.mTranslationX = x;
+            mState.mTranslationY = y;
+            invalidateSelf();
         }
-        if (mHasDarkDrawable && getDrawable(1) instanceof ShadowKeyDrawable) {
-            ((ShadowKeyDrawable) getDrawable(1)).setTranslationY(y);
+    }
+
+    public void setShadowProperties(int x, int y, int size, int color) {
+        if (canAnimate()) {
+            // AnimatedVectorDrawables will not support shadows
+            return;
         }
+        if (mState.mShadowOffsetX != x || mState.mShadowOffsetY != y
+                || mState.mShadowSize != size || mState.mShadowColor != color) {
+            mState.mShadowOffsetX = x;
+            mState.mShadowOffsetY = y;
+            mState.mShadowSize = size;
+            mState.mShadowColor = color;
+            mShadowPaint.setColorFilter(
+                    new PorterDuffColorFilter(mState.mShadowColor, Mode.SRC_ATOP));
+            updateShadowAlpha();
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mState.mAlpha = alpha;
+        mIconPaint.setAlpha(alpha);
+        updateShadowAlpha();
+        invalidateSelf();
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        mIconPaint.setColorFilter(colorFilter);
+        if (mAnimatedDrawable != null) {
+            mAnimatedDrawable.setColorFilter(colorFilter);
+        }
+        invalidateSelf();
+    }
+
+    public float getDarkIntensity() {
+        return mState.mDarkIntensity;
     }
 
     public float getRotation() {
-        if (getDrawable(0) instanceof ShadowKeyDrawable) {
-            return ((ShadowKeyDrawable) getDrawable(0)).getRotation();
-        }
-        if (mHasDarkDrawable && getDrawable(1) instanceof ShadowKeyDrawable) {
-            return ((ShadowKeyDrawable) getDrawable(1)).getRotation();
-        }
-        return 0;
+        return mState.mRotateDegrees;
+    }
+
+    public float getTranslationX() {
+        return mState.mTranslationX;
     }
 
     public float getTranslationY() {
-        if (getDrawable(0) instanceof ShadowKeyDrawable) {
-            return ((ShadowKeyDrawable) getDrawable(0)).getTranslationY();
+        return mState.mTranslationY;
+    }
+
+    @Override
+    public ConstantState getConstantState() {
+        return mState;
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return mState.mBaseHeight + (mState.mShadowSize + Math.abs(mState.mShadowOffsetY)) * 2;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return mState.mBaseWidth + (mState.mShadowSize + Math.abs(mState.mShadowOffsetX)) * 2;
+    }
+
+    public boolean canAnimate() {
+        return mState.mSupportsAnimation;
+    }
+
+    public void startAnimation() {
+        if (mAnimatedDrawable != null) {
+            mAnimatedDrawable.start();
         }
-        if (mHasDarkDrawable && getDrawable(1) instanceof ShadowKeyDrawable) {
-            return ((ShadowKeyDrawable) getDrawable(1)).getTranslationY();
+    }
+
+    public void resetAnimation() {
+        if (mAnimatedDrawable != null) {
+            mAnimatedDrawable.reset();
         }
-        return 0;
+    }
+
+    public void clearAnimationCallbacks() {
+        if (mAnimatedDrawable != null) {
+            mAnimatedDrawable.clearAnimationCallbacks();
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        Rect bounds = getBounds();
+        if (bounds.isEmpty()) {
+            return;
+        }
+
+        if (mAnimatedDrawable != null) {
+            mAnimatedDrawable.draw(canvas);
+        } else {
+            // If no cache or previous cached bitmap is hardware/software acceleration does not
+            // match the current canvas on draw then regenerate
+            boolean hwBitmapChanged = mState.mIsHardwareBitmap != canvas.isHardwareAccelerated();
+            if (hwBitmapChanged) {
+                mState.mIsHardwareBitmap = canvas.isHardwareAccelerated();
+            }
+            if (mState.mLastDrawnIcon == null || hwBitmapChanged) {
+                regenerateBitmapIconCache();
+            }
+            canvas.save();
+            canvas.translate(mState.mTranslationX, mState.mTranslationY);
+            canvas.rotate(mState.mRotateDegrees, getIntrinsicWidth() / 2, getIntrinsicHeight() / 2);
+
+            if (mState.mShadowSize > 0) {
+                if (mState.mLastDrawnShadow == null || hwBitmapChanged) {
+                    regenerateBitmapShadowCache();
+                }
+
+                // Translate (with rotation offset) before drawing the shadow
+                final float radians = (float) (mState.mRotateDegrees * Math.PI / 180);
+                final float shadowOffsetX = (float) (Math.sin(radians) * mState.mShadowOffsetY
+                        + Math.cos(radians) * mState.mShadowOffsetX) - mState.mTranslationX;
+                final float shadowOffsetY = (float) (Math.cos(radians) * mState.mShadowOffsetY
+                        - Math.sin(radians) * mState.mShadowOffsetX) - mState.mTranslationY;
+                canvas.drawBitmap(mState.mLastDrawnShadow, shadowOffsetX, shadowOffsetY,
+                        mShadowPaint);
+            }
+            canvas.drawBitmap(mState.mLastDrawnIcon, null, bounds, mIconPaint);
+            canvas.restore();
+        }
+    }
+
+    @Override
+    public boolean canApplyTheme() {
+        return mState.canApplyTheme();
+    }
+
+    private void regenerateBitmapIconCache() {
+        final int width = getIntrinsicWidth();
+        final int height = getIntrinsicHeight();
+        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        final Canvas canvas = new Canvas(bitmap);
+
+        // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
+        final Drawable d = mState.mChildState.newDrawable().mutate();
+        setDrawableBounds(d);
+        d.draw(canvas);
+
+        if (mState.mIsHardwareBitmap) {
+            bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
+        }
+        mState.mLastDrawnIcon = bitmap;
+    }
+
+    private void regenerateBitmapShadowCache() {
+        if (mState.mShadowSize == 0) {
+            // No shadow
+            mState.mLastDrawnIcon = null;
+            return;
+        }
+
+        final int width = getIntrinsicWidth();
+        final int height = getIntrinsicHeight();
+        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+
+        // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
+        final Drawable d = mState.mChildState.newDrawable().mutate();
+        setDrawableBounds(d);
+        d.draw(canvas);
+
+        // Draws the shadow from original drawable
+        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+        paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, Blur.NORMAL));
+        int[] offset = new int[2];
+        final Bitmap shadow = bitmap.extractAlpha(paint, offset);
+        paint.setMaskFilter(null);
+        bitmap.eraseColor(Color.TRANSPARENT);
+        canvas.drawBitmap(shadow, offset[0], offset[1], paint);
+
+        if (mState.mIsHardwareBitmap) {
+            bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
+        }
+        mState.mLastDrawnShadow = bitmap;
+    }
+
+    /**
+     * Set the alpha of the shadow. As dark intensity increases, drop the alpha of the shadow since
+     * dark color and shadow should not be visible at the same time.
+     */
+    private void updateShadowAlpha() {
+        // Update the color from the original color's alpha as the max
+        int alpha = Color.alpha(mState.mShadowColor);
+        mShadowPaint.setAlpha(
+                Math.round(alpha * (mState.mAlpha / 255f) * (1 - mState.mDarkIntensity)));
+    }
+
+    /**
+     * Prevent shadow clipping by offsetting the drawable bounds by the shadow and its offset
+     * @param d the drawable to set the bounds
+     */
+    private void setDrawableBounds(Drawable d) {
+        final int offsetX = mState.mShadowSize + Math.abs(mState.mShadowOffsetX);
+        final int offsetY = mState.mShadowSize + Math.abs(mState.mShadowOffsetY);
+        d.setBounds(offsetX, offsetY, getIntrinsicWidth() - offsetX,
+                getIntrinsicHeight() - offsetY);
+    }
+
+    private static class ShadowDrawableState extends ConstantState {
+        int mChangingConfigurations;
+        int mBaseWidth;
+        int mBaseHeight;
+        float mRotateDegrees;
+        float mTranslationX;
+        float mTranslationY;
+        int mShadowOffsetX;
+        int mShadowOffsetY;
+        int mShadowSize;
+        int mShadowColor;
+        float mDarkIntensity;
+        int mAlpha;
+
+        boolean mIsHardwareBitmap;
+        Bitmap mLastDrawnIcon;
+        Bitmap mLastDrawnShadow;
+        ConstantState mChildState;
+
+        final int mLightColor;
+        final int mDarkColor;
+        final boolean mSupportsAnimation;
+
+        public ShadowDrawableState(@ColorInt int lightColor, @ColorInt int darkColor,
+                boolean animated) {
+            mLightColor = lightColor;
+            mDarkColor = darkColor;
+            mSupportsAnimation = animated;
+            mAlpha = 255;
+        }
+
+        @Override
+        public Drawable newDrawable() {
+            return new KeyButtonDrawable(null, this);
+        }
+
+        @Override
+        public int getChangingConfigurations() {
+            return mChangingConfigurations;
+        }
+
+        @Override
+        public boolean canApplyTheme() {
+            return true;
+        }
+    }
+
+    public static KeyButtonDrawable create(Context lightContext, Context darkContext,
+        @DrawableRes int iconResId, boolean hasShadow) {
+        return create(lightContext,
+            Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor),
+            Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor),
+            iconResId, hasShadow);
+    }
+
+    public static KeyButtonDrawable create(Context context, @ColorInt int lightColor,
+        @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow) {
+        final KeyButtonDrawable drawable = new KeyButtonDrawable(context.getDrawable(iconResId),
+            lightColor, darkColor);
+        if (hasShadow) {
+            final Resources res = context.getResources();
+            int offsetX = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_x);
+            int offsetY = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_y);
+            int radius = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_radius);
+            int color = context.getColor(R.color.nav_key_button_shadow_color);
+            drawable.setShadowProperties(offsetX, offsetY, radius, color);
+        }
+        return drawable;
     }
 }