ExtendedBitmapDrawable.java revision 5030ae34cd5978a8ab8a06f6c3b69b8645873122
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    private final ExtendedOptions mOpts;
53
54    // Parallax.
55    private static final float DECODE_VERTICAL_CENTER = 1f / 3;
56    private float mParallaxFraction = 1f / 2;
57
58    // State changes.
59    private static final int LOAD_STATE_UNINITIALIZED = 0;
60    private static final int LOAD_STATE_NOT_YET_LOADED = 1;
61    private static final int LOAD_STATE_LOADING = 2;
62    private static final int LOAD_STATE_LOADED = 3;
63    private static final int LOAD_STATE_FAILED = 4;
64    private int mLoadState = LOAD_STATE_UNINITIALIZED;
65    private Placeholder mPlaceholder;
66    private Progress mProgress;
67    private int mProgressDelayMs;
68    private final Handler mHandler = new Handler();
69
70    public static final boolean DEBUG = false;
71    public static final String TAG = ExtendedBitmapDrawable.class.getSimpleName();
72
73    public ExtendedBitmapDrawable(final Resources res, final BitmapCache cache,
74            final boolean limitDensity, final ExtendedOptions opts) {
75        super(res, cache, limitDensity);
76
77        opts.validate();
78        mOpts = opts;
79
80        // Placeholder and progress.
81        if ((opts.features & ExtendedOptions.FEATURE_STATE_CHANGES) != 0) {
82            final int fadeOutDurationMs = res.getInteger(R.integer.bitmap_fade_animation_duration);
83            mProgressDelayMs = res.getInteger(R.integer.bitmap_progress_animation_delay);
84
85            // Placeholder is not optional because of backgroundColor.
86            int placeholderSize = res.getDimensionPixelSize(R.dimen.placeholder_size);
87            mPlaceholder = new Placeholder(
88                    opts.placeholder != null ? opts.placeholder.getConstantState().newDrawable(res)
89                            : null, res, placeholderSize, placeholderSize, fadeOutDurationMs, opts);
90            mPlaceholder.setCallback(this);
91
92            // Progress bar is optional.
93            if (opts.progressBar != null) {
94                int progressBarSize = res.getDimensionPixelSize(R.dimen.progress_bar_size);
95                mProgress = new Progress(opts.progressBar.getConstantState().newDrawable(res), res,
96                        progressBarSize, progressBarSize, fadeOutDurationMs, opts);
97                mProgress.setCallback(this);
98            }
99        }
100    }
101
102    @Override
103    public void setParallaxFraction(float fraction) {
104        mParallaxFraction = fraction;
105        invalidateSelf();
106    }
107
108    /**
109     * Get the ExtendedOptions used to instantiate this ExtendedBitmapDrawable. Any changes made to
110     * the parameters inside the options will take effect immediately.
111     */
112    public ExtendedOptions getExtendedOptions() {
113        return mOpts;
114    }
115
116    /**
117     * This sets the drawable to the failed state, which remove all animations from the placeholder.
118     * This is different from unbinding to the uninitialized state, where we expect animations.
119     */
120    public void showStaticPlaceholder() {
121        setLoadState(LOAD_STATE_FAILED);
122    }
123
124    @Override
125    protected void setImage(final RequestKey key) {
126        if (mCurrKey != null && mCurrKey.equals(key)) {
127            return;
128        }
129
130        if (mCurrKey != null && getDecodeAggregator() != null) {
131            getDecodeAggregator().forget(mCurrKey);
132        }
133
134        mHandler.removeCallbacks(this);
135        // start from a clean slate on every bind
136        // this allows the initial transition to be specially instantaneous, so e.g. a cache hit
137        // doesn't unnecessarily trigger a fade-in
138        setLoadState(LOAD_STATE_UNINITIALIZED);
139        if (key == null) {
140            setLoadState(LOAD_STATE_FAILED);
141        }
142
143        super.setImage(key);
144    }
145
146    @Override
147    protected void setBitmap(ReusableBitmap bmp) {
148        setLoadState((bmp != null) ? LOAD_STATE_LOADED : LOAD_STATE_FAILED);
149
150        super.setBitmap(bmp);
151    }
152
153    @Override
154    protected void decode(final FileDescriptorFactory factory) {
155        boolean executeStateChange = shouldExecuteStateChange();
156        if (executeStateChange) {
157            setLoadState(LOAD_STATE_NOT_YET_LOADED);
158        }
159
160        super.decode(factory);
161    }
162
163    protected boolean shouldExecuteStateChange() {
164        // TODO: AttachmentDrawable should override this method to match prev and curr request keys.
165        return /* opts.stateChanges */ true;
166    }
167
168    @Override
169    public float getDrawVerticalCenter() {
170        return mParallaxFraction;
171    }
172
173    @Override
174    protected float getDrawVerticalOffsetMultiplier() {
175        return mOpts.parallaxSpeedMultiplier;
176    }
177
178    @Override
179    protected float getDecodeVerticalCenter() {
180        return DECODE_VERTICAL_CENTER;
181    }
182
183    private DecodeAggregator getDecodeAggregator() {
184        return mOpts.decodeAggregator;
185    }
186
187    /**
188     * Instead of overriding this method, subclasses should override {@link #onDraw(Canvas)}.
189     *
190     * The reason for this is that we need the placeholder and progress bar to be drawn over our
191     * content. Those two drawables fade out, giving the impression that our content is fading in.
192     */
193    @Override
194    public final void draw(final Canvas canvas) {
195        final Rect bounds = getBounds();
196        if (bounds.isEmpty()) {
197            return;
198        }
199
200        onDraw(canvas);
201
202        // Draw the two possible overlay layers in reverse-priority order.
203        // (each layer will no-op the draw when appropriate)
204        // This ordering means cross-fade transitions are just fade-outs of each layer.
205        if (mProgress != null) mProgress.draw(canvas);
206        if (mPlaceholder != null) mPlaceholder.draw(canvas);
207    }
208
209    /**
210     * Overriding this method to add your own custom drawing.
211     */
212    protected void onDraw(final Canvas canvas) {
213        super.draw(canvas);
214    }
215
216    @Override
217    public void setAlpha(int alpha) {
218        final int old = mPaint.getAlpha();
219        super.setAlpha(alpha);
220        if (mPlaceholder != null) mPlaceholder.setAlpha(alpha);
221        if (mProgress != null) mProgress.setAlpha(alpha);
222        if (alpha != old) {
223            invalidateSelf();
224        }
225    }
226
227    @Override
228    public void setColorFilter(ColorFilter cf) {
229        super.setColorFilter(cf);
230        if (mPlaceholder != null) mPlaceholder.setColorFilter(cf);
231        if (mProgress != null) mProgress.setColorFilter(cf);
232        invalidateSelf();
233    }
234
235    @Override
236    protected void onBoundsChange(Rect bounds) {
237        super.onBoundsChange(bounds);
238        if (mPlaceholder != null) mPlaceholder.setBounds(bounds);
239        if (mProgress != null) mProgress.setBounds(bounds);
240    }
241
242    @Override
243    public void onDecodeBegin(final RequestKey key) {
244        if (getDecodeAggregator() != null) {
245            getDecodeAggregator().expect(key, this);
246        } else {
247            onBecomeFirstExpected(key);
248        }
249        super.onDecodeBegin(key);
250    }
251
252    @Override
253    public void onBecomeFirstExpected(final RequestKey key) {
254        if (!key.equals(mCurrKey)) {
255            return;
256        }
257        // normally, we'd transition to the LOADING state now, but we want to delay that a bit
258        // to minimize excess occurrences of the rotating spinner
259        mHandler.postDelayed(this, mProgressDelayMs);
260    }
261
262    @Override
263    public void run() {
264        if (mLoadState == LOAD_STATE_NOT_YET_LOADED) {
265            setLoadState(LOAD_STATE_LOADING);
266        }
267    }
268
269    @Override
270    public void onDecodeComplete(final RequestKey key, final ReusableBitmap result) {
271        if (getDecodeAggregator() != null) {
272            getDecodeAggregator().execute(key, new Runnable() {
273                @Override
274                public void run() {
275                    ExtendedBitmapDrawable.super.onDecodeComplete(key, result);
276                }
277
278                @Override
279                public String toString() {
280                    return "DONE";
281                }
282            });
283        } else {
284            super.onDecodeComplete(key, result);
285        }
286    }
287
288    @Override
289    public void onDecodeCancel(final RequestKey key) {
290        if (getDecodeAggregator() != null) {
291            getDecodeAggregator().forget(key);
292        }
293        super.onDecodeCancel(key);
294    }
295
296    /**
297     * Each attachment gets its own placeholder and progress indicator, to be shown, hidden,
298     * and animated based on Drawable#setVisible() changes, which are in turn driven by
299     * setLoadState().
300     */
301    private void setLoadState(int loadState) {
302        if (DEBUG) {
303            Log.v(TAG, String.format("IN setLoadState. old=%s new=%s key=%s this=%s",
304                    mLoadState, loadState, mCurrKey, this));
305        }
306        if (mLoadState == loadState) {
307            if (DEBUG) {
308                Log.v(TAG, "OUT no-op setLoadState");
309            }
310            return;
311        }
312
313        Trace.beginSection("set load state");
314        switch (loadState) {
315            // This state differs from LOADED in that the subsequent state transition away from
316            // UNINITIALIZED will not have a fancy transition. This allows list item binds to
317            // cached data to take immediate effect without unnecessary whizzery.
318            case LOAD_STATE_UNINITIALIZED:
319                if (mPlaceholder != null) mPlaceholder.reset();
320                if (mProgress != null) mProgress.reset();
321                break;
322            case LOAD_STATE_NOT_YET_LOADED:
323                if (mPlaceholder != null) {
324                    mPlaceholder.setPulseEnabled(true);
325                    mPlaceholder.setVisible(true);
326                }
327                if (mProgress != null) mProgress.setVisible(false);
328                break;
329            case LOAD_STATE_LOADING:
330                if (mProgress == null) {
331                    // Stay in same visual state as LOAD_STATE_NOT_YET_LOADED.
332                    break;
333                }
334                if (mPlaceholder != null) mPlaceholder.setVisible(false);
335                if (mProgress != null) mProgress.setVisible(true);
336                break;
337            case LOAD_STATE_LOADED:
338                if (mPlaceholder != null) mPlaceholder.setVisible(false);
339                if (mProgress != null) mProgress.setVisible(false);
340                break;
341            case LOAD_STATE_FAILED:
342                if (mPlaceholder != null) {
343                    mPlaceholder.setPulseEnabled(false);
344                    mPlaceholder.setVisible(true);
345                }
346                if (mProgress != null) mProgress.setVisible(false);
347                break;
348        }
349        Trace.endSection();
350
351        mLoadState = loadState;
352        boolean placeholderVisible = mPlaceholder != null && mPlaceholder.isVisible();
353        boolean progressVisible = mProgress != null && mProgress.isVisible();
354
355        if (DEBUG) {
356            Log.v(TAG, String.format("OUT stateful setLoadState. new=%s placeholder=%s progress=%s",
357                    loadState, placeholderVisible, progressVisible));
358        }
359    }
360
361    private static class Placeholder extends TileDrawable {
362
363        private final ValueAnimator mPulseAnimator;
364        private boolean mPulseEnabled = true;
365        private float mPulseAlphaFraction = 1f;
366
367        public Placeholder(Drawable placeholder, Resources res, int placeholderWidth,
368                int placeholderHeight, int fadeOutDurationMs, ExtendedOptions opts) {
369            super(placeholder, placeholderWidth, placeholderHeight, fadeOutDurationMs, opts);
370
371            mPulseAnimator = ValueAnimator.ofInt(55, 255)
372                    .setDuration(res.getInteger(R.integer.bitmap_placeholder_animation_duration));
373            mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
374            mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE);
375            mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() {
376                @Override
377                public void onAnimationUpdate(ValueAnimator animation) {
378                    mPulseAlphaFraction = ((Integer) animation.getAnimatedValue()) / 255f;
379                    setInnerAlpha(getCurrentAlpha());
380                }
381            });
382            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
383                @Override
384                public void onAnimationEnd(Animator animation) {
385                    stopPulsing();
386                }
387            });
388        }
389
390        @Override
391        public void setInnerAlpha(final int alpha) {
392            super.setInnerAlpha((int) (alpha * mPulseAlphaFraction));
393        }
394
395        public void setPulseEnabled(boolean enabled) {
396            mPulseEnabled = enabled;
397            if (!mPulseEnabled) {
398                stopPulsing();
399            }
400        }
401
402        private void stopPulsing() {
403            if (mPulseAnimator != null) {
404                mPulseAnimator.cancel();
405                mPulseAlphaFraction = 1f;
406                setInnerAlpha(getCurrentAlpha());
407            }
408        }
409
410        @Override
411        public boolean setVisible(boolean visible) {
412            final boolean changed = super.setVisible(visible);
413            if (changed) {
414                if (isVisible()) {
415                    // start
416                    if (mPulseAnimator != null && mPulseEnabled) {
417                        mPulseAnimator.start();
418                    }
419                } else {
420                    // can't cancel the pulsing yet-- wait for the fade-out animation to end
421                    // one exception: if alpha is already zero, there is no fade-out, so stop now
422                    if (getCurrentAlpha() == 0) {
423                        stopPulsing();
424                    }
425                }
426            }
427            return changed;
428        }
429
430    }
431
432    private static class Progress extends TileDrawable {
433
434        private final ValueAnimator mRotateAnimator;
435
436        public Progress(Drawable progress, Resources res,
437                int progressBarWidth, int progressBarHeight, int fadeOutDurationMs,
438                ExtendedOptions opts) {
439            super(progress, progressBarWidth, progressBarHeight, fadeOutDurationMs, opts);
440
441            mRotateAnimator = ValueAnimator.ofInt(0, 10000)
442                    .setDuration(res.getInteger(R.integer.bitmap_progress_animation_duration));
443            mRotateAnimator.setInterpolator(new LinearInterpolator());
444            mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
445            mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() {
446                @Override
447                public void onAnimationUpdate(ValueAnimator animation) {
448                    setLevel((Integer) animation.getAnimatedValue());
449                }
450            });
451            mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
452                @Override
453                public void onAnimationEnd(Animator animation) {
454                    if (mRotateAnimator != null) {
455                        mRotateAnimator.cancel();
456                    }
457                }
458            });
459        }
460
461        @Override
462        public boolean setVisible(boolean visible) {
463            final boolean changed = super.setVisible(visible);
464            if (changed) {
465                if (isVisible()) {
466                    if (mRotateAnimator != null) {
467                        mRotateAnimator.start();
468                    }
469                } else {
470                    // can't cancel the rotate yet-- wait for the fade-out animation to end
471                    // one exception: if alpha is already zero, there is no fade-out, so stop now
472                    if (getCurrentAlpha() == 0 && mRotateAnimator != null) {
473                        mRotateAnimator.cancel();
474                    }
475                }
476            }
477            return changed;
478        }
479    }
480
481    /**
482     * This class contains the features a client can specify, and arguments to those features.
483     * Clients can later retrieve the ExtendedOptions from an ExtendedBitmapDrawable and change the
484     * parameters, which will be reflected immediately.
485     */
486    public static class ExtendedOptions {
487
488        /**
489         * Summary:
490         * This feature enables you to draw decoded bitmap in order on the screen, to give the
491         * visual effect of a single decode thread.
492         *
493         * <p/>
494         * Explanation:
495         * Since DecodeTasks are asynchronous, multiple tasks may finish decoding at different
496         * times. To have a smooth user experience, provide a shared {@link DecodeAggregator} to all
497         * the ExtendedBitmapDrawables, and the decode aggregator will hold finished decodes so they
498         * come back in order.
499         *
500         * <p/>
501         * Pros:
502         * Visual consistency. Images are not popping up randomly all over the place.
503         *
504         * <p/>
505         * Cons:
506         * Artificial delay. Images are not drawn as soon as they are decoded. They must wait
507         * for their turn.
508         *
509         * <p/>
510         * Requirements:
511         * Set {@link #decodeAggregator} to a shared {@link DecodeAggregator}.
512         */
513        public static final int FEATURE_ORDERED_DISPLAY = 1;
514
515        /**
516         * Summary:
517         * This feature enables the image to move in parallax as the user scrolls, to give visual
518         * flair to your images.
519         *
520         * <p/>
521         * Explanation:
522         * When the user scrolls D pixels in the vertical direction, this ExtendedBitmapDrawable
523         * shifts its Bitmap f(D) pixels in the vertical direction before drawing to the screen.
524         * Depending on the function f, the parallax effect can give varying interesting results.
525         *
526         * <p/>
527         * Pros:
528         * Visual pop and playfulness. Feeling of movement. Pleasantly surprise your users.
529         *
530         * <p/>
531         * Cons:
532         * Some users report motion sickness with certain speed multiplier values. Decode height
533         * must be greater than visual bounds to account for the parallax. This uses more memory and
534         * decoding time.
535         *
536         * <p/>
537         * Requirements:
538         * Set {@link #parallaxSpeedMultiplier} to the ratio between the decoded height and the
539         * visual bound height. Call {@link ExtendedBitmapDrawable#setParallaxFraction(float)} when
540         * the user scrolls, usually accomplished in your view's
541         * {@link android.view.View#onDraw(android.graphics.Canvas)} method.
542         */
543        public static final int FEATURE_PARALLAX = 1 << 1;
544
545        /**
546         * Summary:
547         * This feature enables fading in between multiple decode states, to give smooth transitions
548         * to and from the placeholder, progress bars, and decoded image.
549         *
550         * <p/>
551         * Explanation:
552         * The states are: {@link ExtendedBitmapDrawable#LOAD_STATE_UNINITIALIZED},
553         * {@link ExtendedBitmapDrawable#LOAD_STATE_NOT_YET_LOADED},
554         * {@link ExtendedBitmapDrawable#LOAD_STATE_LOADING},
555         * {@link ExtendedBitmapDrawable#LOAD_STATE_LOADED}, and
556         * {@link ExtendedBitmapDrawable#LOAD_STATE_FAILED}. These states affect whether the
557         * placeholder and/or the progress bar is showing and animating. We first show the
558         * pulsating placeholder when an image begins decoding. After 2 seconds, we fade in a
559         * spinning progress bar. When the decode completes, we fade in the image.
560         *
561         * <p/>
562         * Pros:
563         * Smooth, beautiful transitions avoid perceived jank. Progress indicator informs users that
564         * work is being done and the app is not stalled.
565         *
566         * <p/>
567         * Cons:
568         * Very fast decodes' short decode time would be eclipsed by the animation duration. Static
569         * placeholder could be accomplished by {@link BasicBitmapDrawable} without the added
570         * complexity of states.
571         *
572         * <p/>
573         * Requirements:
574         * Set {@link #backgroundColor} to the color used for the background of the placeholder and
575         * progress bar. Use the alternative constructor to populate {@link #placeholder} and
576         * {@link #progressBar}.
577         */
578        public static final int FEATURE_STATE_CHANGES = 1 << 2;
579
580        /**
581         * Non-changeable bit field describing the features you want the
582         * {@link ExtendedBitmapDrawable} to support.
583         *
584         * <p/>
585         * Example:
586         * <code>
587         * opts.features = FEATURE_ORDERED_DISPLAY | FEATURE_PARALLAX | FEATURE_STATE_CHANGES;
588         * </code>
589         */
590        public final int features;
591
592        /**
593         * Required field if {@link #FEATURE_ORDERED_DISPLAY} is supported.
594         */
595        public DecodeAggregator decodeAggregator = null;
596
597        /**
598         * Required field if {@link #FEATURE_PARALLAX} is supported.
599         *
600         * A value of 1.5f gives a subtle parallax, and is a good value to
601         * start with. 2.0f gives a more obvious parallax, arguably exaggerated. Some users report
602         * motion sickness with 2.0f. A value of 1.0f is synonymous with no parallax. Be careful not
603         * to set too high a value, since we will start cropping the widths if the image's height is
604         * not sufficient.
605         */
606        public float parallaxSpeedMultiplier = 1;
607
608        /**
609         * Optional field if {@link #FEATURE_STATE_CHANGES} is supported.
610         *
611         * See {@link android.graphics.Color}.
612         */
613        public int backgroundColor = 0;
614
615        /**
616         * Optional non-changeable field if {@link #FEATURE_STATE_CHANGES} is supported.
617         */
618        public final Drawable placeholder;
619
620        /**
621         * Optional non-changeable field if {@link #FEATURE_STATE_CHANGES} is supported.
622         */
623        public final Drawable progressBar;
624
625        /**
626         * Use this constructor when all the feature parameters are changeable.
627         */
628        public ExtendedOptions(final int features) {
629            this(features, null, null);
630        }
631
632        /**
633         * Use this constructor when you have to specify non-changeable feature parameters.
634         */
635        public ExtendedOptions(final int features, final Drawable placeholder,
636                final Drawable progressBar) {
637            this.features = features;
638            this.placeholder = placeholder;
639            this.progressBar = progressBar;
640        }
641
642        /**
643         * Validate this ExtendedOptions instance to make sure that all the required fields are set
644         * for the requested features.
645         *
646         * This will throw an IllegalStateException if validation fails.
647         */
648        private void validate()
649                throws IllegalStateException {
650            if ((features & FEATURE_ORDERED_DISPLAY) != 0 && decodeAggregator == null) {
651                throw new IllegalStateException(
652                        "ExtendedOptions: To support FEATURE_ORDERED_DISPLAY, "
653                                + "decodeAggregator must be set.");
654            }
655            if ((features & FEATURE_PARALLAX) != 0 && parallaxSpeedMultiplier == 0) {
656                throw new IllegalStateException(
657                        "ExtendedOptions: To support FEATURE_PARALLAX, "
658                                + "parallaxSpeedMultiplier must be set.");
659            }
660            if ((features & FEATURE_STATE_CHANGES) != 0 && backgroundColor == 0
661                    && placeholder == null) {
662                throw new IllegalStateException(
663                        "ExtendedOptions: To support FEATURE_STATE_CHANGES, "
664                                + "either backgroundColor or placeholder must be set.");
665            }
666        }
667    }
668}
669