ExtendedBitmapDrawable.java revision 2e4d0863dba53435372ec96538f2ef3e1c3675bf
1/*
2 * Copyright (C) 2013 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.bitmap.drawable;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.animation.ValueAnimator.AnimatorUpdateListener;
23import android.content.res.Resources;
24import android.graphics.Canvas;
25import android.graphics.ColorFilter;
26import android.graphics.Rect;
27import android.graphics.drawable.Drawable;
28import android.os.Handler;
29import android.util.Log;
30import android.view.animation.LinearInterpolator;
31
32import com.android.bitmap.BitmapCache;
33import com.android.bitmap.DecodeAggregator;
34import com.android.bitmap.DecodeTask;
35import com.android.bitmap.R;
36import com.android.bitmap.RequestKey;
37import com.android.bitmap.RequestKey.FileDescriptorFactory;
38import com.android.bitmap.ReusableBitmap;
39import com.android.bitmap.util.Trace;
40
41/**
42 * This class encapsulates all functionality needed to display a single image bitmap,
43 * including request creation/cancelling, data unbinding and re-binding, and fancy animations
44 * to draw upon state changes.
45 * <p>
46 * The actual bitmap decode work is handled by {@link DecodeTask}.
47 * TODO: have this class extend from BasicBitmapDrawable
48 */
49public class ExtendedBitmapDrawable extends BasicBitmapDrawable implements
50    Runnable, Parallaxable, DecodeAggregator.Callback {
51
52    // Ordered display.
53    private DecodeAggregator mDecodeAggregator;
54
55    // Parallax.
56    private float mParallaxFraction = 0.5f;
57    private float mParallaxSpeedMultiplier;
58    private static final float DECODE_VERTICAL_CENTER = 1f / 3;
59
60    // Placeholder and progress.
61    private static final int LOAD_STATE_UNINITIALIZED = 0;
62    private static final int LOAD_STATE_NOT_YET_LOADED = 1;
63    private static final int LOAD_STATE_LOADING = 2;
64    private static final int LOAD_STATE_LOADED = 3;
65    private static final int LOAD_STATE_FAILED = 4;
66    private int mLoadState = LOAD_STATE_UNINITIALIZED;
67    private Placeholder mPlaceholder;
68    private Progress mProgress;
69    private int mProgressDelayMs;
70    private final Handler mHandler = new Handler();
71
72    public static final boolean DEBUG = false;
73    public static final String TAG = ExtendedBitmapDrawable.class.getSimpleName();
74
75    public ExtendedBitmapDrawable(final Resources res, final BitmapCache cache,
76            final boolean limitDensity, final DecodeAggregator decodeAggregator,
77            final Drawable placeholder, final Drawable progress) {
78        super(res, cache, limitDensity);
79
80        // Ordered display.
81        this.mDecodeAggregator = decodeAggregator;
82
83        // Placeholder and progress.
84        final int fadeOutDurationMs = res.getInteger(R.integer.bitmap_fade_animation_duration);
85        final int tileColor = res.getColor(R.color.bitmap_placeholder_background_color);
86        mProgressDelayMs = res.getInteger(R.integer.bitmap_progress_animation_delay);
87
88        int placeholderSize = res.getDimensionPixelSize(R.dimen.placeholder_size);
89        mPlaceholder = new Placeholder(placeholder.getConstantState().newDrawable(res), res,
90                placeholderSize, placeholderSize, fadeOutDurationMs, tileColor);
91        mPlaceholder.setCallback(this);
92
93        int progressBarSize = res.getDimensionPixelSize(R.dimen.progress_bar_size);
94        mProgress = new Progress(progress.getConstantState().newDrawable(res), res,
95                progressBarSize, progressBarSize, fadeOutDurationMs, tileColor);
96        mProgress.setCallback(this);
97    }
98
99    @Override
100    public void setParallaxFraction(float fraction) {
101        mParallaxFraction = fraction;
102        invalidateSelf();
103    }
104
105    public void setParallaxSpeedMultiplier(final float parallaxSpeedMultiplier) {
106        mParallaxSpeedMultiplier = parallaxSpeedMultiplier;
107        invalidateSelf();
108    }
109
110    /**
111     * This sets the drawable to the failed state, which remove all animations from the placeholder.
112     * This is different from unbinding to the uninitialized state, where we expect animations.
113     */
114    public void showStaticPlaceholder() {
115        setLoadState(LOAD_STATE_FAILED);
116    }
117
118    @Override
119    protected void setImage(final RequestKey key) {
120        if (mCurrKey != null && mCurrKey.equals(key)) {
121            return;
122        }
123
124        if (mCurrKey != null && mDecodeAggregator != null) {
125            mDecodeAggregator.forget(mCurrKey);
126        }
127
128        mHandler.removeCallbacks(this);
129        // start from a clean slate on every bind
130        // this allows the initial transition to be specially instantaneous, so e.g. a cache hit
131        // doesn't unnecessarily trigger a fade-in
132        setLoadState(LOAD_STATE_UNINITIALIZED);
133        if (key == null) {
134            setLoadState(LOAD_STATE_FAILED);
135        }
136
137        super.setImage(key);
138    }
139
140    @Override
141    protected void setBitmap(ReusableBitmap bmp) {
142        setLoadState((bmp != null) ? LOAD_STATE_LOADED : LOAD_STATE_FAILED);
143
144        super.setBitmap(bmp);
145    }
146
147    @Override
148    protected void decode(final FileDescriptorFactory factory) {
149        boolean executeStateChange = shouldExecuteStateChange();
150        if (executeStateChange) {
151            setLoadState(LOAD_STATE_NOT_YET_LOADED);
152        }
153
154        super.decode(factory);
155    }
156
157    protected boolean shouldExecuteStateChange() {
158        // TODO: AttachmentDrawable should override this method to match prev and curr request keys.
159        return /* opts.stateChanges */ true;
160    }
161
162    @Override
163    public float getDrawVerticalCenter() {
164        return mParallaxFraction;
165    }
166
167    @Override
168    protected float getDrawVerticalOffsetMultiplier() {
169        return mParallaxSpeedMultiplier;
170    }
171
172    @Override
173    protected float getDecodeVerticalCenter() {
174        return DECODE_VERTICAL_CENTER;
175    }
176
177    @Override
178    public void draw(final Canvas canvas) {
179        final Rect bounds = getBounds();
180        if (bounds.isEmpty()) {
181            return;
182        }
183
184        super.draw(canvas);
185
186        // Draw the two possible overlay layers in reverse-priority order.
187        // (each layer will no-op the draw when appropriate)
188        // This ordering means cross-fade transitions are just fade-outs of each layer.
189        mProgress.draw(canvas);
190        mPlaceholder.draw(canvas);
191    }
192
193    @Override
194    public void setAlpha(int alpha) {
195        final int old = mPaint.getAlpha();
196        super.setAlpha(alpha);
197        mPlaceholder.setAlpha(alpha);
198        mProgress.setAlpha(alpha);
199        if (alpha != old) {
200            invalidateSelf();
201        }
202    }
203
204    @Override
205    public void setColorFilter(ColorFilter cf) {
206        super.setColorFilter(cf);
207        mPlaceholder.setColorFilter(cf);
208        mProgress.setColorFilter(cf);
209        invalidateSelf();
210    }
211
212    @Override
213    protected void onBoundsChange(Rect bounds) {
214        super.onBoundsChange(bounds);
215
216        mPlaceholder.setBounds(bounds);
217        mProgress.setBounds(bounds);
218    }
219
220    @Override
221    public void onDecodeBegin(final RequestKey key) {
222        if (mDecodeAggregator != null) {
223            mDecodeAggregator.expect(key, this);
224        } else {
225            onBecomeFirstExpected(key);
226        }
227        super.onDecodeBegin(key);
228    }
229
230    @Override
231    public void onBecomeFirstExpected(final RequestKey key) {
232        if (!key.equals(mCurrKey)) {
233            return;
234        }
235        // normally, we'd transition to the LOADING state now, but we want to delay that a bit
236        // to minimize excess occurrences of the rotating spinner
237        mHandler.postDelayed(this, mProgressDelayMs);
238    }
239
240    @Override
241    public void run() {
242        if (mLoadState == LOAD_STATE_NOT_YET_LOADED) {
243            setLoadState(LOAD_STATE_LOADING);
244        }
245    }
246
247    @Override
248    public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
249        if (mDecodeAggregator != null) {
250            mDecodeAggregator.execute(key, new Runnable() {
251                @Override
252                public void run() {
253                    ExtendedBitmapDrawable.super.onDecodeComplete(key, result);
254                }
255
256                @Override
257                public String toString() {
258                    return "DONE";
259                }
260            });
261        } else {
262            super.onDecodeComplete(key, result);
263        }
264    }
265
266    @Override
267    public void onDecodeCancel(final RequestKey key) {
268        if (mDecodeAggregator != null) {
269            mDecodeAggregator.forget(key);
270        }
271        super.onDecodeCancel(key);
272    }
273
274    /**
275     * Each attachment gets its own placeholder and progress indicator, to be shown, hidden,
276     * and animated based on Drawable#setVisible() changes, which are in turn driven by
277     * setLoadState().
278     */
279    private void setLoadState(int loadState) {
280        if (DEBUG) {
281            Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s",
282                    mLoadState, loadState, mCurrKey, this));
283        }
284        if (mLoadState == loadState) {
285            if (DEBUG) {
286                Log.v(TAG, "OUT no-op setLoadState");
287            }
288            return;
289        }
290
291        Trace.beginSection("set load state");
292        switch (loadState) {
293            // This state differs from LOADED in that the subsequent state transition away from
294            // UNINITIALIZED will not have a fancy transition. This allows list item binds to
295            // cached data to take immediate effect without unnecessary whizzery.
296            case LOAD_STATE_UNINITIALIZED:
297                mPlaceholder.reset();
298                mProgress.reset();
299                break;
300            case LOAD_STATE_NOT_YET_LOADED:
301                mPlaceholder.setPulseEnabled(true);
302                mPlaceholder.setVisible(true);
303                mProgress.setVisible(false);
304                break;
305            case LOAD_STATE_LOADING:
306                mPlaceholder.setVisible(false);
307                mProgress.setVisible(true);
308                break;
309            case LOAD_STATE_LOADED:
310                mPlaceholder.setVisible(false);
311                mProgress.setVisible(false);
312                break;
313            case LOAD_STATE_FAILED:
314                mPlaceholder.setPulseEnabled(false);
315                mPlaceholder.setVisible(true);
316                mProgress.setVisible(false);
317                break;
318        }
319        Trace.endSection();
320
321        mLoadState = loadState;
322        boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible();
323        boolean progressVisible = mProgress != null && mProgress.isVisible();
324
325        if (DEBUG) {
326            Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s",
327                    loadState, placeholderVisible, progressVisible));
328        }
329    }
330
331    private static class Placeholder extends TileDrawable {
332
333        private final ValueAnimator mPulseAnimator;
334        private boolean mPulseEnabled = true;
335        private float mPulseAlphaFraction = 1f;
336
337        public Placeholder(Drawable placeholder, Resources res,
338                int placeholderWidth, int placeholderHeight, int fadeOutDurationMs,
339                int tileColor) {
340            super(placeholder, placeholderWidth, placeholderHeight, tileColor, fadeOutDurationMs);
341            mPulseAnimator = ValueAnimator.ofInt(55, 255)
342                    .setDuration(res.getInteger(R.integer.bitmap_placeholder_animation_duration));
343            mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
344            mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE);
345            mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() {
346                @Override
347                public void onAnimationUpdate(ValueAnimator animation) {
348                    mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f;
349                    setInnerAlpha(getCurrentAlpha());
350                }
351            });
352            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
353                @Override
354                public void onAnimationEnd(Animator animation) {
355                    stopPulsing();
356                }
357            });
358        }
359
360        @Override
361        public void setInnerAlpha(final int alpha) {
362            super.setInnerAlpha((int) (alpha * mPulseAlphaFraction));
363        }
364
365        public void setPulseEnabled(boolean enabled) {
366            mPulseEnabled = enabled;
367            if (!mPulseEnabled) {
368                stopPulsing();
369            }
370        }
371
372        private void stopPulsing() {
373            if (mPulseAnimator != null) {
374                mPulseAnimator.cancel();
375                mPulseAlphaFraction = 1f;
376                setInnerAlpha(getCurrentAlpha());
377            }
378        }
379
380        @Override
381        public boolean setVisible(boolean visible) {
382            final boolean changed = super.setVisible(visible);
383            if (changed) {
384                if (isVisible()) {
385                    // start
386                    if (mPulseAnimator != null && mPulseEnabled) {
387                        mPulseAnimator.start();
388                    }
389                } else {
390                    // can't cancel the pulsing yet-- wait for the fade-out animation to end
391                    // one exception: if alpha is already zero, there is no fade-out, so stop now
392                    if (getCurrentAlpha() == 0) {
393                        stopPulsing();
394                    }
395                }
396            }
397            return changed;
398        }
399
400    }
401
402    private static class Progress extends TileDrawable {
403
404        private final ValueAnimator mRotateAnimator;
405
406        public Progress(Drawable progress, Resources res,
407                int progressBarWidth, int progressBarHeight, int fadeOutDurationMs,
408                int tileColor) {
409            super(progress, progressBarWidth, progressBarHeight, tileColor, fadeOutDurationMs);
410
411            mRotateAnimator = ValueAnimator.ofInt(0, 10000)
412                    .setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration));
413            mRotateAnimator.setInterpolator(new LinearInterpolator());
414            mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
415            mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() {
416                @Override
417                public void onAnimationUpdate(ValueAnimator animation) {
418                    setLevel((Integer) animation.getAnimatedValue());
419                }
420            });
421            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
422                @Override
423                public void onAnimationEnd(Animator animation) {
424                    if (mRotateAnimator != null) {
425                        mRotateAnimator.cancel();
426                    }
427                }
428            });
429        }
430
431        @Override
432        public boolean setVisible(boolean visible) {
433            final boolean changed = super.setVisible(visible);
434            if (changed) {
435                if (isVisible()) {
436                    if (mRotateAnimator != null) {
437                        mRotateAnimator.start();
438                    }
439                } else {
440                    // can't cancel the rotate yet-- wait for the fade-out animation to end
441                    // one exception: if alpha is already zero, there is no fade-out, so stop now
442                    if (getCurrentAlpha() == 0 && mRotateAnimator != null) {
443                        mRotateAnimator.cancel();
444                    }
445                }
446            }
447            return changed;
448        }
449
450    }
451}
452