| /* |
| * 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.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.Property; |
| import android.util.SparseArray; |
| |
| import com.android.launcher3.graphics.IconPalette; |
| |
| public class FastBitmapDrawable extends Drawable { |
| |
| private static final float PRESSED_BRIGHTNESS = 100f / 255f; |
| private static final float DISABLED_DESATURATION = 1f; |
| private static final float DISABLED_BRIGHTNESS = 0.5f; |
| |
| 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; |
| |
| // 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<ColorFilter> sCachedFilter = new SparseArray<>(); |
| |
| // Temporary matrices used for calculation |
| private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix(); |
| private static final ColorMatrix sTempFilterMatrix = new ColorMatrix(); |
| |
| protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); |
| private final Bitmap mBitmap; |
| |
| private boolean mIsPressed; |
| private boolean mIsDisabled; |
| |
| private IconPalette mIconPalette; |
| |
| private static final Property<FastBitmapDrawable, Float> BRIGHTNESS |
| = new Property<FastBitmapDrawable, Float>(Float.TYPE, "brightness") { |
| @Override |
| public Float get(FastBitmapDrawable fastBitmapDrawable) { |
| return fastBitmapDrawable.getBrightness(); |
| } |
| |
| @Override |
| public void set(FastBitmapDrawable fastBitmapDrawable, Float value) { |
| fastBitmapDrawable.setBrightness(value); |
| } |
| }; |
| |
| // 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 brightness |
| private ObjectAnimator mBrightnessAnimator; |
| |
| public FastBitmapDrawable(Bitmap b) { |
| mBitmap = b; |
| setFilterBitmap(true); |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| canvas.drawBitmap(mBitmap, null, getBounds(), mPaint); |
| } |
| |
| public IconPalette getIconPalette() { |
| if (mIconPalette == null) { |
| mIconPalette = IconPalette.fromDominantColor(Utilities |
| .findDominantColorByHue(mBitmap, 20), true /* desaturateBackground */); |
| } |
| return mIconPalette; |
| } |
| |
| @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; |
| } |
| |
| @Override |
| public boolean isStateful() { |
| return true; |
| } |
| |
| @Override |
| public ColorFilter getColorFilter() { |
| return mPaint.getColorFilter(); |
| } |
| |
| @Override |
| protected boolean onStateChange(int[] state) { |
| boolean isPressed = false; |
| for (int s : state) { |
| if (s == android.R.attr.state_pressed) { |
| isPressed = true; |
| break; |
| } |
| } |
| if (mIsPressed != isPressed) { |
| mIsPressed = isPressed; |
| |
| if (mBrightnessAnimator != null) { |
| mBrightnessAnimator.cancel(); |
| } |
| |
| if (mIsPressed) { |
| // Animate when going to pressed state |
| mBrightnessAnimator = ObjectAnimator.ofFloat( |
| this, BRIGHTNESS, getExpectedBrightness()); |
| mBrightnessAnimator.setDuration(CLICK_FEEDBACK_DURATION); |
| mBrightnessAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR); |
| mBrightnessAnimator.start(); |
| } else { |
| setBrightness(getExpectedBrightness()); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private void invalidateDesaturationAndBrightness() { |
| setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0); |
| setBrightness(getExpectedBrightness()); |
| } |
| |
| private float getExpectedBrightness() { |
| return mIsDisabled ? DISABLED_BRIGHTNESS : |
| (mIsPressed ? PRESSED_BRIGHTNESS : 0); |
| } |
| |
| public void setIsDisabled(boolean isDisabled) { |
| if (mIsDisabled != isDisabled) { |
| mIsDisabled = isDisabled; |
| invalidateDesaturationAndBrightness(); |
| } |
| } |
| |
| /** |
| * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] |
| */ |
| private 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] |
| */ |
| private void setBrightness(float brightness) { |
| int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE); |
| if (mBrightness != newBrightness) { |
| mBrightness = newBrightness; |
| updateFilter(); |
| } |
| } |
| |
| private 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(); |
| } |
| } |