| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.graphics.drawable; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.pm.ActivityInfo.Config; |
| import android.content.res.ColorStateList; |
| import android.content.res.Resources; |
| import android.content.res.Resources.Theme; |
| import android.content.res.TypedArray; |
| import android.graphics.BlendMode; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.Insets; |
| import android.graphics.Outline; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.Xfermode; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.view.View; |
| |
| import com.android.internal.R; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| |
| /** |
| * Drawable container with only one child element. |
| */ |
| public abstract class DrawableWrapper extends Drawable implements Drawable.Callback { |
| @UnsupportedAppUsage |
| private DrawableWrapperState mState; |
| private Drawable mDrawable; |
| private boolean mMutated; |
| |
| DrawableWrapper(DrawableWrapperState state, Resources res) { |
| mState = state; |
| |
| updateLocalState(res); |
| } |
| |
| /** |
| * Creates a new wrapper around the specified drawable. |
| * |
| * @param dr the drawable to wrap |
| */ |
| public DrawableWrapper(@Nullable Drawable dr) { |
| mState = null; |
| setDrawable(dr); |
| } |
| |
| /** |
| * Initializes local dynamic properties from state. This should be called |
| * after significant state changes, e.g. from the One True Constructor and |
| * after inflating or applying a theme. |
| */ |
| private void updateLocalState(Resources res) { |
| if (mState != null && mState.mDrawableState != null) { |
| final Drawable dr = mState.mDrawableState.newDrawable(res); |
| setDrawable(dr); |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| @Override |
| public void setXfermode(Xfermode mode) { |
| if (mDrawable != null) { |
| mDrawable.setXfermode(mode); |
| } |
| } |
| |
| /** |
| * Sets the wrapped drawable. |
| * |
| * @param dr the wrapped drawable |
| */ |
| public void setDrawable(@Nullable Drawable dr) { |
| if (mDrawable != null) { |
| mDrawable.setCallback(null); |
| } |
| |
| mDrawable = dr; |
| |
| if (dr != null) { |
| dr.setCallback(this); |
| |
| // Only call setters for data that's stored in the base Drawable. |
| dr.setVisible(isVisible(), true); |
| dr.setState(getState()); |
| dr.setLevel(getLevel()); |
| dr.setBounds(getBounds()); |
| dr.setLayoutDirection(getLayoutDirection()); |
| |
| if (mState != null) { |
| mState.mDrawableState = dr.getConstantState(); |
| } |
| } |
| |
| invalidateSelf(); |
| } |
| |
| /** |
| * @return the wrapped drawable |
| */ |
| @Nullable |
| public Drawable getDrawable() { |
| return mDrawable; |
| } |
| |
| @Override |
| public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, |
| @NonNull AttributeSet attrs, @Nullable Theme theme) |
| throws XmlPullParserException, IOException { |
| super.inflate(r, parser, attrs, theme); |
| |
| final DrawableWrapperState state = mState; |
| if (state == null) { |
| return; |
| } |
| |
| // The density may have changed since the last update. This will |
| // apply scaling to any existing constant state properties. |
| final int densityDpi = r.getDisplayMetrics().densityDpi; |
| final int targetDensity = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; |
| state.setDensity(targetDensity); |
| state.mSrcDensityOverride = mSrcDensityOverride; |
| |
| final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.DrawableWrapper); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| |
| inflateChildDrawable(r, parser, attrs, theme); |
| } |
| |
| @Override |
| public void applyTheme(@NonNull Theme t) { |
| super.applyTheme(t); |
| |
| // If we load the drawable later as part of updating from the typed |
| // array, it will already be themed correctly. So, we can theme the |
| // local drawable first. |
| if (mDrawable != null && mDrawable.canApplyTheme()) { |
| mDrawable.applyTheme(t); |
| } |
| |
| final DrawableWrapperState state = mState; |
| if (state == null) { |
| return; |
| } |
| |
| final int densityDpi = t.getResources().getDisplayMetrics().densityDpi; |
| final int density = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; |
| state.setDensity(density); |
| |
| if (state.mThemeAttrs != null) { |
| final TypedArray a = t.resolveAttributes( |
| state.mThemeAttrs, R.styleable.DrawableWrapper); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| } |
| } |
| |
| /** |
| * Updates constant state properties from the provided typed array. |
| * <p> |
| * Implementing subclasses should call through to the super method first. |
| * |
| * @param a the typed array rom which properties should be read |
| */ |
| private void updateStateFromTypedArray(@NonNull TypedArray a) { |
| final DrawableWrapperState state = mState; |
| if (state == null) { |
| return; |
| } |
| |
| // Account for any configuration changes. |
| state.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| state.mThemeAttrs = a.extractThemeAttrs(); |
| |
| if (a.hasValueOrEmpty(R.styleable.DrawableWrapper_drawable)) { |
| setDrawable(a.getDrawable(R.styleable.DrawableWrapper_drawable)); |
| } |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); |
| } |
| |
| @Override |
| public void invalidateDrawable(@NonNull Drawable who) { |
| final Callback callback = getCallback(); |
| if (callback != null) { |
| callback.invalidateDrawable(this); |
| } |
| } |
| |
| @Override |
| public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { |
| final Callback callback = getCallback(); |
| if (callback != null) { |
| callback.scheduleDrawable(this, what, when); |
| } |
| } |
| |
| @Override |
| public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { |
| final Callback callback = getCallback(); |
| if (callback != null) { |
| callback.unscheduleDrawable(this, what); |
| } |
| } |
| |
| @Override |
| public void draw(@NonNull Canvas canvas) { |
| if (mDrawable != null) { |
| mDrawable.draw(canvas); |
| } |
| } |
| |
| @Override |
| public @Config int getChangingConfigurations() { |
| return super.getChangingConfigurations() |
| | (mState != null ? mState.getChangingConfigurations() : 0) |
| | mDrawable.getChangingConfigurations(); |
| } |
| |
| @Override |
| public boolean getPadding(@NonNull Rect padding) { |
| return mDrawable != null && mDrawable.getPadding(padding); |
| } |
| |
| @Override |
| public Insets getOpticalInsets() { |
| return mDrawable != null ? mDrawable.getOpticalInsets() : Insets.NONE; |
| } |
| |
| @Override |
| public void setHotspot(float x, float y) { |
| if (mDrawable != null) { |
| mDrawable.setHotspot(x, y); |
| } |
| } |
| |
| @Override |
| public void setHotspotBounds(int left, int top, int right, int bottom) { |
| if (mDrawable != null) { |
| mDrawable.setHotspotBounds(left, top, right, bottom); |
| } |
| } |
| |
| @Override |
| public void getHotspotBounds(@NonNull Rect outRect) { |
| if (mDrawable != null) { |
| mDrawable.getHotspotBounds(outRect); |
| } else { |
| outRect.set(getBounds()); |
| } |
| } |
| |
| @Override |
| public boolean setVisible(boolean visible, boolean restart) { |
| final boolean superChanged = super.setVisible(visible, restart); |
| final boolean changed = mDrawable != null && mDrawable.setVisible(visible, restart); |
| return superChanged | changed; |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| if (mDrawable != null) { |
| mDrawable.setAlpha(alpha); |
| } |
| } |
| |
| @Override |
| public int getAlpha() { |
| return mDrawable != null ? mDrawable.getAlpha() : 255; |
| } |
| |
| @Override |
| public void setColorFilter(@Nullable ColorFilter colorFilter) { |
| if (mDrawable != null) { |
| mDrawable.setColorFilter(colorFilter); |
| } |
| } |
| |
| @Override |
| public ColorFilter getColorFilter() { |
| final Drawable drawable = getDrawable(); |
| if (drawable != null) { |
| return drawable.getColorFilter(); |
| } |
| return super.getColorFilter(); |
| } |
| |
| @Override |
| public void setTintList(@Nullable ColorStateList tint) { |
| if (mDrawable != null) { |
| mDrawable.setTintList(tint); |
| } |
| } |
| |
| @Override |
| public void setTintBlendMode(@NonNull BlendMode blendMode) { |
| if (mDrawable != null) { |
| mDrawable.setTintBlendMode(blendMode); |
| } |
| } |
| |
| @Override |
| public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) { |
| return mDrawable != null && mDrawable.setLayoutDirection(layoutDirection); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return mDrawable != null ? mDrawable.getOpacity() : PixelFormat.TRANSPARENT; |
| } |
| |
| @Override |
| public boolean isStateful() { |
| return mDrawable != null && mDrawable.isStateful(); |
| } |
| |
| /** @hide */ |
| @Override |
| public boolean hasFocusStateSpecified() { |
| return mDrawable != null && mDrawable.hasFocusStateSpecified(); |
| } |
| |
| @Override |
| protected boolean onStateChange(int[] state) { |
| if (mDrawable != null && mDrawable.isStateful()) { |
| final boolean changed = mDrawable.setState(state); |
| if (changed) { |
| onBoundsChange(getBounds()); |
| } |
| return changed; |
| } |
| return false; |
| } |
| |
| @Override |
| public void jumpToCurrentState() { |
| if (mDrawable != null) { |
| mDrawable.jumpToCurrentState(); |
| } |
| } |
| |
| @Override |
| protected boolean onLevelChange(int level) { |
| return mDrawable != null && mDrawable.setLevel(level); |
| } |
| |
| @Override |
| protected void onBoundsChange(@NonNull Rect bounds) { |
| if (mDrawable != null) { |
| mDrawable.setBounds(bounds); |
| } |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return mDrawable != null ? mDrawable.getIntrinsicWidth() : -1; |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return mDrawable != null ? mDrawable.getIntrinsicHeight() : -1; |
| } |
| |
| @Override |
| public void getOutline(@NonNull Outline outline) { |
| if (mDrawable != null) { |
| mDrawable.getOutline(outline); |
| } else { |
| super.getOutline(outline); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public ConstantState getConstantState() { |
| if (mState != null && mState.canConstantState()) { |
| mState.mChangingConfigurations = getChangingConfigurations(); |
| return mState; |
| } |
| return null; |
| } |
| |
| @Override |
| @NonNull |
| public Drawable mutate() { |
| if (!mMutated && super.mutate() == this) { |
| mState = mutateConstantState(); |
| if (mDrawable != null) { |
| mDrawable.mutate(); |
| } |
| if (mState != null) { |
| mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null; |
| } |
| mMutated = true; |
| } |
| return this; |
| } |
| |
| /** |
| * Mutates the constant state and returns the new state. Responsible for |
| * updating any local copy. |
| * <p> |
| * This method should never call the super implementation; it should always |
| * mutate and return its own constant state. |
| * |
| * @return the new state |
| */ |
| DrawableWrapperState mutateConstantState() { |
| return mState; |
| } |
| |
| /** |
| * @hide Only used by the framework for pre-loading resources. |
| */ |
| public void clearMutated() { |
| super.clearMutated(); |
| if (mDrawable != null) { |
| mDrawable.clearMutated(); |
| } |
| mMutated = false; |
| } |
| |
| /** |
| * Called during inflation to inflate the child element. The last valid |
| * child element will take precedence over any other child elements or |
| * explicit drawable attribute. |
| */ |
| private void inflateChildDrawable(@NonNull Resources r, @NonNull XmlPullParser parser, |
| @NonNull AttributeSet attrs, @Nullable Theme theme) |
| throws XmlPullParserException, IOException { |
| // Seek to the first child element. |
| Drawable dr = null; |
| int type; |
| final int outerDepth = parser.getDepth(); |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { |
| if (type == XmlPullParser.START_TAG) { |
| dr = Drawable.createFromXmlInnerForDensity(r, parser, attrs, |
| mState.mSrcDensityOverride, theme); |
| } |
| } |
| |
| if (dr != null) { |
| setDrawable(dr); |
| } |
| } |
| |
| abstract static class DrawableWrapperState extends Drawable.ConstantState { |
| private int[] mThemeAttrs; |
| |
| @Config int mChangingConfigurations; |
| int mDensity = DisplayMetrics.DENSITY_DEFAULT; |
| |
| /** |
| * The density to use when looking up resources from |
| * {@link Resources#getDrawableForDensity(int, int, Theme)}. |
| * A value of 0 means there is no override and the system density will be used. |
| * @hide |
| */ |
| int mSrcDensityOverride = 0; |
| |
| Drawable.ConstantState mDrawableState; |
| |
| DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) { |
| if (orig != null) { |
| mThemeAttrs = orig.mThemeAttrs; |
| mChangingConfigurations = orig.mChangingConfigurations; |
| mDrawableState = orig.mDrawableState; |
| mSrcDensityOverride = orig.mSrcDensityOverride; |
| } |
| |
| final int density; |
| if (res != null) { |
| density = res.getDisplayMetrics().densityDpi; |
| } else if (orig != null) { |
| density = orig.mDensity; |
| } else { |
| density = 0; |
| } |
| |
| mDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; |
| } |
| |
| /** |
| * Sets the constant state density. |
| * <p> |
| * If the density has been previously set, dispatches the change to |
| * subclasses so that density-dependent properties may be scaled as |
| * necessary. |
| * |
| * @param targetDensity the new constant state density |
| */ |
| public final void setDensity(int targetDensity) { |
| if (mDensity != targetDensity) { |
| final int sourceDensity = mDensity; |
| mDensity = targetDensity; |
| |
| onDensityChanged(sourceDensity, targetDensity); |
| } |
| } |
| |
| /** |
| * Called when the constant state density changes. |
| * <p> |
| * Subclasses with density-dependent constant state properties should |
| * override this method and scale their properties as necessary. |
| * |
| * @param sourceDensity the previous constant state density |
| * @param targetDensity the new constant state density |
| */ |
| void onDensityChanged(int sourceDensity, int targetDensity) { |
| // Stub method. |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return mThemeAttrs != null |
| || (mDrawableState != null && mDrawableState.canApplyTheme()) |
| || super.canApplyTheme(); |
| } |
| |
| @Override |
| public Drawable newDrawable() { |
| return newDrawable(null); |
| } |
| |
| @Override |
| public abstract Drawable newDrawable(@Nullable Resources res); |
| |
| @Override |
| public @Config int getChangingConfigurations() { |
| return mChangingConfigurations |
| | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0); |
| } |
| |
| public boolean canConstantState() { |
| return mDrawableState != null; |
| } |
| } |
| } |