Files
frameworks_base/graphics/java/android/graphics/drawable/DrawableWrapper.java
Alan Viverette 7e3ede2889 Support for changing density of DrawableWrapper subclasses
Includes a refactoring of DrawableWrapper classes so that the wrapper
super class handles both drawable management and inflation. This allows
us to immediately call through to super() in inflate and applyTheme,
which simplifies density management.

Bug: 25081461
Change-Id: I8c157d340fd1f28a3a2b786c56850a67cdd452e4
2015-10-28 16:57:57 -04:00

537 lines
16 KiB
Java

/*
* 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 com.android.internal.R;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Insets;
import android.graphics.Outline;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import java.io.IOException;
import java.util.Collection;
/**
* Drawable container with only one child element.
*/
public abstract class DrawableWrapper extends Drawable implements Drawable.Callback {
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;
mDrawable = 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);
}
}
/**
* 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);
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(Drawable who) {
final Callback callback = getCallback();
if (callback != null) {
callback.invalidateDrawable(this);
}
}
@Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
final Callback callback = getCallback();
if (callback != null) {
callback.scheduleDrawable(this, what, when);
}
}
@Override
public void unscheduleDrawable(Drawable who, 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 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);
}
/** @hide */
@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 void setTintList(@Nullable ColorStateList tint) {
if (mDrawable != null) {
mDrawable.setTintList(tint);
}
}
@Override
public void setTintMode(@Nullable PorterDuff.Mode tintMode) {
if (mDrawable != null) {
mDrawable.setTintMode(tintMode);
}
}
@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();
}
@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
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.createFromXmlInner(r, parser, attrs, theme);
}
}
if (dr != null) {
setDrawable(dr);
}
}
abstract static class DrawableWrapperState extends Drawable.ConstantState {
private int[] mThemeAttrs;
int mChangingConfigurations;
int mDensity = DisplayMetrics.DENSITY_DEFAULT;
Drawable.ConstantState mDrawableState;
DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
if (orig != null) {
mThemeAttrs = orig.mThemeAttrs;
mChangingConfigurations = orig.mChangingConfigurations;
mDrawableState = orig.mDrawableState;
}
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 int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
final Drawable.ConstantState state = mDrawableState;
if (state != null) {
return state.addAtlasableBitmaps(atlasList);
}
return 0;
}
@Override
public Drawable newDrawable() {
return newDrawable(null);
}
@Override
public abstract Drawable newDrawable(@Nullable Resources res);
@Override
public int getChangingConfigurations() {
return mChangingConfigurations
| (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0);
}
public boolean canConstantState() {
return mDrawableState != null;
}
}
}