summaryrefslogtreecommitdiffstats
path: root/src/com/android/launcher3/FastBitmapDrawable.java
blob: 38700805f6885286e49ea9daa6ea387276d4d722 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
/*
 * 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<ColorFilter> 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;
    }
}