1 /*
2  * Copyright (C) 2015 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.annotation.Nullable;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.pm.ActivityInfo.Config;
23 import android.content.res.ColorStateList;
24 import android.content.res.Resources;
25 import android.content.res.Resources.Theme;
26 import android.content.res.TypedArray;
27 import android.graphics.BlendMode;
28 import android.graphics.Canvas;
29 import android.graphics.ColorFilter;
30 import android.graphics.Insets;
31 import android.graphics.Outline;
32 import android.graphics.PixelFormat;
33 import android.graphics.Rect;
34 import android.graphics.Xfermode;
35 import android.os.Build;
36 import android.util.AttributeSet;
37 import android.util.DisplayMetrics;
38 import android.view.View;
39 
40 import com.android.internal.R;
41 
42 import org.xmlpull.v1.XmlPullParser;
43 import org.xmlpull.v1.XmlPullParserException;
44 
45 import java.io.IOException;
46 
47 /**
48  * Drawable container with only one child element.
49  */
50 public abstract class DrawableWrapper extends Drawable implements Drawable.Callback {
51     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
52     private DrawableWrapperState mState;
53     private Drawable mDrawable;
54     private boolean mMutated;
55 
DrawableWrapper(DrawableWrapperState state, Resources res)56     DrawableWrapper(DrawableWrapperState state, Resources res) {
57         mState = state;
58 
59         updateLocalState(res);
60     }
61 
62     /**
63      * Creates a new wrapper around the specified drawable.
64      *
65      * @param dr the drawable to wrap
66      */
DrawableWrapper(@ullable Drawable dr)67     public DrawableWrapper(@Nullable Drawable dr) {
68         mState = null;
69         setDrawable(dr);
70     }
71 
72     /**
73      * Initializes local dynamic properties from state. This should be called
74      * after significant state changes, e.g. from the One True Constructor and
75      * after inflating or applying a theme.
76      */
updateLocalState(Resources res)77     private void updateLocalState(Resources res) {
78         if (mState != null && mState.mDrawableState != null) {
79             final Drawable dr = mState.mDrawableState.newDrawable(res);
80             setDrawable(dr);
81         }
82     }
83 
84     /**
85      * @hide
86      */
87     @Override
setXfermode(Xfermode mode)88     public void setXfermode(Xfermode mode) {
89         if (mDrawable != null) {
90             mDrawable.setXfermode(mode);
91         }
92     }
93 
94     /**
95      * Sets the wrapped drawable.
96      *
97      * @param dr the wrapped drawable
98      */
setDrawable(@ullable Drawable dr)99     public void setDrawable(@Nullable Drawable dr) {
100         if (mDrawable != null) {
101             mDrawable.setCallback(null);
102         }
103 
104         mDrawable = dr;
105 
106         if (dr != null) {
107             dr.setCallback(this);
108 
109             // Only call setters for data that's stored in the base Drawable.
110             dr.setVisible(isVisible(), true);
111             dr.setState(getState());
112             dr.setLevel(getLevel());
113             dr.setBounds(getBounds());
114             dr.setLayoutDirection(getLayoutDirection());
115 
116             if (mState != null) {
117                 mState.mDrawableState = dr.getConstantState();
118             }
119         }
120 
121         invalidateSelf();
122     }
123 
124     /**
125      * @return the wrapped drawable
126      */
127     @Nullable
getDrawable()128     public Drawable getDrawable() {
129         return mDrawable;
130     }
131 
132     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)133     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
134             @NonNull AttributeSet attrs, @Nullable Theme theme)
135             throws XmlPullParserException, IOException {
136         super.inflate(r, parser, attrs, theme);
137 
138         final DrawableWrapperState state = mState;
139         if (state == null) {
140             return;
141         }
142 
143         // The density may have changed since the last update. This will
144         // apply scaling to any existing constant state properties.
145         final int densityDpi = r.getDisplayMetrics().densityDpi;
146         final int targetDensity = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
147         state.setDensity(targetDensity);
148         state.mSrcDensityOverride = mSrcDensityOverride;
149 
150         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.DrawableWrapper);
151         updateStateFromTypedArray(a);
152         a.recycle();
153 
154         inflateChildDrawable(r, parser, attrs, theme);
155     }
156 
157     @Override
applyTheme(@onNull Theme t)158     public void applyTheme(@NonNull Theme t) {
159         super.applyTheme(t);
160 
161         // If we load the drawable later as part of updating from the typed
162         // array, it will already be themed correctly. So, we can theme the
163         // local drawable first.
164         if (mDrawable != null && mDrawable.canApplyTheme()) {
165             mDrawable.applyTheme(t);
166         }
167 
168         final DrawableWrapperState state = mState;
169         if (state == null) {
170             return;
171         }
172 
173         final int densityDpi = t.getResources().getDisplayMetrics().densityDpi;
174         final int density = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
175         state.setDensity(density);
176 
177         if (state.mThemeAttrs != null) {
178             final TypedArray a = t.resolveAttributes(
179                     state.mThemeAttrs, R.styleable.DrawableWrapper);
180             updateStateFromTypedArray(a);
181             a.recycle();
182         }
183     }
184 
185     /**
186      * Updates constant state properties from the provided typed array.
187      * <p>
188      * Implementing subclasses should call through to the super method first.
189      *
190      * @param a the typed array rom which properties should be read
191      */
updateStateFromTypedArray(@onNull TypedArray a)192     private void updateStateFromTypedArray(@NonNull TypedArray a) {
193         final DrawableWrapperState state = mState;
194         if (state == null) {
195             return;
196         }
197 
198         // Account for any configuration changes.
199         state.mChangingConfigurations |= a.getChangingConfigurations();
200 
201         // Extract the theme attributes, if any.
202         state.mThemeAttrs = a.extractThemeAttrs();
203 
204         if (a.hasValueOrEmpty(R.styleable.DrawableWrapper_drawable)) {
205             setDrawable(a.getDrawable(R.styleable.DrawableWrapper_drawable));
206         }
207     }
208 
209     @Override
canApplyTheme()210     public boolean canApplyTheme() {
211         return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
212     }
213 
214     @Override
invalidateDrawable(@onNull Drawable who)215     public void invalidateDrawable(@NonNull Drawable who) {
216         final Callback callback = getCallback();
217         if (callback != null) {
218             callback.invalidateDrawable(this);
219         }
220     }
221 
222     @Override
scheduleDrawable(@onNull Drawable who, @NonNull Runnable what, long when)223     public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
224         final Callback callback = getCallback();
225         if (callback != null) {
226             callback.scheduleDrawable(this, what, when);
227         }
228     }
229 
230     @Override
unscheduleDrawable(@onNull Drawable who, @NonNull Runnable what)231     public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
232         final Callback callback = getCallback();
233         if (callback != null) {
234             callback.unscheduleDrawable(this, what);
235         }
236     }
237 
238     @Override
draw(@onNull Canvas canvas)239     public void draw(@NonNull Canvas canvas) {
240         if (mDrawable != null) {
241             mDrawable.draw(canvas);
242         }
243     }
244 
245     @Override
getChangingConfigurations()246     public @Config int getChangingConfigurations() {
247         return super.getChangingConfigurations()
248                 | (mState != null ? mState.getChangingConfigurations() : 0)
249                 | mDrawable.getChangingConfigurations();
250     }
251 
252     @Override
getPadding(@onNull Rect padding)253     public boolean getPadding(@NonNull Rect padding) {
254         return mDrawable != null && mDrawable.getPadding(padding);
255     }
256 
257     @Override
getOpticalInsets()258     public Insets getOpticalInsets() {
259         return mDrawable != null ? mDrawable.getOpticalInsets() : Insets.NONE;
260     }
261 
262     @Override
setHotspot(float x, float y)263     public void setHotspot(float x, float y) {
264         if (mDrawable != null) {
265             mDrawable.setHotspot(x, y);
266         }
267     }
268 
269     @Override
setHotspotBounds(int left, int top, int right, int bottom)270     public void setHotspotBounds(int left, int top, int right, int bottom) {
271         if (mDrawable != null) {
272             mDrawable.setHotspotBounds(left, top, right, bottom);
273         }
274     }
275 
276     @Override
getHotspotBounds(@onNull Rect outRect)277     public void getHotspotBounds(@NonNull Rect outRect) {
278         if (mDrawable != null) {
279             mDrawable.getHotspotBounds(outRect);
280         } else {
281             outRect.set(getBounds());
282         }
283     }
284 
285     @Override
setVisible(boolean visible, boolean restart)286     public boolean setVisible(boolean visible, boolean restart) {
287         final boolean superChanged = super.setVisible(visible, restart);
288         final boolean changed = mDrawable != null && mDrawable.setVisible(visible, restart);
289         return superChanged | changed;
290     }
291 
292     @Override
setAlpha(int alpha)293     public void setAlpha(int alpha) {
294         if (mDrawable != null) {
295             mDrawable.setAlpha(alpha);
296         }
297     }
298 
299     @Override
getAlpha()300     public int getAlpha() {
301         return mDrawable != null ? mDrawable.getAlpha() : 255;
302     }
303 
304     @Override
setColorFilter(@ullable ColorFilter colorFilter)305     public void setColorFilter(@Nullable ColorFilter colorFilter) {
306         if (mDrawable != null) {
307             mDrawable.setColorFilter(colorFilter);
308         }
309     }
310 
311     @Override
getColorFilter()312     public ColorFilter getColorFilter() {
313         final Drawable drawable = getDrawable();
314         if (drawable != null) {
315             return drawable.getColorFilter();
316         }
317         return super.getColorFilter();
318     }
319 
320     @Override
setTintList(@ullable ColorStateList tint)321     public void setTintList(@Nullable ColorStateList tint) {
322         if (mDrawable != null) {
323             mDrawable.setTintList(tint);
324         }
325     }
326 
327     @Override
setTintBlendMode(@onNull BlendMode blendMode)328     public void setTintBlendMode(@NonNull BlendMode blendMode) {
329         if (mDrawable != null) {
330             mDrawable.setTintBlendMode(blendMode);
331         }
332     }
333 
334     @Override
onLayoutDirectionChanged(@iew.ResolvedLayoutDir int layoutDirection)335     public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
336         return mDrawable != null && mDrawable.setLayoutDirection(layoutDirection);
337     }
338 
339     @Override
getOpacity()340     public int getOpacity() {
341         return mDrawable != null ? mDrawable.getOpacity() : PixelFormat.TRANSPARENT;
342     }
343 
344     @Override
isStateful()345     public boolean isStateful() {
346         return mDrawable != null && mDrawable.isStateful();
347     }
348 
349     @Override
hasFocusStateSpecified()350     public boolean hasFocusStateSpecified() {
351         return mDrawable != null && mDrawable.hasFocusStateSpecified();
352     }
353 
354     @Override
onStateChange(@onNull int[] state)355     protected boolean onStateChange(@NonNull int[] state) {
356         if (mDrawable != null && mDrawable.isStateful()) {
357             final boolean changed = mDrawable.setState(state);
358             if (changed) {
359                 onBoundsChange(getBounds());
360             }
361             return changed;
362         }
363         return false;
364     }
365 
366     @Override
jumpToCurrentState()367     public void jumpToCurrentState() {
368         if (mDrawable != null) {
369             mDrawable.jumpToCurrentState();
370         }
371     }
372 
373     @Override
onLevelChange(int level)374     protected boolean onLevelChange(int level) {
375         return mDrawable != null && mDrawable.setLevel(level);
376     }
377 
378     @Override
onBoundsChange(@onNull Rect bounds)379     protected void onBoundsChange(@NonNull Rect bounds) {
380         if (mDrawable != null) {
381             mDrawable.setBounds(bounds);
382         }
383     }
384 
385     @Override
getIntrinsicWidth()386     public int getIntrinsicWidth() {
387         return mDrawable != null ? mDrawable.getIntrinsicWidth() : -1;
388     }
389 
390     @Override
getIntrinsicHeight()391     public int getIntrinsicHeight() {
392         return mDrawable != null ? mDrawable.getIntrinsicHeight() : -1;
393     }
394 
395     @Override
getOutline(@onNull Outline outline)396     public void getOutline(@NonNull Outline outline) {
397         if (mDrawable != null) {
398             mDrawable.getOutline(outline);
399         } else {
400             super.getOutline(outline);
401         }
402     }
403 
404     @Override
405     @Nullable
getConstantState()406     public ConstantState getConstantState() {
407         if (mState != null && mState.canConstantState()) {
408             mState.mChangingConfigurations = getChangingConfigurations();
409             return mState;
410         }
411         return null;
412     }
413 
414     @Override
415     @NonNull
mutate()416     public Drawable mutate() {
417         if (!mMutated && super.mutate() == this) {
418             mState = mutateConstantState();
419             if (mDrawable != null) {
420                 mDrawable.mutate();
421             }
422             if (mState != null) {
423                 mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null;
424             }
425             mMutated = true;
426         }
427         return this;
428     }
429 
430     /**
431      * Mutates the constant state and returns the new state. Responsible for
432      * updating any local copy.
433      * <p>
434      * This method should never call the super implementation; it should always
435      * mutate and return its own constant state.
436      *
437      * @return the new state
438      */
mutateConstantState()439     DrawableWrapperState mutateConstantState() {
440         return mState;
441     }
442 
443     /**
444      * @hide Only used by the framework for pre-loading resources.
445      */
clearMutated()446     public void clearMutated() {
447         super.clearMutated();
448         if (mDrawable != null) {
449             mDrawable.clearMutated();
450         }
451         mMutated = false;
452     }
453 
454     /**
455      * Called during inflation to inflate the child element. The last valid
456      * child element will take precedence over any other child elements or
457      * explicit drawable attribute.
458      */
inflateChildDrawable(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)459     private void inflateChildDrawable(@NonNull Resources r, @NonNull XmlPullParser parser,
460             @NonNull AttributeSet attrs, @Nullable Theme theme)
461             throws XmlPullParserException, IOException {
462         // Seek to the first child element.
463         Drawable dr = null;
464         int type;
465         final int outerDepth = parser.getDepth();
466         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
467                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
468             if (type == XmlPullParser.START_TAG) {
469                 dr = Drawable.createFromXmlInnerForDensity(r, parser, attrs,
470                         mState.mSrcDensityOverride, theme);
471             }
472         }
473 
474         if (dr != null) {
475             setDrawable(dr);
476         }
477     }
478 
479     abstract static class DrawableWrapperState extends Drawable.ConstantState {
480         private int[] mThemeAttrs;
481 
482         @Config int mChangingConfigurations;
483         int mDensity = DisplayMetrics.DENSITY_DEFAULT;
484 
485         /**
486          * The density to use when looking up resources from
487          * {@link Resources#getDrawableForDensity(int, int, Theme)}.
488          * A value of 0 means there is no override and the system density will be used.
489          * @hide
490          */
491         int mSrcDensityOverride = 0;
492 
493         Drawable.ConstantState mDrawableState;
494 
DrawableWrapperState(@ullable DrawableWrapperState orig, @Nullable Resources res)495         DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
496             if (orig != null) {
497                 mThemeAttrs = orig.mThemeAttrs;
498                 mChangingConfigurations = orig.mChangingConfigurations;
499                 mDrawableState = orig.mDrawableState;
500                 mSrcDensityOverride = orig.mSrcDensityOverride;
501             }
502 
503             final int density;
504             if (res != null) {
505                 density = res.getDisplayMetrics().densityDpi;
506             } else if (orig != null) {
507                 density = orig.mDensity;
508             } else {
509                 density = 0;
510             }
511 
512             mDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
513         }
514 
515         /**
516          * Sets the constant state density.
517          * <p>
518          * If the density has been previously set, dispatches the change to
519          * subclasses so that density-dependent properties may be scaled as
520          * necessary.
521          *
522          * @param targetDensity the new constant state density
523          */
setDensity(int targetDensity)524         public final void setDensity(int targetDensity) {
525             if (mDensity != targetDensity) {
526                 final int sourceDensity = mDensity;
527                 mDensity = targetDensity;
528 
529                 onDensityChanged(sourceDensity, targetDensity);
530             }
531         }
532 
533         /**
534          * Called when the constant state density changes.
535          * <p>
536          * Subclasses with density-dependent constant state properties should
537          * override this method and scale their properties as necessary.
538          *
539          * @param sourceDensity the previous constant state density
540          * @param targetDensity the new constant state density
541          */
onDensityChanged(int sourceDensity, int targetDensity)542         void onDensityChanged(int sourceDensity, int targetDensity) {
543             // Stub method.
544         }
545 
546         @Override
canApplyTheme()547         public boolean canApplyTheme() {
548             return mThemeAttrs != null
549                     || (mDrawableState != null && mDrawableState.canApplyTheme())
550                     || super.canApplyTheme();
551         }
552 
553         @Override
newDrawable()554         public Drawable newDrawable() {
555             return newDrawable(null);
556         }
557 
558         @Override
newDrawable(@ullable Resources res)559         public abstract Drawable newDrawable(@Nullable Resources res);
560 
561         @Override
getChangingConfigurations()562         public @Config int getChangingConfigurations() {
563             return mChangingConfigurations
564                     | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0);
565         }
566 
canConstantState()567         public boolean canConstantState() {
568             return mDrawableState != null;
569         }
570     }
571 }
572