Home | History | Annotate | Download | only in launcher3
      1 /*
      2  * Copyright (C) 2008 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.launcher3;
     18 
     19 import android.animation.ObjectAnimator;
     20 import android.animation.TimeInterpolator;
     21 import android.graphics.Bitmap;
     22 import android.graphics.Canvas;
     23 import android.graphics.Color;
     24 import android.graphics.ColorFilter;
     25 import android.graphics.ColorMatrix;
     26 import android.graphics.ColorMatrixColorFilter;
     27 import android.graphics.Paint;
     28 import android.graphics.PixelFormat;
     29 import android.graphics.PorterDuff;
     30 import android.graphics.PorterDuffColorFilter;
     31 import android.graphics.drawable.Drawable;
     32 import android.util.Property;
     33 import android.util.SparseArray;
     34 
     35 import com.android.launcher3.graphics.IconPalette;
     36 
     37 public class FastBitmapDrawable extends Drawable {
     38 
     39     private static final int[] STATE_PRESSED = new int[] {android.R.attr.state_pressed};
     40 
     41     private static final float PRESSED_BRIGHTNESS = 100f / 255f;
     42     private static final float DISABLED_DESATURATION = 1f;
     43     private static final float DISABLED_BRIGHTNESS = 0.5f;
     44 
     45     public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
     46 
     47         @Override
     48         public float getInterpolation(float input) {
     49             if (input < 0.05f) {
     50                 return input / 0.05f;
     51             } else if (input < 0.3f){
     52                 return 1;
     53             } else {
     54                 return (1 - input) / 0.7f;
     55             }
     56         }
     57     };
     58     public static final int CLICK_FEEDBACK_DURATION = 2000;
     59 
     60     // Since we don't need 256^2 values for combinations of both the brightness and saturation, we
     61     // reduce the value space to a smaller value V, which reduces the number of cached
     62     // ColorMatrixColorFilters that we need to keep to V^2
     63     private static final int REDUCED_FILTER_VALUE_SPACE = 48;
     64 
     65     // A cache of ColorFilters for optimizing brightness and saturation animations
     66     private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>();
     67 
     68     // Temporary matrices used for calculation
     69     private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix();
     70     private static final ColorMatrix sTempFilterMatrix = new ColorMatrix();
     71 
     72     protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
     73     private final Bitmap mBitmap;
     74 
     75     private boolean mIsPressed;
     76     private boolean mIsDisabled;
     77 
     78     private IconPalette mIconPalette;
     79 
     80     private static final Property<FastBitmapDrawable, Float> BRIGHTNESS
     81             = new Property<FastBitmapDrawable, Float>(Float.TYPE, "brightness") {
     82         @Override
     83         public Float get(FastBitmapDrawable fastBitmapDrawable) {
     84             return fastBitmapDrawable.getBrightness();
     85         }
     86 
     87         @Override
     88         public void set(FastBitmapDrawable fastBitmapDrawable, Float value) {
     89             fastBitmapDrawable.setBrightness(value);
     90         }
     91     };
     92 
     93     // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and
     94     // as a result, can be used to compose the key for the cached ColorMatrixColorFilters
     95     private int mDesaturation = 0;
     96     private int mBrightness = 0;
     97     private int mAlpha = 255;
     98     private int mPrevUpdateKey = Integer.MAX_VALUE;
     99 
    100     // Animators for the fast bitmap drawable's brightness
    101     private ObjectAnimator mBrightnessAnimator;
    102 
    103     public FastBitmapDrawable(Bitmap b) {
    104         mBitmap = b;
    105         setFilterBitmap(true);
    106     }
    107 
    108     @Override
    109     public void draw(Canvas canvas) {
    110         drawInternal(canvas);
    111     }
    112 
    113     public void drawWithBrightness(Canvas canvas, float brightness) {
    114         float oldBrightness = getBrightness();
    115         setBrightness(brightness);
    116         drawInternal(canvas);
    117         setBrightness(oldBrightness);
    118     }
    119 
    120     protected void drawInternal(Canvas canvas) {
    121         canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
    122     }
    123 
    124     public IconPalette getIconPalette() {
    125         if (mIconPalette == null) {
    126             mIconPalette = IconPalette.fromDominantColor(Utilities
    127                     .findDominantColorByHue(mBitmap, 20), true /* desaturateBackground */);
    128         }
    129         return mIconPalette;
    130     }
    131 
    132     @Override
    133     public void setColorFilter(ColorFilter cf) {
    134         // No op
    135     }
    136 
    137     @Override
    138     public int getOpacity() {
    139         return PixelFormat.TRANSLUCENT;
    140     }
    141 
    142     @Override
    143     public void setAlpha(int alpha) {
    144         mAlpha = alpha;
    145         mPaint.setAlpha(alpha);
    146     }
    147 
    148     @Override
    149     public void setFilterBitmap(boolean filterBitmap) {
    150         mPaint.setFilterBitmap(filterBitmap);
    151         mPaint.setAntiAlias(filterBitmap);
    152     }
    153 
    154     public int getAlpha() {
    155         return mAlpha;
    156     }
    157 
    158     @Override
    159     public int getIntrinsicWidth() {
    160         return mBitmap.getWidth();
    161     }
    162 
    163     @Override
    164     public int getIntrinsicHeight() {
    165         return mBitmap.getHeight();
    166     }
    167 
    168     @Override
    169     public int getMinimumWidth() {
    170         return getBounds().width();
    171     }
    172 
    173     @Override
    174     public int getMinimumHeight() {
    175         return getBounds().height();
    176     }
    177 
    178     public Bitmap getBitmap() {
    179         return mBitmap;
    180     }
    181 
    182     @Override
    183     public boolean isStateful() {
    184         return true;
    185     }
    186 
    187     @Override
    188     protected boolean onStateChange(int[] state) {
    189         boolean isPressed = false;
    190         for (int s : state) {
    191             if (s == android.R.attr.state_pressed) {
    192                 isPressed = true;
    193                 break;
    194             }
    195         }
    196         if (mIsPressed != isPressed) {
    197             mIsPressed = isPressed;
    198 
    199             if (mBrightnessAnimator != null) {
    200                 mBrightnessAnimator.cancel();
    201             }
    202 
    203             if (mIsPressed) {
    204                 // Animate when going to pressed state
    205                 mBrightnessAnimator = ObjectAnimator.ofFloat(
    206                         this, BRIGHTNESS, getExpectedBrightness());
    207                 mBrightnessAnimator.setDuration(CLICK_FEEDBACK_DURATION);
    208                 mBrightnessAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR);
    209                 mBrightnessAnimator.start();
    210             } else {
    211                 setBrightness(getExpectedBrightness());
    212             }
    213             return true;
    214         }
    215         return false;
    216     }
    217 
    218     private void invalidateDesaturationAndBrightness() {
    219         setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0);
    220         setBrightness(getExpectedBrightness());
    221     }
    222 
    223     private float getExpectedBrightness() {
    224         return mIsDisabled ? DISABLED_BRIGHTNESS :
    225                 (mIsPressed ? PRESSED_BRIGHTNESS : 0);
    226     }
    227 
    228     public void setIsDisabled(boolean isDisabled) {
    229         if (mIsDisabled != isDisabled) {
    230             mIsDisabled = isDisabled;
    231             invalidateDesaturationAndBrightness();
    232         }
    233     }
    234 
    235     /**
    236      * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated]
    237      */
    238     private void setDesaturation(float desaturation) {
    239         int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE);
    240         if (mDesaturation != newDesaturation) {
    241             mDesaturation = newDesaturation;
    242             updateFilter();
    243         }
    244     }
    245 
    246     public float getDesaturation() {
    247         return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE;
    248     }
    249 
    250     /**
    251      * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious]
    252      */
    253     private void setBrightness(float brightness) {
    254         int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE);
    255         if (mBrightness != newBrightness) {
    256             mBrightness = newBrightness;
    257             updateFilter();
    258         }
    259     }
    260 
    261     private float getBrightness() {
    262         return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE;
    263     }
    264 
    265     /**
    266      * Updates the paint to reflect the current brightness and saturation.
    267      */
    268     private void updateFilter() {
    269         boolean usePorterDuffFilter = false;
    270         int key = -1;
    271         if (mDesaturation > 0) {
    272             key = (mDesaturation << 16) | mBrightness;
    273         } else if (mBrightness > 0) {
    274             // Compose a key with a fully saturated icon if we are just animating brightness
    275             key = (1 << 16) | mBrightness;
    276 
    277             // We found that in L, ColorFilters cause drawing artifacts with shadows baked into
    278             // icons, so just use a PorterDuff filter when we aren't animating saturation
    279             usePorterDuffFilter = true;
    280         }
    281 
    282         // Debounce multiple updates on the same frame
    283         if (key == mPrevUpdateKey) {
    284             return;
    285         }
    286         mPrevUpdateKey = key;
    287 
    288         if (key != -1) {
    289             ColorFilter filter = sCachedFilter.get(key);
    290             if (filter == null) {
    291                 float brightnessF = getBrightness();
    292                 int brightnessI = (int) (255 * brightnessF);
    293                 if (usePorterDuffFilter) {
    294                     filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255),
    295                             PorterDuff.Mode.SRC_ATOP);
    296                 } else {
    297                     float saturationF = 1f - getDesaturation();
    298                     sTempFilterMatrix.setSaturation(saturationF);
    299                     if (mBrightness > 0) {
    300                         // Brightness: C-new = C-old*(1-amount) + amount
    301                         float scale = 1f - brightnessF;
    302                         float[] mat = sTempBrightnessMatrix.getArray();
    303                         mat[0] = scale;
    304                         mat[6] = scale;
    305                         mat[12] = scale;
    306                         mat[4] = brightnessI;
    307                         mat[9] = brightnessI;
    308                         mat[14] = brightnessI;
    309                         sTempFilterMatrix.preConcat(sTempBrightnessMatrix);
    310                     }
    311                     filter = new ColorMatrixColorFilter(sTempFilterMatrix);
    312                 }
    313                 sCachedFilter.append(key, filter);
    314             }
    315             mPaint.setColorFilter(filter);
    316         } else {
    317             mPaint.setColorFilter(null);
    318         }
    319         invalidateSelf();
    320     }
    321 }
    322