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 androidx.core.graphics.drawable;
18
19import android.content.res.ColorStateList;
20import android.content.res.Resources;
21import android.graphics.Canvas;
22import android.graphics.ColorFilter;
23import android.graphics.PorterDuff;
24import android.graphics.Rect;
25import android.graphics.Region;
26import android.graphics.drawable.Drawable;
27
28import androidx.annotation.NonNull;
29import androidx.annotation.Nullable;
30import androidx.annotation.RequiresApi;
31
32/**
33 * Drawable which delegates all calls to its wrapped {@link Drawable}.
34 * <p/>
35 * Also allows backward compatible tinting via a color or {@link ColorStateList}.
36 * This functionality is accessed via static methods in {@code DrawableCompat}.
37 */
38class WrappedDrawableApi14 extends Drawable
39        implements Drawable.Callback, WrappedDrawable, TintAwareDrawable {
40
41    static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
42
43    private int mCurrentColor;
44    private PorterDuff.Mode mCurrentMode;
45    private boolean mColorFilterSet;
46
47    DrawableWrapperState mState;
48    private boolean mMutated;
49
50    Drawable mDrawable;
51
52    WrappedDrawableApi14(@NonNull DrawableWrapperState state, @Nullable Resources res) {
53        mState = state;
54        updateLocalState(res);
55    }
56
57    /**
58     * Creates a new wrapper around the specified drawable.
59     *
60     * @param dr the drawable to wrap
61     */
62    WrappedDrawableApi14(@Nullable Drawable dr) {
63        mState = mutateConstantState();
64        // Now set the drawable...
65        setWrappedDrawable(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(@Nullable Resources res) {
74        if (mState != null && mState.mDrawableState != null) {
75            setWrappedDrawable(mState.mDrawableState.newDrawable(res));
76        }
77    }
78
79    @Override
80    public void jumpToCurrentState() {
81        mDrawable.jumpToCurrentState();
82    }
83
84    @Override
85    public void draw(@NonNull Canvas canvas) {
86        mDrawable.draw(canvas);
87    }
88
89    @Override
90    protected void onBoundsChange(Rect bounds) {
91        if (mDrawable != null) {
92            mDrawable.setBounds(bounds);
93        }
94    }
95
96    @Override
97    public void setChangingConfigurations(int configs) {
98        mDrawable.setChangingConfigurations(configs);
99    }
100
101    @Override
102    public int getChangingConfigurations() {
103        return super.getChangingConfigurations()
104                | (mState != null ? mState.getChangingConfigurations() : 0)
105                | mDrawable.getChangingConfigurations();
106    }
107
108    @Override
109    public void setDither(boolean dither) {
110        mDrawable.setDither(dither);
111    }
112
113    @Override
114    public void setFilterBitmap(boolean filter) {
115        mDrawable.setFilterBitmap(filter);
116    }
117
118    @Override
119    public void setAlpha(int alpha) {
120        mDrawable.setAlpha(alpha);
121    }
122
123    @Override
124    public void setColorFilter(ColorFilter cf) {
125        mDrawable.setColorFilter(cf);
126    }
127
128    @Override
129    public boolean isStateful() {
130        final ColorStateList tintList = (isCompatTintEnabled() && mState != null)
131                ? mState.mTint
132                : null;
133        return (tintList != null && tintList.isStateful()) || mDrawable.isStateful();
134    }
135
136    @Override
137    public boolean setState(@NonNull int[] stateSet) {
138        boolean handled = mDrawable.setState(stateSet);
139        handled = updateTint(stateSet) || handled;
140        return handled;
141    }
142
143    @NonNull
144    @Override
145    public int[] getState() {
146        return mDrawable.getState();
147    }
148
149    @NonNull
150    @Override
151    public Drawable getCurrent() {
152        return mDrawable.getCurrent();
153    }
154
155    @Override
156    public boolean setVisible(boolean visible, boolean restart) {
157        return super.setVisible(visible, restart) || mDrawable.setVisible(visible, restart);
158    }
159
160    @Override
161    public int getOpacity() {
162        return mDrawable.getOpacity();
163    }
164
165    @Override
166    public Region getTransparentRegion() {
167        return mDrawable.getTransparentRegion();
168    }
169
170    @Override
171    public int getIntrinsicWidth() {
172        return mDrawable.getIntrinsicWidth();
173    }
174
175    @Override
176    public int getIntrinsicHeight() {
177        return mDrawable.getIntrinsicHeight();
178    }
179
180    @Override
181    public int getMinimumWidth() {
182        return mDrawable.getMinimumWidth();
183    }
184
185    @Override
186    public int getMinimumHeight() {
187        return mDrawable.getMinimumHeight();
188    }
189
190    @Override
191    public boolean getPadding(@NonNull Rect padding) {
192        return mDrawable.getPadding(padding);
193    }
194
195    @Override
196    @RequiresApi(19)
197    public void setAutoMirrored(boolean mirrored) {
198        mDrawable.setAutoMirrored(mirrored);
199    }
200
201    @Override
202    @RequiresApi(19)
203    public boolean isAutoMirrored() {
204        return mDrawable.isAutoMirrored();
205    }
206
207    @Override
208    @Nullable
209    public ConstantState getConstantState() {
210        if (mState != null && mState.canConstantState()) {
211            mState.mChangingConfigurations = getChangingConfigurations();
212            return mState;
213        }
214        return null;
215    }
216
217    @NonNull
218    @Override
219    public Drawable mutate() {
220        if (!mMutated && super.mutate() == this) {
221            mState = mutateConstantState();
222            if (mDrawable != null) {
223                mDrawable.mutate();
224            }
225            if (mState != null) {
226                mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null;
227            }
228            mMutated = true;
229        }
230        return this;
231    }
232
233    /**
234     * Mutates the constant state and returns the new state.
235     * <p>
236     * This method should never call the super implementation; it should always
237     * mutate and return its own constant state.
238     *
239     * @return the new state
240     */
241    @NonNull
242    DrawableWrapperState mutateConstantState() {
243        return new DrawableWrapperStateBase(mState, null);
244    }
245
246    /**
247     * {@inheritDoc}
248     */
249    @Override
250    public void invalidateDrawable(@NonNull Drawable who) {
251        invalidateSelf();
252    }
253
254    /**
255     * {@inheritDoc}
256     */
257    @Override
258    public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
259        scheduleSelf(what, when);
260    }
261
262    /**
263     * {@inheritDoc}
264     */
265    @Override
266    public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
267        unscheduleSelf(what);
268    }
269
270    @Override
271    protected boolean onLevelChange(int level) {
272        return mDrawable.setLevel(level);
273    }
274
275    @Override
276    public void setTint(int tint) {
277        setTintList(ColorStateList.valueOf(tint));
278    }
279
280    @Override
281    public void setTintList(ColorStateList tint) {
282        mState.mTint = tint;
283        updateTint(getState());
284    }
285
286    @Override
287    public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
288        mState.mTintMode = tintMode;
289        updateTint(getState());
290    }
291
292    private boolean updateTint(int[] state) {
293        if (!isCompatTintEnabled()) {
294            // If compat tinting is not enabled, fail fast
295            return false;
296        }
297
298        final ColorStateList tintList = mState.mTint;
299        final PorterDuff.Mode tintMode = mState.mTintMode;
300
301        if (tintList != null && tintMode != null) {
302            final int color = tintList.getColorForState(state, tintList.getDefaultColor());
303            if (!mColorFilterSet || color != mCurrentColor || tintMode != mCurrentMode) {
304                setColorFilter(color, tintMode);
305                mCurrentColor = color;
306                mCurrentMode = tintMode;
307                mColorFilterSet = true;
308                return true;
309            }
310        } else {
311            mColorFilterSet = false;
312            clearColorFilter();
313        }
314        return false;
315    }
316
317    /**
318     * Returns the wrapped {@link Drawable}
319     */
320    @Override
321    public final Drawable getWrappedDrawable() {
322        return mDrawable;
323    }
324
325    /**
326     * Sets the current wrapped {@link Drawable}
327     */
328    @Override
329    public final void setWrappedDrawable(Drawable dr) {
330        if (mDrawable != null) {
331            mDrawable.setCallback(null);
332        }
333
334        mDrawable = dr;
335
336        if (dr != null) {
337            dr.setCallback(this);
338            // Only call setters for data that's stored in the base Drawable.
339            setVisible(dr.isVisible(), true);
340            setState(dr.getState());
341            setLevel(dr.getLevel());
342            setBounds(dr.getBounds());
343            if (mState != null) {
344                mState.mDrawableState = dr.getConstantState();
345            }
346        }
347
348        invalidateSelf();
349    }
350
351    protected boolean isCompatTintEnabled() {
352        // It's enabled by default on Gingerbread
353        return true;
354    }
355
356    protected abstract static class DrawableWrapperState extends Drawable.ConstantState {
357        int mChangingConfigurations;
358        Drawable.ConstantState mDrawableState;
359
360        ColorStateList mTint = null;
361        PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;
362
363        DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
364            if (orig != null) {
365                mChangingConfigurations = orig.mChangingConfigurations;
366                mDrawableState = orig.mDrawableState;
367                mTint = orig.mTint;
368                mTintMode = orig.mTintMode;
369            }
370        }
371
372        @NonNull
373        @Override
374        public Drawable newDrawable() {
375            return newDrawable(null);
376        }
377
378        @NonNull
379        @Override
380        public abstract Drawable newDrawable(@Nullable Resources res);
381
382        @Override
383        public int getChangingConfigurations() {
384            return mChangingConfigurations
385                    | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0);
386        }
387
388        boolean canConstantState() {
389            return mDrawableState != null;
390        }
391    }
392
393    private static class DrawableWrapperStateBase extends DrawableWrapperState {
394        DrawableWrapperStateBase(
395                @Nullable DrawableWrapperState orig, @Nullable Resources res) {
396            super(orig, res);
397        }
398
399        @NonNull
400        @Override
401        public Drawable newDrawable(@Nullable Resources res) {
402            return new WrappedDrawableApi14(this, res);
403        }
404    }
405}
406