Home | History | Annotate | Download | only in drawable
      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 com.android.internal.R;
     20 
     21 import org.xmlpull.v1.XmlPullParser;
     22 import org.xmlpull.v1.XmlPullParserException;
     23 
     24 import android.annotation.NonNull;
     25 import android.annotation.Nullable;
     26 import android.content.pm.ActivityInfo.Config;
     27 import android.content.res.ColorStateList;
     28 import android.content.res.Resources;
     29 import android.content.res.Resources.Theme;
     30 import android.content.res.TypedArray;
     31 import android.graphics.Canvas;
     32 import android.graphics.ColorFilter;
     33 import android.graphics.Insets;
     34 import android.graphics.Outline;
     35 import android.graphics.PixelFormat;
     36 import android.graphics.PorterDuff;
     37 import android.graphics.Rect;
     38 import android.util.AttributeSet;
     39 import android.util.DisplayMetrics;
     40 import android.view.View;
     41 
     42 import java.io.IOException;
     43 
     44 /**
     45  * Drawable container with only one child element.
     46  */
     47 public 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 ColorFilter getColorFilter() {
    300         final Drawable drawable = getDrawable();
    301         if (drawable != null) {
    302             return drawable.getColorFilter();
    303         }
    304         return super.getColorFilter();
    305     }
    306 
    307     @Override
    308     public void setTintList(@Nullable ColorStateList tint) {
    309         if (mDrawable != null) {
    310             mDrawable.setTintList(tint);
    311         }
    312     }
    313 
    314     @Override
    315     public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
    316         if (mDrawable != null) {
    317             mDrawable.setTintMode(tintMode);
    318         }
    319     }
    320 
    321     @Override
    322     public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) {
    323         return mDrawable != null && mDrawable.setLayoutDirection(layoutDirection);
    324     }
    325 
    326     @Override
    327     public int getOpacity() {
    328         return mDrawable != null ? mDrawable.getOpacity() : PixelFormat.TRANSPARENT;
    329     }
    330 
    331     @Override
    332     public boolean isStateful() {
    333         return mDrawable != null && mDrawable.isStateful();
    334     }
    335 
    336     /** @hide */
    337     @Override
    338     public boolean hasFocusStateSpecified() {
    339         return mDrawable != null && mDrawable.hasFocusStateSpecified();
    340     }
    341 
    342     @Override
    343     protected boolean onStateChange(int[] state) {
    344         if (mDrawable != null && mDrawable.isStateful()) {
    345             final boolean changed = mDrawable.setState(state);
    346             if (changed) {
    347                 onBoundsChange(getBounds());
    348             }
    349             return changed;
    350         }
    351         return false;
    352     }
    353 
    354     @Override
    355     protected boolean onLevelChange(int level) {
    356         return mDrawable != null && mDrawable.setLevel(level);
    357     }
    358 
    359     @Override
    360     protected void onBoundsChange(@NonNull Rect bounds) {
    361         if (mDrawable != null) {
    362             mDrawable.setBounds(bounds);
    363         }
    364     }
    365 
    366     @Override
    367     public int getIntrinsicWidth() {
    368         return mDrawable != null ? mDrawable.getIntrinsicWidth() : -1;
    369     }
    370 
    371     @Override
    372     public int getIntrinsicHeight() {
    373         return mDrawable != null ? mDrawable.getIntrinsicHeight() : -1;
    374     }
    375 
    376     @Override
    377     public void getOutline(@NonNull Outline outline) {
    378         if (mDrawable != null) {
    379             mDrawable.getOutline(outline);
    380         } else {
    381             super.getOutline(outline);
    382         }
    383     }
    384 
    385     @Override
    386     @Nullable
    387     public ConstantState getConstantState() {
    388         if (mState != null && mState.canConstantState()) {
    389             mState.mChangingConfigurations = getChangingConfigurations();
    390             return mState;
    391         }
    392         return null;
    393     }
    394 
    395     @Override
    396     @NonNull
    397     public Drawable mutate() {
    398         if (!mMutated && super.mutate() == this) {
    399             mState = mutateConstantState();
    400             if (mDrawable != null) {
    401                 mDrawable.mutate();
    402             }
    403             if (mState != null) {
    404                 mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null;
    405             }
    406             mMutated = true;
    407         }
    408         return this;
    409     }
    410 
    411     /**
    412      * Mutates the constant state and returns the new state. Responsible for
    413      * updating any local copy.
    414      * <p>
    415      * This method should never call the super implementation; it should always
    416      * mutate and return its own constant state.
    417      *
    418      * @return the new state
    419      */
    420     DrawableWrapperState mutateConstantState() {
    421         return mState;
    422     }
    423 
    424     /**
    425      * @hide Only used by the framework for pre-loading resources.
    426      */
    427     public void clearMutated() {
    428         super.clearMutated();
    429         if (mDrawable != null) {
    430             mDrawable.clearMutated();
    431         }
    432         mMutated = false;
    433     }
    434 
    435     /**
    436      * Called during inflation to inflate the child element. The last valid
    437      * child element will take precedence over any other child elements or
    438      * explicit drawable attribute.
    439      */
    440     private void inflateChildDrawable(@NonNull Resources r, @NonNull XmlPullParser parser,
    441             @NonNull AttributeSet attrs, @Nullable Theme theme)
    442             throws XmlPullParserException, IOException {
    443         // Seek to the first child element.
    444         Drawable dr = null;
    445         int type;
    446         final int outerDepth = parser.getDepth();
    447         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    448                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
    449             if (type == XmlPullParser.START_TAG) {
    450                 dr = Drawable.createFromXmlInnerForDensity(r, parser, attrs,
    451                         mState.mSrcDensityOverride, theme);
    452             }
    453         }
    454 
    455         if (dr != null) {
    456             setDrawable(dr);
    457         }
    458     }
    459 
    460     abstract static class DrawableWrapperState extends Drawable.ConstantState {
    461         private int[] mThemeAttrs;
    462 
    463         @Config int mChangingConfigurations;
    464         int mDensity = DisplayMetrics.DENSITY_DEFAULT;
    465 
    466         /**
    467          * The density to use when looking up resources from
    468          * {@link Resources#getDrawableForDensity(int, int, Theme)}.
    469          * A value of 0 means there is no override and the system density will be used.
    470          * @hide
    471          */
    472         int mSrcDensityOverride = 0;
    473 
    474         Drawable.ConstantState mDrawableState;
    475 
    476         DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
    477             if (orig != null) {
    478                 mThemeAttrs = orig.mThemeAttrs;
    479                 mChangingConfigurations = orig.mChangingConfigurations;
    480                 mDrawableState = orig.mDrawableState;
    481                 mSrcDensityOverride = orig.mSrcDensityOverride;
    482             }
    483 
    484             final int density;
    485             if (res != null) {
    486                 density = res.getDisplayMetrics().densityDpi;
    487             } else if (orig != null) {
    488                 density = orig.mDensity;
    489             } else {
    490                 density = 0;
    491             }
    492 
    493             mDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
    494         }
    495 
    496         /**
    497          * Sets the constant state density.
    498          * <p>
    499          * If the density has been previously set, dispatches the change to
    500          * subclasses so that density-dependent properties may be scaled as
    501          * necessary.
    502          *
    503          * @param targetDensity the new constant state density
    504          */
    505         public final void setDensity(int targetDensity) {
    506             if (mDensity != targetDensity) {
    507                 final int sourceDensity = mDensity;
    508                 mDensity = targetDensity;
    509 
    510                 onDensityChanged(sourceDensity, targetDensity);
    511             }
    512         }
    513 
    514         /**
    515          * Called when the constant state density changes.
    516          * <p>
    517          * Subclasses with density-dependent constant state properties should
    518          * override this method and scale their properties as necessary.
    519          *
    520          * @param sourceDensity the previous constant state density
    521          * @param targetDensity the new constant state density
    522          */
    523         void onDensityChanged(int sourceDensity, int targetDensity) {
    524             // Stub method.
    525         }
    526 
    527         @Override
    528         public boolean canApplyTheme() {
    529             return mThemeAttrs != null
    530                     || (mDrawableState != null && mDrawableState.canApplyTheme())
    531                     || super.canApplyTheme();
    532         }
    533 
    534         @Override
    535         public Drawable newDrawable() {
    536             return newDrawable(null);
    537         }
    538 
    539         @Override
    540         public abstract Drawable newDrawable(@Nullable Resources res);
    541 
    542         @Override
    543         public @Config int getChangingConfigurations() {
    544             return mChangingConfigurations
    545                     | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0);
    546         }
    547 
    548         public boolean canConstantState() {
    549             return mDrawableState != null;
    550         }
    551     }
    552 }
    553