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