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 org.xmlpull.v1.XmlPullParser;
20 import org.xmlpull.v1.XmlPullParserException;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.res.ColorStateList;
25 import android.content.res.Resources;
26 import android.content.res.TypedArray;
27 import android.graphics.Bitmap;
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.PorterDuff;
34 import android.graphics.Rect;
35 import android.util.AttributeSet;
36 import android.view.View;
37 
38 import java.io.IOException;
39 import java.util.Collection;
40 
41 /**
42  * Drawable container with only one child element.
43  */
44 public abstract class DrawableWrapper extends Drawable implements Drawable.Callback {
45     private DrawableWrapperState mState;
46     private Drawable mDrawable;
47     private boolean mMutated;
48 
DrawableWrapper(DrawableWrapperState state, Resources res)49     DrawableWrapper(DrawableWrapperState state, Resources res) {
50         mState = state;
51 
52         updateLocalState(res);
53     }
54 
55     /**
56      * Creates a new wrapper around the specified drawable.
57      *
58      * @param dr the drawable to wrap
59      */
DrawableWrapper(@ullable Drawable dr)60     public DrawableWrapper(@Nullable Drawable dr) {
61         mState = null;
62         mDrawable = dr;
63     }
64 
65     /**
66      * Initializes local dynamic properties from state. This should be called
67      * after significant state changes, e.g. from the One True Constructor and
68      * after inflating or applying a theme.
69      */
updateLocalState(Resources res)70     private void updateLocalState(Resources res) {
71         if (mState != null && mState.mDrawableState != null) {
72             final Drawable dr = mState.mDrawableState.newDrawable(res);
73             setDrawable(dr);
74         }
75     }
76 
77     /**
78      * Sets the wrapped drawable.
79      *
80      * @param dr the wrapped drawable
81      */
setDrawable(@ullable Drawable dr)82     public void setDrawable(@Nullable Drawable dr) {
83         if (mDrawable != null) {
84             mDrawable.setCallback(null);
85         }
86 
87         mDrawable = dr;
88 
89         if (dr != null) {
90             dr.setCallback(this);
91 
92             // Only call setters for data that's stored in the base Drawable.
93             dr.setVisible(isVisible(), true);
94             dr.setState(getState());
95             dr.setLevel(getLevel());
96             dr.setBounds(getBounds());
97             dr.setLayoutDirection(getLayoutDirection());
98 
99             if (mState != null) {
100                 mState.mDrawableState = dr.getConstantState();
101             }
102         }
103 
104         invalidateSelf();
105     }
106 
107     /**
108      * @return the wrapped drawable
109      */
110     @Nullable
getDrawable()111     public Drawable getDrawable() {
112         return mDrawable;
113     }
114 
updateStateFromTypedArray(TypedArray a)115     void updateStateFromTypedArray(TypedArray a) {
116         final DrawableWrapperState state = mState;
117         if (state == null) {
118             return;
119         }
120 
121         // Account for any configuration changes.
122         state.mChangingConfigurations |= a.getChangingConfigurations();
123 
124         // Extract the theme attributes, if any.
125         state.mThemeAttrs = a.extractThemeAttrs();
126 
127         // TODO: Consider using R.styleable.DrawableWrapper_drawable
128     }
129 
130     @Override
applyTheme(Resources.Theme t)131     public void applyTheme(Resources.Theme t) {
132         super.applyTheme(t);
133 
134         final DrawableWrapperState state = mState;
135         if (state == null) {
136             return;
137         }
138 
139         if (mDrawable != null && mDrawable.canApplyTheme()) {
140             mDrawable.applyTheme(t);
141         }
142     }
143 
144     @Override
canApplyTheme()145     public boolean canApplyTheme() {
146         return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
147     }
148 
149     @Override
invalidateDrawable(Drawable who)150     public void invalidateDrawable(Drawable who) {
151         final Callback callback = getCallback();
152         if (callback != null) {
153             callback.invalidateDrawable(this);
154         }
155     }
156 
157     @Override
scheduleDrawable(Drawable who, Runnable what, long when)158     public void scheduleDrawable(Drawable who, Runnable what, long when) {
159         final Callback callback = getCallback();
160         if (callback != null) {
161             callback.scheduleDrawable(this, what, when);
162         }
163     }
164 
165     @Override
unscheduleDrawable(Drawable who, Runnable what)166     public void unscheduleDrawable(Drawable who, Runnable what) {
167         final Callback callback = getCallback();
168         if (callback != null) {
169             callback.unscheduleDrawable(this, what);
170         }
171     }
172 
173     @Override
draw(@onNull Canvas canvas)174     public void draw(@NonNull Canvas canvas) {
175         if (mDrawable != null) {
176             mDrawable.draw(canvas);
177         }
178     }
179 
180     @Override
getChangingConfigurations()181     public int getChangingConfigurations() {
182         return super.getChangingConfigurations()
183                 | (mState != null ? mState.getChangingConfigurations() : 0)
184                 | mDrawable.getChangingConfigurations();
185     }
186 
187     @Override
getPadding(@onNull Rect padding)188     public boolean getPadding(@NonNull Rect padding) {
189         return mDrawable != null && mDrawable.getPadding(padding);
190     }
191 
192     /** @hide */
193     @Override
getOpticalInsets()194     public Insets getOpticalInsets() {
195         return mDrawable != null ? mDrawable.getOpticalInsets() : Insets.NONE;
196     }
197 
198     @Override
setHotspot(float x, float y)199     public void setHotspot(float x, float y) {
200         if (mDrawable != null) {
201             mDrawable.setHotspot(x, y);
202         }
203     }
204 
205     @Override
setHotspotBounds(int left, int top, int right, int bottom)206     public void setHotspotBounds(int left, int top, int right, int bottom) {
207         if (mDrawable != null) {
208             mDrawable.setHotspotBounds(left, top, right, bottom);
209         }
210     }
211 
212     @Override
getHotspotBounds(@onNull Rect outRect)213     public void getHotspotBounds(@NonNull Rect outRect) {
214         if (mDrawable != null) {
215             mDrawable.getHotspotBounds(outRect);
216         } else {
217             outRect.set(getBounds());
218         }
219     }
220 
221     @Override
setVisible(boolean visible, boolean restart)222     public boolean setVisible(boolean visible, boolean restart) {
223         final boolean superChanged = super.setVisible(visible, restart);
224         final boolean changed = mDrawable != null && mDrawable.setVisible(visible, restart);
225         return superChanged | changed;
226     }
227 
228     @Override
setAlpha(int alpha)229     public void setAlpha(int alpha) {
230         if (mDrawable != null) {
231             mDrawable.setAlpha(alpha);
232         }
233     }
234 
235     @Override
getAlpha()236     public int getAlpha() {
237         return mDrawable != null ? mDrawable.getAlpha() : 255;
238     }
239 
240     @Override
setColorFilter(@ullable ColorFilter colorFilter)241     public void setColorFilter(@Nullable ColorFilter colorFilter) {
242         if (mDrawable != null) {
243             mDrawable.setColorFilter(colorFilter);
244         }
245     }
246 
247     @Override
setTintList(@ullable ColorStateList tint)248     public void setTintList(@Nullable ColorStateList tint) {
249         if (mDrawable != null) {
250             mDrawable.setTintList(tint);
251         }
252     }
253 
254     @Override
setTintMode(@ullable PorterDuff.Mode tintMode)255     public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
256         if (mDrawable != null) {
257             mDrawable.setTintMode(tintMode);
258         }
259     }
260 
261     @Override
onLayoutDirectionChanged(@iew.ResolvedLayoutDir int layoutDirection)262     public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
263         return mDrawable != null && mDrawable.setLayoutDirection(layoutDirection);
264     }
265 
266     @Override
getOpacity()267     public int getOpacity() {
268         return mDrawable != null ? mDrawable.getOpacity() : PixelFormat.TRANSPARENT;
269     }
270 
271     @Override
isStateful()272     public boolean isStateful() {
273         return mDrawable != null && mDrawable.isStateful();
274     }
275 
276     @Override
onStateChange(int[] state)277     protected boolean onStateChange(int[] state) {
278         if (mDrawable != null && mDrawable.isStateful()) {
279             final boolean changed = mDrawable.setState(state);
280             if (changed) {
281                 onBoundsChange(getBounds());
282             }
283             return changed;
284         }
285         return false;
286     }
287 
288     @Override
onLevelChange(int level)289     protected boolean onLevelChange(int level) {
290         return mDrawable != null && mDrawable.setLevel(level);
291     }
292 
293     @Override
onBoundsChange(@onNull Rect bounds)294     protected void onBoundsChange(@NonNull Rect bounds) {
295         if (mDrawable != null) {
296             mDrawable.setBounds(bounds);
297         }
298     }
299 
300     @Override
getIntrinsicWidth()301     public int getIntrinsicWidth() {
302         return mDrawable != null ? mDrawable.getIntrinsicWidth() : -1;
303     }
304 
305     @Override
getIntrinsicHeight()306     public int getIntrinsicHeight() {
307         return mDrawable != null ? mDrawable.getIntrinsicHeight() : -1;
308     }
309 
310     @Override
getOutline(@onNull Outline outline)311     public void getOutline(@NonNull Outline outline) {
312         if (mDrawable != null) {
313             mDrawable.getOutline(outline);
314         } else {
315             super.getOutline(outline);
316         }
317     }
318 
319     @Override
320     @Nullable
getConstantState()321     public ConstantState getConstantState() {
322         if (mState != null && mState.canConstantState()) {
323             mState.mChangingConfigurations = getChangingConfigurations();
324             return mState;
325         }
326         return null;
327     }
328 
329     @Override
330     @NonNull
mutate()331     public Drawable mutate() {
332         if (!mMutated && super.mutate() == this) {
333             mState = mutateConstantState();
334             if (mDrawable != null) {
335                 mDrawable.mutate();
336             }
337             if (mState != null) {
338                 mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null;
339             }
340             mMutated = true;
341         }
342         return this;
343     }
344 
345     /**
346      * Mutates the constant state and returns the new state. Responsible for
347      * updating any local copy.
348      * <p>
349      * This method should never call the super implementation; it should always
350      * mutate and return its own constant state.
351      *
352      * @return the new state
353      */
mutateConstantState()354     DrawableWrapperState mutateConstantState() {
355         return mState;
356     }
357 
358     /**
359      * @hide Only used by the framework for pre-loading resources.
360      */
clearMutated()361     public void clearMutated() {
362         super.clearMutated();
363         if (mDrawable != null) {
364             mDrawable.clearMutated();
365         }
366         mMutated = false;
367     }
368 
369     /**
370      * Called during inflation to inflate the child element. The last valid
371      * child element will take precedence over any other child elements or
372      * explicit drawable attribute.
373      */
inflateChildDrawable(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme)374     void inflateChildDrawable(Resources r, XmlPullParser parser, AttributeSet attrs,
375             Resources.Theme theme) throws XmlPullParserException, IOException {
376         // Seek to the first child element.
377         Drawable dr = null;
378         int type;
379         final int outerDepth = parser.getDepth();
380         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
381                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
382             if (type == XmlPullParser.START_TAG) {
383                 dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
384             }
385         }
386 
387         if (dr != null) {
388             setDrawable(dr);
389         }
390     }
391 
392     abstract static class DrawableWrapperState extends Drawable.ConstantState {
393         int[] mThemeAttrs;
394         int mChangingConfigurations;
395 
396         Drawable.ConstantState mDrawableState;
397 
DrawableWrapperState(DrawableWrapperState orig)398         DrawableWrapperState(DrawableWrapperState orig) {
399             if (orig != null) {
400                 mThemeAttrs = orig.mThemeAttrs;
401                 mChangingConfigurations = orig.mChangingConfigurations;
402                 mDrawableState = orig.mDrawableState;
403             }
404         }
405 
406         @Override
canApplyTheme()407         public boolean canApplyTheme() {
408             return mThemeAttrs != null
409                     || (mDrawableState != null && mDrawableState.canApplyTheme())
410                     || super.canApplyTheme();
411         }
412 
413         @Override
addAtlasableBitmaps(Collection<Bitmap> atlasList)414         public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
415             final Drawable.ConstantState state = mDrawableState;
416             if (state != null) {
417                 return state.addAtlasableBitmaps(atlasList);
418             }
419             return 0;
420         }
421 
422         @Override
newDrawable()423         public Drawable newDrawable() {
424             return newDrawable(null);
425         }
426 
427         @Override
newDrawable(Resources res)428         public abstract Drawable newDrawable(Resources res);
429 
430         @Override
getChangingConfigurations()431         public int getChangingConfigurations() {
432             return mChangingConfigurations
433                     | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0);
434         }
435 
canConstantState()436         public boolean canConstantState() {
437             return mDrawableState != null;
438         }
439     }
440 }
441