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