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