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 FastBitmapDrawable(Bitmap b)103 public FastBitmapDrawable(Bitmap b) { 104 mBitmap = b; 105 setFilterBitmap(true); 106 } 107 108 @Override draw(Canvas canvas)109 public void draw(Canvas canvas) { 110 drawInternal(canvas); 111 } 112 drawWithBrightness(Canvas canvas, float brightness)113 public void drawWithBrightness(Canvas canvas, float brightness) { 114 float oldBrightness = getBrightness(); 115 setBrightness(brightness); 116 drawInternal(canvas); 117 setBrightness(oldBrightness); 118 } 119 drawInternal(Canvas canvas)120 protected void drawInternal(Canvas canvas) { 121 canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); 122 } 123 getIconPalette()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 setColorFilter(ColorFilter cf)133 public void setColorFilter(ColorFilter cf) { 134 // No op 135 } 136 137 @Override getOpacity()138 public int getOpacity() { 139 return PixelFormat.TRANSLUCENT; 140 } 141 142 @Override setAlpha(int alpha)143 public void setAlpha(int alpha) { 144 mAlpha = alpha; 145 mPaint.setAlpha(alpha); 146 } 147 148 @Override setFilterBitmap(boolean filterBitmap)149 public void setFilterBitmap(boolean filterBitmap) { 150 mPaint.setFilterBitmap(filterBitmap); 151 mPaint.setAntiAlias(filterBitmap); 152 } 153 getAlpha()154 public int getAlpha() { 155 return mAlpha; 156 } 157 158 @Override getIntrinsicWidth()159 public int getIntrinsicWidth() { 160 return mBitmap.getWidth(); 161 } 162 163 @Override getIntrinsicHeight()164 public int getIntrinsicHeight() { 165 return mBitmap.getHeight(); 166 } 167 168 @Override getMinimumWidth()169 public int getMinimumWidth() { 170 return getBounds().width(); 171 } 172 173 @Override getMinimumHeight()174 public int getMinimumHeight() { 175 return getBounds().height(); 176 } 177 getBitmap()178 public Bitmap getBitmap() { 179 return mBitmap; 180 } 181 182 @Override isStateful()183 public boolean isStateful() { 184 return true; 185 } 186 187 @Override onStateChange(int[] state)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 invalidateDesaturationAndBrightness()218 private void invalidateDesaturationAndBrightness() { 219 setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0); 220 setBrightness(getExpectedBrightness()); 221 } 222 getExpectedBrightness()223 private float getExpectedBrightness() { 224 return mIsDisabled ? DISABLED_BRIGHTNESS : 225 (mIsPressed ? PRESSED_BRIGHTNESS : 0); 226 } 227 setIsDisabled(boolean isDisabled)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 */ setDesaturation(float desaturation)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 getDesaturation()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 */ setBrightness(float brightness)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 getBrightness()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 */ updateFilter()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