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.AnimatorSet; 20 import android.animation.ObjectAnimator; 21 import android.animation.TimeInterpolator; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.ColorFilter; 26 import android.graphics.ColorMatrix; 27 import android.graphics.ColorMatrixColorFilter; 28 import android.graphics.Paint; 29 import android.graphics.PixelFormat; 30 import android.graphics.PorterDuff; 31 import android.graphics.PorterDuffColorFilter; 32 import android.graphics.drawable.Drawable; 33 import android.util.SparseArray; 34 import android.view.animation.DecelerateInterpolator; 35 36 public class FastBitmapDrawable extends Drawable { 37 38 /** 39 * The possible states that a FastBitmapDrawable can be in. 40 */ 41 public enum State { 42 43 NORMAL (0f, 0f, 1f, new DecelerateInterpolator()), 44 PRESSED (0f, 100f / 255f, 1f, CLICK_FEEDBACK_INTERPOLATOR), 45 FAST_SCROLL_HIGHLIGHTED (0f, 0f, 1.15f, new DecelerateInterpolator()), 46 FAST_SCROLL_UNHIGHLIGHTED (0f, 0f, 1f, new DecelerateInterpolator()), 47 DISABLED (1f, 0.5f, 1f, new DecelerateInterpolator()); 48 49 public final float desaturation; 50 public final float brightness; 51 /** 52 * Used specifically by the view drawing this FastBitmapDrawable. 53 */ 54 public final float viewScale; 55 public final TimeInterpolator interpolator; 56 State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator)57 State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator) { 58 this.desaturation = desaturation; 59 this.brightness = brightness; 60 this.viewScale = viewScale; 61 this.interpolator = interpolator; 62 } 63 } 64 65 public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { 66 67 @Override 68 public float getInterpolation(float input) { 69 if (input < 0.05f) { 70 return input / 0.05f; 71 } else if (input < 0.3f){ 72 return 1; 73 } else { 74 return (1 - input) / 0.7f; 75 } 76 } 77 }; 78 public static final int CLICK_FEEDBACK_DURATION = 2000; 79 public static final int FAST_SCROLL_HIGHLIGHT_DURATION = 225; 80 public static final int FAST_SCROLL_UNHIGHLIGHT_DURATION = 150; 81 public static final int FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION = 225; 82 public static final int FAST_SCROLL_INACTIVE_DURATION = 275; 83 84 // Since we don't need 256^2 values for combinations of both the brightness and saturation, we 85 // reduce the value space to a smaller value V, which reduces the number of cached 86 // ColorMatrixColorFilters that we need to keep to V^2 87 private static final int REDUCED_FILTER_VALUE_SPACE = 48; 88 89 // A cache of ColorFilters for optimizing brightness and saturation animations 90 private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>(); 91 92 // Temporary matrices used for calculation 93 private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix(); 94 private static final ColorMatrix sTempFilterMatrix = new ColorMatrix(); 95 96 private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 97 private final Bitmap mBitmap; 98 private State mState = State.NORMAL; 99 100 // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and 101 // as a result, can be used to compose the key for the cached ColorMatrixColorFilters 102 private int mDesaturation = 0; 103 private int mBrightness = 0; 104 private int mAlpha = 255; 105 private int mPrevUpdateKey = Integer.MAX_VALUE; 106 107 // Animators for the fast bitmap drawable's properties 108 private AnimatorSet mPropertyAnimator; 109 FastBitmapDrawable(Bitmap b)110 public FastBitmapDrawable(Bitmap b) { 111 mBitmap = b; 112 setBounds(0, 0, b.getWidth(), b.getHeight()); 113 } 114 115 @Override draw(Canvas canvas)116 public void draw(Canvas canvas) { 117 canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); 118 } 119 120 @Override setColorFilter(ColorFilter cf)121 public void setColorFilter(ColorFilter cf) { 122 // No op 123 } 124 125 @Override getOpacity()126 public int getOpacity() { 127 return PixelFormat.TRANSLUCENT; 128 } 129 130 @Override setAlpha(int alpha)131 public void setAlpha(int alpha) { 132 mAlpha = alpha; 133 mPaint.setAlpha(alpha); 134 } 135 136 @Override setFilterBitmap(boolean filterBitmap)137 public void setFilterBitmap(boolean filterBitmap) { 138 mPaint.setFilterBitmap(filterBitmap); 139 mPaint.setAntiAlias(filterBitmap); 140 } 141 getAlpha()142 public int getAlpha() { 143 return mAlpha; 144 } 145 146 @Override getIntrinsicWidth()147 public int getIntrinsicWidth() { 148 return mBitmap.getWidth(); 149 } 150 151 @Override getIntrinsicHeight()152 public int getIntrinsicHeight() { 153 return mBitmap.getHeight(); 154 } 155 156 @Override getMinimumWidth()157 public int getMinimumWidth() { 158 return getBounds().width(); 159 } 160 161 @Override getMinimumHeight()162 public int getMinimumHeight() { 163 return getBounds().height(); 164 } 165 getBitmap()166 public Bitmap getBitmap() { 167 return mBitmap; 168 } 169 170 /** 171 * Animates this drawable to a new state. 172 * 173 * @return whether the state has changed. 174 */ animateState(State newState)175 public boolean animateState(State newState) { 176 State prevState = mState; 177 if (mState != newState) { 178 mState = newState; 179 180 mPropertyAnimator = cancelAnimator(mPropertyAnimator); 181 mPropertyAnimator = new AnimatorSet(); 182 mPropertyAnimator.playTogether( 183 ObjectAnimator 184 .ofFloat(this, "desaturation", newState.desaturation), 185 ObjectAnimator 186 .ofFloat(this, "brightness", newState.brightness)); 187 mPropertyAnimator.setInterpolator(newState.interpolator); 188 mPropertyAnimator.setDuration(getDurationForStateChange(prevState, newState)); 189 mPropertyAnimator.setStartDelay(getStartDelayForStateChange(prevState, newState)); 190 mPropertyAnimator.start(); 191 return true; 192 } 193 return false; 194 } 195 196 /** 197 * Immediately sets this drawable to a new state. 198 * 199 * @return whether the state has changed. 200 */ setState(State newState)201 public boolean setState(State newState) { 202 if (mState != newState) { 203 mState = newState; 204 205 mPropertyAnimator = cancelAnimator(mPropertyAnimator); 206 207 setDesaturation(newState.desaturation); 208 setBrightness(newState.brightness); 209 return true; 210 } 211 return false; 212 } 213 214 /** 215 * Returns the current state. 216 */ getCurrentState()217 public State getCurrentState() { 218 return mState; 219 } 220 221 /** 222 * Returns the duration for the state change animation. 223 */ getDurationForStateChange(State fromState, State toState)224 public static int getDurationForStateChange(State fromState, State toState) { 225 switch (toState) { 226 case NORMAL: 227 switch (fromState) { 228 case PRESSED: 229 return 0; 230 case FAST_SCROLL_HIGHLIGHTED: 231 case FAST_SCROLL_UNHIGHLIGHTED: 232 return FAST_SCROLL_INACTIVE_DURATION; 233 } 234 case PRESSED: 235 return CLICK_FEEDBACK_DURATION; 236 case FAST_SCROLL_HIGHLIGHTED: 237 return FAST_SCROLL_HIGHLIGHT_DURATION; 238 case FAST_SCROLL_UNHIGHLIGHTED: 239 switch (fromState) { 240 case NORMAL: 241 // When animating from normal state, take a little longer 242 return FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION; 243 default: 244 return FAST_SCROLL_UNHIGHLIGHT_DURATION; 245 } 246 } 247 return 0; 248 } 249 250 /** 251 * Returns the start delay when animating between certain fast scroll states. 252 */ getStartDelayForStateChange(State fromState, State toState)253 public static int getStartDelayForStateChange(State fromState, State toState) { 254 switch (toState) { 255 case FAST_SCROLL_UNHIGHLIGHTED: 256 switch (fromState) { 257 case NORMAL: 258 return FAST_SCROLL_UNHIGHLIGHT_DURATION / 4; 259 } 260 } 261 return 0; 262 } 263 264 /** 265 * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] 266 */ setDesaturation(float desaturation)267 public void setDesaturation(float desaturation) { 268 int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE); 269 if (mDesaturation != newDesaturation) { 270 mDesaturation = newDesaturation; 271 updateFilter(); 272 } 273 } 274 getDesaturation()275 public float getDesaturation() { 276 return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE; 277 } 278 279 /** 280 * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious] 281 */ setBrightness(float brightness)282 public void setBrightness(float brightness) { 283 int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE); 284 if (mBrightness != newBrightness) { 285 mBrightness = newBrightness; 286 updateFilter(); 287 } 288 } 289 getBrightness()290 public float getBrightness() { 291 return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE; 292 } 293 294 /** 295 * Updates the paint to reflect the current brightness and saturation. 296 */ updateFilter()297 private void updateFilter() { 298 boolean usePorterDuffFilter = false; 299 int key = -1; 300 if (mDesaturation > 0) { 301 key = (mDesaturation << 16) | mBrightness; 302 } else if (mBrightness > 0) { 303 // Compose a key with a fully saturated icon if we are just animating brightness 304 key = (1 << 16) | mBrightness; 305 306 // We found that in L, ColorFilters cause drawing artifacts with shadows baked into 307 // icons, so just use a PorterDuff filter when we aren't animating saturation 308 usePorterDuffFilter = true; 309 } 310 311 // Debounce multiple updates on the same frame 312 if (key == mPrevUpdateKey) { 313 return; 314 } 315 mPrevUpdateKey = key; 316 317 if (key != -1) { 318 ColorFilter filter = sCachedFilter.get(key); 319 if (filter == null) { 320 float brightnessF = getBrightness(); 321 int brightnessI = (int) (255 * brightnessF); 322 if (usePorterDuffFilter) { 323 filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255), 324 PorterDuff.Mode.SRC_ATOP); 325 } else { 326 float saturationF = 1f - getDesaturation(); 327 sTempFilterMatrix.setSaturation(saturationF); 328 if (mBrightness > 0) { 329 // Brightness: C-new = C-old*(1-amount) + amount 330 float scale = 1f - brightnessF; 331 float[] mat = sTempBrightnessMatrix.getArray(); 332 mat[0] = scale; 333 mat[6] = scale; 334 mat[12] = scale; 335 mat[4] = brightnessI; 336 mat[9] = brightnessI; 337 mat[14] = brightnessI; 338 sTempFilterMatrix.preConcat(sTempBrightnessMatrix); 339 } 340 filter = new ColorMatrixColorFilter(sTempFilterMatrix); 341 } 342 sCachedFilter.append(key, filter); 343 } 344 mPaint.setColorFilter(filter); 345 } else { 346 mPaint.setColorFilter(null); 347 } 348 invalidateSelf(); 349 } 350 cancelAnimator(AnimatorSet animator)351 private AnimatorSet cancelAnimator(AnimatorSet animator) { 352 if (animator != null) { 353 animator.removeAllListeners(); 354 animator.cancel(); 355 } 356 return null; 357 } 358 } 359