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