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
17package com.android.launcher3;
18
19import android.animation.ObjectAnimator;
20import android.animation.TimeInterpolator;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.ColorFilter;
25import android.graphics.ColorMatrix;
26import android.graphics.ColorMatrixColorFilter;
27import android.graphics.Paint;
28import android.graphics.PixelFormat;
29import android.graphics.PorterDuff;
30import android.graphics.PorterDuffColorFilter;
31import android.graphics.Rect;
32import android.graphics.drawable.Drawable;
33import android.util.SparseArray;
34
35class FastBitmapDrawable extends Drawable {
36
37    static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
38
39        @Override
40        public float getInterpolation(float input) {
41            if (input < 0.05f) {
42                return input / 0.05f;
43            } else if (input < 0.3f){
44                return 1;
45            } else {
46                return (1 - input) / 0.7f;
47            }
48        }
49    };
50    static final long CLICK_FEEDBACK_DURATION = 2000;
51
52    private static final int PRESSED_BRIGHTNESS = 100;
53    private static ColorMatrix sGhostModeMatrix;
54    private static final ColorMatrix sTempMatrix = new ColorMatrix();
55
56    /**
57     * Store the brightness colors filters to optimize animations during icon press. This
58     * only works for non-ghost-mode icons.
59     */
60    private static final SparseArray<ColorFilter> sCachedBrightnessFilter =
61            new SparseArray<ColorFilter>();
62
63    private static final int GHOST_MODE_MIN_COLOR_RANGE = 130;
64
65    private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
66    private final Bitmap mBitmap;
67    private int mAlpha;
68
69    private int mBrightness = 0;
70    private boolean mGhostModeEnabled = false;
71
72    private boolean mPressed = false;
73    private ObjectAnimator mPressedAnimator;
74
75    FastBitmapDrawable(Bitmap b) {
76        mAlpha = 255;
77        mBitmap = b;
78        setBounds(0, 0, b.getWidth(), b.getHeight());
79    }
80
81    @Override
82    public void draw(Canvas canvas) {
83        final Rect r = getBounds();
84        // Draw the bitmap into the bounding rect
85        canvas.drawBitmap(mBitmap, null, r, mPaint);
86    }
87
88    @Override
89    public void setColorFilter(ColorFilter cf) {
90        // No op
91    }
92
93    @Override
94    public int getOpacity() {
95        return PixelFormat.TRANSLUCENT;
96    }
97
98    @Override
99    public void setAlpha(int alpha) {
100        mAlpha = alpha;
101        mPaint.setAlpha(alpha);
102    }
103
104    @Override
105    public void setFilterBitmap(boolean filterBitmap) {
106        mPaint.setFilterBitmap(filterBitmap);
107        mPaint.setAntiAlias(filterBitmap);
108    }
109
110    public int getAlpha() {
111        return mAlpha;
112    }
113
114    @Override
115    public int getIntrinsicWidth() {
116        return mBitmap.getWidth();
117    }
118
119    @Override
120    public int getIntrinsicHeight() {
121        return mBitmap.getHeight();
122    }
123
124    @Override
125    public int getMinimumWidth() {
126        return getBounds().width();
127    }
128
129    @Override
130    public int getMinimumHeight() {
131        return getBounds().height();
132    }
133
134    public Bitmap getBitmap() {
135        return mBitmap;
136    }
137
138    /**
139     * When enabled, the icon is grayed out and the contrast is increased to give it a 'ghost'
140     * appearance.
141     */
142    public void setGhostModeEnabled(boolean enabled) {
143        if (mGhostModeEnabled != enabled) {
144            mGhostModeEnabled = enabled;
145            updateFilter();
146        }
147    }
148
149    public void setPressed(boolean pressed) {
150        if (mPressed != pressed) {
151            mPressed = pressed;
152            if (mPressed) {
153                mPressedAnimator = ObjectAnimator
154                        .ofInt(this, "brightness", PRESSED_BRIGHTNESS)
155                        .setDuration(CLICK_FEEDBACK_DURATION);
156                mPressedAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR);
157                mPressedAnimator.start();
158            } else if (mPressedAnimator != null) {
159                mPressedAnimator.cancel();
160                setBrightness(0);
161            }
162        }
163        invalidateSelf();
164    }
165
166    public boolean isGhostModeEnabled() {
167        return mGhostModeEnabled;
168    }
169
170    public int getBrightness() {
171        return mBrightness;
172    }
173
174    public void setBrightness(int brightness) {
175        if (mBrightness != brightness) {
176            mBrightness = brightness;
177            updateFilter();
178            invalidateSelf();
179        }
180    }
181
182    private void updateFilter() {
183        if (mGhostModeEnabled) {
184            if (sGhostModeMatrix == null) {
185                sGhostModeMatrix = new ColorMatrix();
186                sGhostModeMatrix.setSaturation(0);
187
188                // For ghost mode, set the color range to [GHOST_MODE_MIN_COLOR_RANGE, 255]
189                float range = (255 - GHOST_MODE_MIN_COLOR_RANGE) / 255.0f;
190                sTempMatrix.set(new float[] {
191                        range, 0, 0, 0, GHOST_MODE_MIN_COLOR_RANGE,
192                        0, range, 0, 0, GHOST_MODE_MIN_COLOR_RANGE,
193                        0, 0, range, 0, GHOST_MODE_MIN_COLOR_RANGE,
194                        0, 0, 0, 1, 0 });
195                sGhostModeMatrix.preConcat(sTempMatrix);
196            }
197
198            if (mBrightness == 0) {
199                mPaint.setColorFilter(new ColorMatrixColorFilter(sGhostModeMatrix));
200            } else {
201                setBrightnessMatrix(sTempMatrix, mBrightness);
202                sTempMatrix.postConcat(sGhostModeMatrix);
203                mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix));
204            }
205        } else if (mBrightness != 0) {
206            ColorFilter filter = sCachedBrightnessFilter.get(mBrightness);
207            if (filter == null) {
208                filter = new PorterDuffColorFilter(Color.argb(mBrightness, 255, 255, 255),
209                        PorterDuff.Mode.SRC_ATOP);
210                sCachedBrightnessFilter.put(mBrightness, filter);
211            }
212            mPaint.setColorFilter(filter);
213        } else {
214            mPaint.setColorFilter(null);
215        }
216    }
217
218    private static void setBrightnessMatrix(ColorMatrix matrix, int brightness) {
219        // Brightness: C-new = C-old*(1-amount) + amount
220        float scale = 1 - brightness / 255.0f;
221        matrix.setScale(scale, scale, scale, 1);
222        float[] array = matrix.getArray();
223
224        // Add the amount to RGB components of the matrix, as per the above formula.
225        // Fifth elements in the array correspond to the constant being added to
226        // red, blue, green, and alpha channel respectively.
227        array[4] = brightness;
228        array[9] = brightness;
229        array[14] = brightness;
230    }
231}
232