/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.util.SparseArray; import android.view.animation.DecelerateInterpolator; public class FastBitmapDrawable extends Drawable { /** * The possible states that a FastBitmapDrawable can be in. */ public enum State { NORMAL (0f, 0f, 1f, new DecelerateInterpolator()), PRESSED (0f, 100f / 255f, 1f, CLICK_FEEDBACK_INTERPOLATOR), FAST_SCROLL_HIGHLIGHTED (0f, 0f, 1.15f, new DecelerateInterpolator()), FAST_SCROLL_UNHIGHLIGHTED (0f, 0f, 1f, new DecelerateInterpolator()), DISABLED (1f, 0.5f, 1f, new DecelerateInterpolator()); public final float desaturation; public final float brightness; /** * Used specifically by the view drawing this FastBitmapDrawable. */ public final float viewScale; public final TimeInterpolator interpolator; State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator) { this.desaturation = desaturation; this.brightness = brightness; this.viewScale = viewScale; this.interpolator = interpolator; } } public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() { @Override public float getInterpolation(float input) { if (input < 0.05f) { return input / 0.05f; } else if (input < 0.3f){ return 1; } else { return (1 - input) / 0.7f; } } }; public static final int CLICK_FEEDBACK_DURATION = 2000; public static final int FAST_SCROLL_HIGHLIGHT_DURATION = 225; public static final int FAST_SCROLL_UNHIGHLIGHT_DURATION = 150; public static final int FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION = 225; public static final int FAST_SCROLL_INACTIVE_DURATION = 275; // Since we don't need 256^2 values for combinations of both the brightness and saturation, we // reduce the value space to a smaller value V, which reduces the number of cached // ColorMatrixColorFilters that we need to keep to V^2 private static final int REDUCED_FILTER_VALUE_SPACE = 48; // A cache of ColorFilters for optimizing brightness and saturation animations private static final SparseArray sCachedFilter = new SparseArray<>(); // Temporary matrices used for calculation private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix(); private static final ColorMatrix sTempFilterMatrix = new ColorMatrix(); private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); private final Bitmap mBitmap; private State mState = State.NORMAL; // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and // as a result, can be used to compose the key for the cached ColorMatrixColorFilters private int mDesaturation = 0; private int mBrightness = 0; private int mAlpha = 255; private int mPrevUpdateKey = Integer.MAX_VALUE; // Animators for the fast bitmap drawable's properties private AnimatorSet mPropertyAnimator; public FastBitmapDrawable(Bitmap b) { mBitmap = b; setBounds(0, 0, b.getWidth(), b.getHeight()); } @Override public void draw(Canvas canvas) { canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); } @Override public void setColorFilter(ColorFilter cf) { // No op } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public void setAlpha(int alpha) { mAlpha = alpha; mPaint.setAlpha(alpha); } @Override public void setFilterBitmap(boolean filterBitmap) { mPaint.setFilterBitmap(filterBitmap); mPaint.setAntiAlias(filterBitmap); } public int getAlpha() { return mAlpha; } @Override public int getIntrinsicWidth() { return mBitmap.getWidth(); } @Override public int getIntrinsicHeight() { return mBitmap.getHeight(); } @Override public int getMinimumWidth() { return getBounds().width(); } @Override public int getMinimumHeight() { return getBounds().height(); } public Bitmap getBitmap() { return mBitmap; } /** * Animates this drawable to a new state. * * @return whether the state has changed. */ public boolean animateState(State newState) { State prevState = mState; if (mState != newState) { mState = newState; mPropertyAnimator = cancelAnimator(mPropertyAnimator); mPropertyAnimator = new AnimatorSet(); mPropertyAnimator.playTogether( ObjectAnimator .ofFloat(this, "desaturation", newState.desaturation), ObjectAnimator .ofFloat(this, "brightness", newState.brightness)); mPropertyAnimator.setInterpolator(newState.interpolator); mPropertyAnimator.setDuration(getDurationForStateChange(prevState, newState)); mPropertyAnimator.setStartDelay(getStartDelayForStateChange(prevState, newState)); mPropertyAnimator.start(); return true; } return false; } /** * Immediately sets this drawable to a new state. * * @return whether the state has changed. */ public boolean setState(State newState) { if (mState != newState) { mState = newState; mPropertyAnimator = cancelAnimator(mPropertyAnimator); setDesaturation(newState.desaturation); setBrightness(newState.brightness); return true; } return false; } /** * Returns the current state. */ public State getCurrentState() { return mState; } /** * Returns the duration for the state change animation. */ public static int getDurationForStateChange(State fromState, State toState) { switch (toState) { case NORMAL: switch (fromState) { case PRESSED: return 0; case FAST_SCROLL_HIGHLIGHTED: case FAST_SCROLL_UNHIGHLIGHTED: return FAST_SCROLL_INACTIVE_DURATION; } case PRESSED: return CLICK_FEEDBACK_DURATION; case FAST_SCROLL_HIGHLIGHTED: return FAST_SCROLL_HIGHLIGHT_DURATION; case FAST_SCROLL_UNHIGHLIGHTED: switch (fromState) { case NORMAL: // When animating from normal state, take a little longer return FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION; default: return FAST_SCROLL_UNHIGHLIGHT_DURATION; } } return 0; } /** * Returns the start delay when animating between certain fast scroll states. */ public static int getStartDelayForStateChange(State fromState, State toState) { switch (toState) { case FAST_SCROLL_UNHIGHLIGHTED: switch (fromState) { case NORMAL: return FAST_SCROLL_UNHIGHLIGHT_DURATION / 4; } } return 0; } /** * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] */ public void setDesaturation(float desaturation) { int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE); if (mDesaturation != newDesaturation) { mDesaturation = newDesaturation; updateFilter(); } } public float getDesaturation() { return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE; } /** * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious] */ public void setBrightness(float brightness) { int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE); if (mBrightness != newBrightness) { mBrightness = newBrightness; updateFilter(); } } public float getBrightness() { return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE; } /** * Updates the paint to reflect the current brightness and saturation. */ private void updateFilter() { boolean usePorterDuffFilter = false; int key = -1; if (mDesaturation > 0) { key = (mDesaturation << 16) | mBrightness; } else if (mBrightness > 0) { // Compose a key with a fully saturated icon if we are just animating brightness key = (1 << 16) | mBrightness; // We found that in L, ColorFilters cause drawing artifacts with shadows baked into // icons, so just use a PorterDuff filter when we aren't animating saturation usePorterDuffFilter = true; } // Debounce multiple updates on the same frame if (key == mPrevUpdateKey) { return; } mPrevUpdateKey = key; if (key != -1) { ColorFilter filter = sCachedFilter.get(key); if (filter == null) { float brightnessF = getBrightness(); int brightnessI = (int) (255 * brightnessF); if (usePorterDuffFilter) { filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255), PorterDuff.Mode.SRC_ATOP); } else { float saturationF = 1f - getDesaturation(); sTempFilterMatrix.setSaturation(saturationF); if (mBrightness > 0) { // Brightness: C-new = C-old*(1-amount) + amount float scale = 1f - brightnessF; float[] mat = sTempBrightnessMatrix.getArray(); mat[0] = scale; mat[6] = scale; mat[12] = scale; mat[4] = brightnessI; mat[9] = brightnessI; mat[14] = brightnessI; sTempFilterMatrix.preConcat(sTempBrightnessMatrix); } filter = new ColorMatrixColorFilter(sTempFilterMatrix); } sCachedFilter.append(key, filter); } mPaint.setColorFilter(filter); } else { mPaint.setColorFilter(null); } invalidateSelf(); } private AnimatorSet cancelAnimator(AnimatorSet animator) { if (animator != null) { animator.removeAllListeners(); animator.cancel(); } return null; } }