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