1/*
2 * Copyright (C) 2006 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 android.graphics.drawable;
18
19import android.annotation.NonNull;
20import android.content.pm.ActivityInfo.Config;
21import android.content.res.ColorStateList;
22import android.content.res.Resources;
23import android.content.res.Resources.Theme;
24import android.graphics.Canvas;
25import android.graphics.ColorFilter;
26import android.graphics.Insets;
27import android.graphics.Outline;
28import android.graphics.PixelFormat;
29import android.graphics.PorterDuff.Mode;
30import android.graphics.Rect;
31import android.os.SystemClock;
32import android.util.DisplayMetrics;
33import android.util.LayoutDirection;
34import android.util.SparseArray;
35import android.view.View;
36
37/**
38 * A helper class that contains several {@link Drawable}s and selects which one to use.
39 *
40 * You can subclass it to create your own DrawableContainers or directly use one its child classes.
41 */
42public class DrawableContainer extends Drawable implements Drawable.Callback {
43    private static final boolean DEBUG = false;
44    private static final String TAG = "DrawableContainer";
45
46    /**
47     * To be proper, we should have a getter for dither (and alpha, etc.)
48     * so that proxy classes like this can save/restore their delegates'
49     * values, but we don't have getters. Since we do have setters
50     * (e.g. setDither), which this proxy forwards on, we have to have some
51     * default/initial setting.
52     *
53     * The initial setting for dither is now true, since it almost always seems
54     * to improve the quality at negligible cost.
55     */
56    private static final boolean DEFAULT_DITHER = true;
57    private DrawableContainerState mDrawableContainerState;
58    private Rect mHotspotBounds;
59    private Drawable mCurrDrawable;
60    private Drawable mLastDrawable;
61    private int mAlpha = 0xFF;
62
63    /** Whether setAlpha() has been called at least once. */
64    private boolean mHasAlpha;
65
66    private int mCurIndex = -1;
67    private int mLastIndex = -1;
68    private boolean mMutated;
69
70    // Animations.
71    private Runnable mAnimationRunnable;
72    private long mEnterAnimationEnd;
73    private long mExitAnimationEnd;
74
75    /** Callback that blocks invalidation. Used for drawable initialization. */
76    private BlockInvalidateCallback mBlockInvalidateCallback;
77
78    // overrides from Drawable
79
80    @Override
81    public void draw(Canvas canvas) {
82        if (mCurrDrawable != null) {
83            mCurrDrawable.draw(canvas);
84        }
85        if (mLastDrawable != null) {
86            mLastDrawable.draw(canvas);
87        }
88    }
89
90    @Override
91    public @Config int getChangingConfigurations() {
92        return super.getChangingConfigurations()
93                | mDrawableContainerState.getChangingConfigurations();
94    }
95
96    private boolean needsMirroring() {
97        return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
98    }
99
100    @Override
101    public boolean getPadding(Rect padding) {
102        final Rect r = mDrawableContainerState.getConstantPadding();
103        boolean result;
104        if (r != null) {
105            padding.set(r);
106            result = (r.left | r.top | r.bottom | r.right) != 0;
107        } else {
108            if (mCurrDrawable != null) {
109                result = mCurrDrawable.getPadding(padding);
110            } else {
111                result = super.getPadding(padding);
112            }
113        }
114        if (needsMirroring()) {
115            final int left = padding.left;
116            final int right = padding.right;
117            padding.left = right;
118            padding.right = left;
119        }
120        return result;
121    }
122
123    /**
124     * @hide
125     */
126    @Override
127    public Insets getOpticalInsets() {
128        if (mCurrDrawable != null) {
129            return mCurrDrawable.getOpticalInsets();
130        }
131        return Insets.NONE;
132    }
133
134    @Override
135    public void getOutline(@NonNull Outline outline) {
136        if (mCurrDrawable != null) {
137            mCurrDrawable.getOutline(outline);
138        }
139    }
140
141    @Override
142    public void setAlpha(int alpha) {
143        if (!mHasAlpha || mAlpha != alpha) {
144            mHasAlpha = true;
145            mAlpha = alpha;
146            if (mCurrDrawable != null) {
147                if (mEnterAnimationEnd == 0) {
148                    mCurrDrawable.setAlpha(alpha);
149                } else {
150                    animate(false);
151                }
152            }
153        }
154    }
155
156    @Override
157    public int getAlpha() {
158        return mAlpha;
159    }
160
161    @Override
162    public void setDither(boolean dither) {
163        if (mDrawableContainerState.mDither != dither) {
164            mDrawableContainerState.mDither = dither;
165            if (mCurrDrawable != null) {
166                mCurrDrawable.setDither(mDrawableContainerState.mDither);
167            }
168        }
169    }
170
171    @Override
172    public void setColorFilter(ColorFilter colorFilter) {
173        mDrawableContainerState.mHasColorFilter = true;
174
175        if (mDrawableContainerState.mColorFilter != colorFilter) {
176            mDrawableContainerState.mColorFilter = colorFilter;
177
178            if (mCurrDrawable != null) {
179                mCurrDrawable.setColorFilter(colorFilter);
180            }
181        }
182    }
183
184    @Override
185    public void setTintList(ColorStateList tint) {
186        mDrawableContainerState.mHasTintList = true;
187
188        if (mDrawableContainerState.mTintList != tint) {
189            mDrawableContainerState.mTintList = tint;
190
191            if (mCurrDrawable != null) {
192                mCurrDrawable.setTintList(tint);
193            }
194        }
195    }
196
197    @Override
198    public void setTintMode(Mode tintMode) {
199        mDrawableContainerState.mHasTintMode = true;
200
201        if (mDrawableContainerState.mTintMode != tintMode) {
202            mDrawableContainerState.mTintMode = tintMode;
203
204            if (mCurrDrawable != null) {
205                mCurrDrawable.setTintMode(tintMode);
206            }
207        }
208    }
209
210    /**
211     * Change the global fade duration when a new drawable is entering
212     * the scene.
213     *
214     * @param ms The amount of time to fade in milliseconds.
215     */
216    public void setEnterFadeDuration(int ms) {
217        mDrawableContainerState.mEnterFadeDuration = ms;
218    }
219
220    /**
221     * Change the global fade duration when a new drawable is leaving
222     * the scene.
223     *
224     * @param ms The amount of time to fade in milliseconds.
225     */
226    public void setExitFadeDuration(int ms) {
227        mDrawableContainerState.mExitFadeDuration = ms;
228    }
229
230    @Override
231    protected void onBoundsChange(Rect bounds) {
232        if (mLastDrawable != null) {
233            mLastDrawable.setBounds(bounds);
234        }
235        if (mCurrDrawable != null) {
236            mCurrDrawable.setBounds(bounds);
237        }
238    }
239
240    @Override
241    public boolean isStateful() {
242        return mDrawableContainerState.isStateful();
243    }
244
245    /** @hide */
246    @Override
247    public boolean hasFocusStateSpecified() {
248        if (mCurrDrawable != null) {
249            return mCurrDrawable.hasFocusStateSpecified();
250        }
251        if (mLastDrawable != null) {
252            return mLastDrawable.hasFocusStateSpecified();
253        }
254        return false;
255    }
256
257    @Override
258    public void setAutoMirrored(boolean mirrored) {
259        if (mDrawableContainerState.mAutoMirrored != mirrored) {
260            mDrawableContainerState.mAutoMirrored = mirrored;
261            if (mCurrDrawable != null) {
262                mCurrDrawable.setAutoMirrored(mDrawableContainerState.mAutoMirrored);
263            }
264        }
265    }
266
267    @Override
268    public boolean isAutoMirrored() {
269        return mDrawableContainerState.mAutoMirrored;
270    }
271
272    @Override
273    public void jumpToCurrentState() {
274        boolean changed = false;
275        if (mLastDrawable != null) {
276            mLastDrawable.jumpToCurrentState();
277            mLastDrawable = null;
278            mLastIndex = -1;
279            changed = true;
280        }
281        if (mCurrDrawable != null) {
282            mCurrDrawable.jumpToCurrentState();
283            if (mHasAlpha) {
284                mCurrDrawable.setAlpha(mAlpha);
285            }
286        }
287        if (mExitAnimationEnd != 0) {
288            mExitAnimationEnd = 0;
289            changed = true;
290        }
291        if (mEnterAnimationEnd != 0) {
292            mEnterAnimationEnd = 0;
293            changed = true;
294        }
295        if (changed) {
296            invalidateSelf();
297        }
298    }
299
300    @Override
301    public void setHotspot(float x, float y) {
302        if (mCurrDrawable != null) {
303            mCurrDrawable.setHotspot(x, y);
304        }
305    }
306
307    @Override
308    public void setHotspotBounds(int left, int top, int right, int bottom) {
309        if (mHotspotBounds == null) {
310            mHotspotBounds = new Rect(left, top, right, bottom);
311        } else {
312            mHotspotBounds.set(left, top, right, bottom);
313        }
314
315        if (mCurrDrawable != null) {
316            mCurrDrawable.setHotspotBounds(left, top, right, bottom);
317        }
318    }
319
320    @Override
321    public void getHotspotBounds(Rect outRect) {
322        if (mHotspotBounds != null) {
323            outRect.set(mHotspotBounds);
324        } else {
325            super.getHotspotBounds(outRect);
326        }
327    }
328
329    @Override
330    protected boolean onStateChange(int[] state) {
331        if (mLastDrawable != null) {
332            return mLastDrawable.setState(state);
333        }
334        if (mCurrDrawable != null) {
335            return mCurrDrawable.setState(state);
336        }
337        return false;
338    }
339
340    @Override
341    protected boolean onLevelChange(int level) {
342        if (mLastDrawable != null) {
343            return mLastDrawable.setLevel(level);
344        }
345        if (mCurrDrawable != null) {
346            return mCurrDrawable.setLevel(level);
347        }
348        return false;
349    }
350
351    @Override
352    public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
353        // Let the container handle setting its own layout direction. Otherwise,
354        // we're accessing potentially unused states.
355        return mDrawableContainerState.setLayoutDirection(layoutDirection, getCurrentIndex());
356    }
357
358    @Override
359    public int getIntrinsicWidth() {
360        if (mDrawableContainerState.isConstantSize()) {
361            return mDrawableContainerState.getConstantWidth();
362        }
363        return mCurrDrawable != null ? mCurrDrawable.getIntrinsicWidth() : -1;
364    }
365
366    @Override
367    public int getIntrinsicHeight() {
368        if (mDrawableContainerState.isConstantSize()) {
369            return mDrawableContainerState.getConstantHeight();
370        }
371        return mCurrDrawable != null ? mCurrDrawable.getIntrinsicHeight() : -1;
372    }
373
374    @Override
375    public int getMinimumWidth() {
376        if (mDrawableContainerState.isConstantSize()) {
377            return mDrawableContainerState.getConstantMinimumWidth();
378        }
379        return mCurrDrawable != null ? mCurrDrawable.getMinimumWidth() : 0;
380    }
381
382    @Override
383    public int getMinimumHeight() {
384        if (mDrawableContainerState.isConstantSize()) {
385            return mDrawableContainerState.getConstantMinimumHeight();
386        }
387        return mCurrDrawable != null ? mCurrDrawable.getMinimumHeight() : 0;
388    }
389
390    @Override
391    public void invalidateDrawable(@NonNull Drawable who) {
392        // This may have been called as the result of a tint changing, in
393        // which case we may need to refresh the cached statefulness or
394        // opacity.
395        if (mDrawableContainerState != null) {
396            mDrawableContainerState.invalidateCache();
397        }
398
399        if (who == mCurrDrawable && getCallback() != null) {
400            getCallback().invalidateDrawable(this);
401        }
402    }
403
404    @Override
405    public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
406        if (who == mCurrDrawable && getCallback() != null) {
407            getCallback().scheduleDrawable(this, what, when);
408        }
409    }
410
411    @Override
412    public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
413        if (who == mCurrDrawable && getCallback() != null) {
414            getCallback().unscheduleDrawable(this, what);
415        }
416    }
417
418    @Override
419    public boolean setVisible(boolean visible, boolean restart) {
420        boolean changed = super.setVisible(visible, restart);
421        if (mLastDrawable != null) {
422            mLastDrawable.setVisible(visible, restart);
423        }
424        if (mCurrDrawable != null) {
425            mCurrDrawable.setVisible(visible, restart);
426        }
427        return changed;
428    }
429
430    @Override
431    public int getOpacity() {
432        return mCurrDrawable == null || !mCurrDrawable.isVisible() ? PixelFormat.TRANSPARENT :
433                mDrawableContainerState.getOpacity();
434    }
435
436    /** @hide */
437    public void setCurrentIndex(int index) {
438        selectDrawable(index);
439    }
440
441    /** @hide */
442    public int getCurrentIndex() {
443        return mCurIndex;
444    }
445
446    /**
447     * Sets the currently displayed drawable by index.
448     * <p>
449     * If an invalid index is specified, the current drawable will be set to
450     * {@code null} and the index will be set to {@code -1}.
451     *
452     * @param index the index of the drawable to display
453     * @return {@code true} if the drawable changed, {@code false} otherwise
454     */
455    public boolean selectDrawable(int index) {
456        if (index == mCurIndex) {
457            return false;
458        }
459
460        final long now = SystemClock.uptimeMillis();
461
462        if (DEBUG) android.util.Log.i(TAG, toString() + " from " + mCurIndex + " to " + index
463                + ": exit=" + mDrawableContainerState.mExitFadeDuration
464                + " enter=" + mDrawableContainerState.mEnterFadeDuration);
465
466        if (mDrawableContainerState.mExitFadeDuration > 0) {
467            if (mLastDrawable != null) {
468                mLastDrawable.setVisible(false, false);
469            }
470            if (mCurrDrawable != null) {
471                mLastDrawable = mCurrDrawable;
472                mLastIndex = mCurIndex;
473                mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration;
474            } else {
475                mLastDrawable = null;
476                mLastIndex = -1;
477                mExitAnimationEnd = 0;
478            }
479        } else if (mCurrDrawable != null) {
480            mCurrDrawable.setVisible(false, false);
481        }
482
483        if (index >= 0 && index < mDrawableContainerState.mNumChildren) {
484            final Drawable d = mDrawableContainerState.getChild(index);
485            mCurrDrawable = d;
486            mCurIndex = index;
487            if (d != null) {
488                if (mDrawableContainerState.mEnterFadeDuration > 0) {
489                    mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
490                }
491                initializeDrawableForDisplay(d);
492            }
493        } else {
494            mCurrDrawable = null;
495            mCurIndex = -1;
496        }
497
498        if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
499            if (mAnimationRunnable == null) {
500                mAnimationRunnable = new Runnable() {
501                    @Override public void run() {
502                        animate(true);
503                        invalidateSelf();
504                    }
505                };
506            } else {
507                unscheduleSelf(mAnimationRunnable);
508            }
509            // Compute first frame and schedule next animation.
510            animate(true);
511        }
512
513        invalidateSelf();
514
515        return true;
516    }
517
518    /**
519     * Initializes a drawable for display in this container.
520     *
521     * @param d The drawable to initialize.
522     */
523    private void initializeDrawableForDisplay(Drawable d) {
524        if (mBlockInvalidateCallback == null) {
525            mBlockInvalidateCallback = new BlockInvalidateCallback();
526        }
527
528        // Temporary fix for suspending callbacks during initialization. We
529        // don't want any of these setters causing an invalidate() since that
530        // may call back into DrawableContainer.
531        d.setCallback(mBlockInvalidateCallback.wrap(d.getCallback()));
532
533        try {
534            if (mDrawableContainerState.mEnterFadeDuration <= 0 && mHasAlpha) {
535                d.setAlpha(mAlpha);
536            }
537
538            if (mDrawableContainerState.mHasColorFilter) {
539                // Color filter always overrides tint.
540                d.setColorFilter(mDrawableContainerState.mColorFilter);
541            } else {
542                if (mDrawableContainerState.mHasTintList) {
543                    d.setTintList(mDrawableContainerState.mTintList);
544                }
545                if (mDrawableContainerState.mHasTintMode) {
546                    d.setTintMode(mDrawableContainerState.mTintMode);
547                }
548            }
549
550            d.setVisible(isVisible(), true);
551            d.setDither(mDrawableContainerState.mDither);
552            d.setState(getState());
553            d.setLevel(getLevel());
554            d.setBounds(getBounds());
555            d.setLayoutDirection(getLayoutDirection());
556            d.setAutoMirrored(mDrawableContainerState.mAutoMirrored);
557
558            final Rect hotspotBounds = mHotspotBounds;
559            if (hotspotBounds != null) {
560                d.setHotspotBounds(hotspotBounds.left, hotspotBounds.top,
561                        hotspotBounds.right, hotspotBounds.bottom);
562            }
563        } finally {
564            d.setCallback(mBlockInvalidateCallback.unwrap());
565        }
566    }
567
568    void animate(boolean schedule) {
569        mHasAlpha = true;
570
571        final long now = SystemClock.uptimeMillis();
572        boolean animating = false;
573        if (mCurrDrawable != null) {
574            if (mEnterAnimationEnd != 0) {
575                if (mEnterAnimationEnd <= now) {
576                    mCurrDrawable.setAlpha(mAlpha);
577                    mEnterAnimationEnd = 0;
578                } else {
579                    int animAlpha = (int)((mEnterAnimationEnd-now)*255)
580                            / mDrawableContainerState.mEnterFadeDuration;
581                    mCurrDrawable.setAlpha(((255-animAlpha)*mAlpha)/255);
582                    animating = true;
583                }
584            }
585        } else {
586            mEnterAnimationEnd = 0;
587        }
588        if (mLastDrawable != null) {
589            if (mExitAnimationEnd != 0) {
590                if (mExitAnimationEnd <= now) {
591                    mLastDrawable.setVisible(false, false);
592                    mLastDrawable = null;
593                    mLastIndex = -1;
594                    mExitAnimationEnd = 0;
595                } else {
596                    int animAlpha = (int)((mExitAnimationEnd-now)*255)
597                            / mDrawableContainerState.mExitFadeDuration;
598                    mLastDrawable.setAlpha((animAlpha*mAlpha)/255);
599                    animating = true;
600                }
601            }
602        } else {
603            mExitAnimationEnd = 0;
604        }
605
606        if (schedule && animating) {
607            scheduleSelf(mAnimationRunnable, now + 1000 / 60);
608        }
609    }
610
611    @Override
612    public Drawable getCurrent() {
613        return mCurrDrawable;
614    }
615
616    /**
617     * Updates the source density based on the resources used to inflate
618     * density-dependent values. Implementing classes should call this method
619     * during inflation.
620     *
621     * @param res the resources used to inflate density-dependent values
622     * @hide
623     */
624    protected final void updateDensity(Resources res) {
625        mDrawableContainerState.updateDensity(res);
626    }
627
628    @Override
629    public void applyTheme(Theme theme) {
630        mDrawableContainerState.applyTheme(theme);
631    }
632
633    @Override
634    public boolean canApplyTheme() {
635        return mDrawableContainerState.canApplyTheme();
636    }
637
638    @Override
639    public ConstantState getConstantState() {
640        if (mDrawableContainerState.canConstantState()) {
641            mDrawableContainerState.mChangingConfigurations = getChangingConfigurations();
642            return mDrawableContainerState;
643        }
644        return null;
645    }
646
647    @Override
648    public Drawable mutate() {
649        if (!mMutated && super.mutate() == this) {
650            final DrawableContainerState clone = cloneConstantState();
651            clone.mutate();
652            setConstantState(clone);
653            mMutated = true;
654        }
655        return this;
656    }
657
658    /**
659     * Returns a shallow copy of the container's constant state to be used as
660     * the base state for {@link #mutate()}.
661     *
662     * @return a shallow copy of the constant state
663     */
664    DrawableContainerState cloneConstantState() {
665        return mDrawableContainerState;
666    }
667
668    /**
669     * @hide
670     */
671    public void clearMutated() {
672        super.clearMutated();
673        mDrawableContainerState.clearMutated();
674        mMutated = false;
675    }
676
677    /**
678     * A ConstantState that can contain several {@link Drawable}s.
679     *
680     * This class was made public to enable testing, and its visibility may change in a future
681     * release.
682     */
683    public abstract static class DrawableContainerState extends ConstantState {
684        final DrawableContainer mOwner;
685
686        Resources mSourceRes;
687        int mDensity = DisplayMetrics.DENSITY_DEFAULT;
688        @Config int mChangingConfigurations;
689        @Config int mChildrenChangingConfigurations;
690
691        SparseArray<ConstantState> mDrawableFutures;
692        Drawable[] mDrawables;
693        int mNumChildren;
694
695        boolean mVariablePadding = false;
696        boolean mCheckedPadding;
697        Rect mConstantPadding;
698
699        boolean mConstantSize = false;
700        boolean mCheckedConstantSize;
701        int mConstantWidth;
702        int mConstantHeight;
703        int mConstantMinimumWidth;
704        int mConstantMinimumHeight;
705
706        boolean mCheckedOpacity;
707        int mOpacity;
708
709        boolean mCheckedStateful;
710        boolean mStateful;
711
712        boolean mCheckedConstantState;
713        boolean mCanConstantState;
714
715        boolean mDither = DEFAULT_DITHER;
716
717        boolean mMutated;
718        int mLayoutDirection;
719
720        int mEnterFadeDuration = 0;
721        int mExitFadeDuration = 0;
722
723        boolean mAutoMirrored;
724
725        ColorFilter mColorFilter;
726        boolean mHasColorFilter;
727
728        ColorStateList mTintList;
729        Mode mTintMode;
730        boolean mHasTintList;
731        boolean mHasTintMode;
732
733        /**
734         * @hide
735         */
736        protected DrawableContainerState(DrawableContainerState orig, DrawableContainer owner,
737                Resources res) {
738            mOwner = owner;
739            mSourceRes = res != null ? res : (orig != null ? orig.mSourceRes : null);
740            mDensity = Drawable.resolveDensity(res, orig != null ? orig.mDensity : 0);
741
742            if (orig != null) {
743                mChangingConfigurations = orig.mChangingConfigurations;
744                mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations;
745
746                mCheckedConstantState = true;
747                mCanConstantState = true;
748
749                mVariablePadding = orig.mVariablePadding;
750                mConstantSize = orig.mConstantSize;
751                mDither = orig.mDither;
752                mMutated = orig.mMutated;
753                mLayoutDirection = orig.mLayoutDirection;
754                mEnterFadeDuration = orig.mEnterFadeDuration;
755                mExitFadeDuration = orig.mExitFadeDuration;
756                mAutoMirrored = orig.mAutoMirrored;
757                mColorFilter = orig.mColorFilter;
758                mHasColorFilter = orig.mHasColorFilter;
759                mTintList = orig.mTintList;
760                mTintMode = orig.mTintMode;
761                mHasTintList = orig.mHasTintList;
762                mHasTintMode = orig.mHasTintMode;
763
764                if (orig.mDensity == mDensity) {
765                    if (orig.mCheckedPadding) {
766                        mConstantPadding = new Rect(orig.mConstantPadding);
767                        mCheckedPadding = true;
768                    }
769
770                    if (orig.mCheckedConstantSize) {
771                        mConstantWidth = orig.mConstantWidth;
772                        mConstantHeight = orig.mConstantHeight;
773                        mConstantMinimumWidth = orig.mConstantMinimumWidth;
774                        mConstantMinimumHeight = orig.mConstantMinimumHeight;
775                        mCheckedConstantSize = true;
776                    }
777                }
778
779                if (orig.mCheckedOpacity) {
780                    mOpacity = orig.mOpacity;
781                    mCheckedOpacity = true;
782                }
783
784                if (orig.mCheckedStateful) {
785                    mStateful = orig.mStateful;
786                    mCheckedStateful = true;
787                }
788
789                // Postpone cloning children and futures until we're absolutely
790                // sure that we're done computing values for the original state.
791                final Drawable[] origDr = orig.mDrawables;
792                mDrawables = new Drawable[origDr.length];
793                mNumChildren = orig.mNumChildren;
794
795                final SparseArray<ConstantState> origDf = orig.mDrawableFutures;
796                if (origDf != null) {
797                    mDrawableFutures = origDf.clone();
798                } else {
799                    mDrawableFutures = new SparseArray<>(mNumChildren);
800                }
801
802                // Create futures for drawables with constant states. If a
803                // drawable doesn't have a constant state, then we can't clone
804                // it and we'll have to reference the original.
805                final int N = mNumChildren;
806                for (int i = 0; i < N; i++) {
807                    if (origDr[i] != null) {
808                        final ConstantState cs = origDr[i].getConstantState();
809                        if (cs != null) {
810                            mDrawableFutures.put(i, cs);
811                        } else {
812                            mDrawables[i] = origDr[i];
813                        }
814                    }
815                }
816            } else {
817                mDrawables = new Drawable[10];
818                mNumChildren = 0;
819            }
820        }
821
822        @Override
823        public @Config int getChangingConfigurations() {
824            return mChangingConfigurations | mChildrenChangingConfigurations;
825        }
826
827        /**
828         * Adds the drawable to the end of the list of contained drawables.
829         *
830         * @param dr the drawable to add
831         * @return the position of the drawable within the container
832         */
833        public final int addChild(Drawable dr) {
834            final int pos = mNumChildren;
835            if (pos >= mDrawables.length) {
836                growArray(pos, pos+10);
837            }
838
839            dr.mutate();
840            dr.setVisible(false, true);
841            dr.setCallback(mOwner);
842
843            mDrawables[pos] = dr;
844            mNumChildren++;
845            mChildrenChangingConfigurations |= dr.getChangingConfigurations();
846
847            invalidateCache();
848
849            mConstantPadding = null;
850            mCheckedPadding = false;
851            mCheckedConstantSize = false;
852            mCheckedConstantState = false;
853
854            return pos;
855        }
856
857        /**
858         * Invalidates the cached opacity and statefulness.
859         */
860        void invalidateCache() {
861            mCheckedOpacity = false;
862            mCheckedStateful = false;
863        }
864
865        final int getCapacity() {
866            return mDrawables.length;
867        }
868
869        private void createAllFutures() {
870            if (mDrawableFutures != null) {
871                final int futureCount = mDrawableFutures.size();
872                for (int keyIndex = 0; keyIndex < futureCount; keyIndex++) {
873                    final int index = mDrawableFutures.keyAt(keyIndex);
874                    final ConstantState cs = mDrawableFutures.valueAt(keyIndex);
875                    mDrawables[index] = prepareDrawable(cs.newDrawable(mSourceRes));
876                }
877
878                mDrawableFutures = null;
879            }
880        }
881
882        private Drawable prepareDrawable(Drawable child) {
883            child.setLayoutDirection(mLayoutDirection);
884            child = child.mutate();
885            child.setCallback(mOwner);
886            return child;
887        }
888
889        public final int getChildCount() {
890            return mNumChildren;
891        }
892
893        /*
894         * @deprecated Use {@link #getChild} instead.
895         */
896        public final Drawable[] getChildren() {
897            // Create all futures for backwards compatibility.
898            createAllFutures();
899
900            return mDrawables;
901        }
902
903        public final Drawable getChild(int index) {
904            final Drawable result = mDrawables[index];
905            if (result != null) {
906                return result;
907            }
908
909            // Prepare future drawable if necessary.
910            if (mDrawableFutures != null) {
911                final int keyIndex = mDrawableFutures.indexOfKey(index);
912                if (keyIndex >= 0) {
913                    final ConstantState cs = mDrawableFutures.valueAt(keyIndex);
914                    final Drawable prepared = prepareDrawable(cs.newDrawable(mSourceRes));
915                    mDrawables[index] = prepared;
916                    mDrawableFutures.removeAt(keyIndex);
917                    if (mDrawableFutures.size() == 0) {
918                        mDrawableFutures = null;
919                    }
920                    return prepared;
921                }
922            }
923
924            return null;
925        }
926
927        final boolean setLayoutDirection(int layoutDirection, int currentIndex) {
928            boolean changed = false;
929
930            // No need to call createAllFutures, since future drawables will
931            // change layout direction when they are prepared.
932            final int N = mNumChildren;
933            final Drawable[] drawables = mDrawables;
934            for (int i = 0; i < N; i++) {
935                if (drawables[i] != null) {
936                    final boolean childChanged = drawables[i].setLayoutDirection(layoutDirection);
937                    if (i == currentIndex) {
938                        changed = childChanged;
939                    }
940                }
941            }
942
943            mLayoutDirection = layoutDirection;
944
945            return changed;
946        }
947
948        /**
949         * Updates the source density based on the resources used to inflate
950         * density-dependent values.
951         *
952         * @param res the resources used to inflate density-dependent values
953         */
954        final void updateDensity(Resources res) {
955            if (res != null) {
956                mSourceRes = res;
957
958                // The density may have changed since the last update (if any). Any
959                // dimension-type attributes will need their default values scaled.
960                final int targetDensity = Drawable.resolveDensity(res, mDensity);
961                final int sourceDensity = mDensity;
962                mDensity = targetDensity;
963
964                if (sourceDensity != targetDensity) {
965                    mCheckedConstantSize = false;
966                    mCheckedPadding = false;
967                }
968            }
969        }
970
971        final void applyTheme(Theme theme) {
972            if (theme != null) {
973                createAllFutures();
974
975                final int N = mNumChildren;
976                final Drawable[] drawables = mDrawables;
977                for (int i = 0; i < N; i++) {
978                    if (drawables[i] != null && drawables[i].canApplyTheme()) {
979                        drawables[i].applyTheme(theme);
980
981                        // Update cached mask of child changing configurations.
982                        mChildrenChangingConfigurations |= drawables[i].getChangingConfigurations();
983                    }
984                }
985
986                updateDensity(theme.getResources());
987            }
988        }
989
990        @Override
991        public boolean canApplyTheme() {
992            final int N = mNumChildren;
993            final Drawable[] drawables = mDrawables;
994            for (int i = 0; i < N; i++) {
995                final Drawable d = drawables[i];
996                if (d != null) {
997                    if (d.canApplyTheme()) {
998                        return true;
999                    }
1000                } else {
1001                    final ConstantState future = mDrawableFutures.get(i);
1002                    if (future != null && future.canApplyTheme()) {
1003                        return true;
1004                    }
1005                }
1006            }
1007
1008            return false;
1009        }
1010
1011        private void mutate() {
1012            // No need to call createAllFutures, since future drawables will
1013            // mutate when they are prepared.
1014            final int N = mNumChildren;
1015            final Drawable[] drawables = mDrawables;
1016            for (int i = 0; i < N; i++) {
1017                if (drawables[i] != null) {
1018                    drawables[i].mutate();
1019                }
1020            }
1021
1022            mMutated = true;
1023        }
1024
1025        final void clearMutated() {
1026            final int N = mNumChildren;
1027            final Drawable[] drawables = mDrawables;
1028            for (int i = 0; i < N; i++) {
1029                if (drawables[i] != null) {
1030                    drawables[i].clearMutated();
1031                }
1032            }
1033
1034            mMutated = false;
1035        }
1036
1037        /**
1038         * A boolean value indicating whether to use the maximum padding value
1039         * of all frames in the set (false), or to use the padding value of the
1040         * frame being shown (true). Default value is false.
1041         */
1042        public final void setVariablePadding(boolean variable) {
1043            mVariablePadding = variable;
1044        }
1045
1046        public final Rect getConstantPadding() {
1047            if (mVariablePadding) {
1048                return null;
1049            }
1050
1051            if ((mConstantPadding != null) || mCheckedPadding) {
1052                return mConstantPadding;
1053            }
1054
1055            createAllFutures();
1056
1057            Rect r = null;
1058            final Rect t = new Rect();
1059            final int N = mNumChildren;
1060            final Drawable[] drawables = mDrawables;
1061            for (int i = 0; i < N; i++) {
1062                if (drawables[i].getPadding(t)) {
1063                    if (r == null) r = new Rect(0, 0, 0, 0);
1064                    if (t.left > r.left) r.left = t.left;
1065                    if (t.top > r.top) r.top = t.top;
1066                    if (t.right > r.right) r.right = t.right;
1067                    if (t.bottom > r.bottom) r.bottom = t.bottom;
1068                }
1069            }
1070
1071            mCheckedPadding = true;
1072            return (mConstantPadding = r);
1073        }
1074
1075        public final void setConstantSize(boolean constant) {
1076            mConstantSize = constant;
1077        }
1078
1079        public final boolean isConstantSize() {
1080            return mConstantSize;
1081        }
1082
1083        public final int getConstantWidth() {
1084            if (!mCheckedConstantSize) {
1085                computeConstantSize();
1086            }
1087
1088            return mConstantWidth;
1089        }
1090
1091        public final int getConstantHeight() {
1092            if (!mCheckedConstantSize) {
1093                computeConstantSize();
1094            }
1095
1096            return mConstantHeight;
1097        }
1098
1099        public final int getConstantMinimumWidth() {
1100            if (!mCheckedConstantSize) {
1101                computeConstantSize();
1102            }
1103
1104            return mConstantMinimumWidth;
1105        }
1106
1107        public final int getConstantMinimumHeight() {
1108            if (!mCheckedConstantSize) {
1109                computeConstantSize();
1110            }
1111
1112            return mConstantMinimumHeight;
1113        }
1114
1115        protected void computeConstantSize() {
1116            mCheckedConstantSize = true;
1117
1118            createAllFutures();
1119
1120            final int N = mNumChildren;
1121            final Drawable[] drawables = mDrawables;
1122            mConstantWidth = mConstantHeight = -1;
1123            mConstantMinimumWidth = mConstantMinimumHeight = 0;
1124            for (int i = 0; i < N; i++) {
1125                final Drawable dr = drawables[i];
1126                int s = dr.getIntrinsicWidth();
1127                if (s > mConstantWidth) mConstantWidth = s;
1128                s = dr.getIntrinsicHeight();
1129                if (s > mConstantHeight) mConstantHeight = s;
1130                s = dr.getMinimumWidth();
1131                if (s > mConstantMinimumWidth) mConstantMinimumWidth = s;
1132                s = dr.getMinimumHeight();
1133                if (s > mConstantMinimumHeight) mConstantMinimumHeight = s;
1134            }
1135        }
1136
1137        public final void setEnterFadeDuration(int duration) {
1138            mEnterFadeDuration = duration;
1139        }
1140
1141        public final int getEnterFadeDuration() {
1142            return mEnterFadeDuration;
1143        }
1144
1145        public final void setExitFadeDuration(int duration) {
1146            mExitFadeDuration = duration;
1147        }
1148
1149        public final int getExitFadeDuration() {
1150            return mExitFadeDuration;
1151        }
1152
1153        public final int getOpacity() {
1154            if (mCheckedOpacity) {
1155                return mOpacity;
1156            }
1157
1158            createAllFutures();
1159
1160            final int N = mNumChildren;
1161            final Drawable[] drawables = mDrawables;
1162            int op = (N > 0) ? drawables[0].getOpacity() : PixelFormat.TRANSPARENT;
1163            for (int i = 1; i < N; i++) {
1164                op = Drawable.resolveOpacity(op, drawables[i].getOpacity());
1165            }
1166
1167            mOpacity = op;
1168            mCheckedOpacity = true;
1169            return op;
1170        }
1171
1172        public final boolean isStateful() {
1173            if (mCheckedStateful) {
1174                return mStateful;
1175            }
1176
1177            createAllFutures();
1178
1179            final int N = mNumChildren;
1180            final Drawable[] drawables = mDrawables;
1181            boolean isStateful = false;
1182            for (int i = 0; i < N; i++) {
1183                if (drawables[i].isStateful()) {
1184                    isStateful = true;
1185                    break;
1186                }
1187            }
1188
1189            mStateful = isStateful;
1190            mCheckedStateful = true;
1191            return isStateful;
1192        }
1193
1194        public void growArray(int oldSize, int newSize) {
1195            Drawable[] newDrawables = new Drawable[newSize];
1196            System.arraycopy(mDrawables, 0, newDrawables, 0, oldSize);
1197            mDrawables = newDrawables;
1198        }
1199
1200        public synchronized boolean canConstantState() {
1201            if (mCheckedConstantState) {
1202                return mCanConstantState;
1203            }
1204
1205            createAllFutures();
1206
1207            mCheckedConstantState = true;
1208
1209            final int N = mNumChildren;
1210            final Drawable[] drawables = mDrawables;
1211            for (int i = 0; i < N; i++) {
1212                if (drawables[i].getConstantState() == null) {
1213                    mCanConstantState = false;
1214                    return false;
1215                }
1216            }
1217
1218            mCanConstantState = true;
1219            return true;
1220        }
1221
1222    }
1223
1224    protected void setConstantState(DrawableContainerState state) {
1225        mDrawableContainerState = state;
1226
1227        // The locally cached drawables may have changed.
1228        if (mCurIndex >= 0) {
1229            mCurrDrawable = state.getChild(mCurIndex);
1230            if (mCurrDrawable != null) {
1231                initializeDrawableForDisplay(mCurrDrawable);
1232            }
1233        }
1234
1235        // Clear out the last drawable. We don't have enough information to
1236        // propagate local state from the past.
1237        mLastIndex = -1;
1238        mLastDrawable = null;
1239    }
1240
1241    /**
1242     * Callback that blocks drawable invalidation.
1243     */
1244    private static class BlockInvalidateCallback implements Drawable.Callback {
1245        private Drawable.Callback mCallback;
1246
1247        public BlockInvalidateCallback wrap(Drawable.Callback callback) {
1248            mCallback = callback;
1249            return this;
1250        }
1251
1252        public Drawable.Callback unwrap() {
1253            final Drawable.Callback callback = mCallback;
1254            mCallback = null;
1255            return callback;
1256        }
1257
1258        @Override
1259        public void invalidateDrawable(@NonNull Drawable who) {
1260            // Ignore invalidation.
1261        }
1262
1263        @Override
1264        public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
1265            if (mCallback != null) {
1266                mCallback.scheduleDrawable(who, what, when);
1267            }
1268        }
1269
1270        @Override
1271        public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
1272            if (mCallback != null) {
1273                mCallback.unscheduleDrawable(who, what);
1274            }
1275        }
1276    }
1277}
1278