blob: 51c183f582bed53f37a7a11fd323ed0077f04deb [file] [log] [blame]
/*
* Copyright (C) 2006 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 org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import java.io.IOException;
/**
* A Drawable that manages an array of other Drawables. These are drawn in array
* order, so the element with the largest index will be drawn on top.
* <p>
* It can be defined in an XML file with the <code>&lt;layer-list></code> element.
* Each Drawable in the layer is defined in a nested <code>&lt;item></code>. For more
* information, see the guide to <a
* href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
*
* @attr ref android.R.styleable#LayerDrawableItem_left
* @attr ref android.R.styleable#LayerDrawableItem_top
* @attr ref android.R.styleable#LayerDrawableItem_right
* @attr ref android.R.styleable#LayerDrawableItem_bottom
* @attr ref android.R.styleable#LayerDrawableItem_drawable
* @attr ref android.R.styleable#LayerDrawableItem_id
*/
public class LayerDrawable extends Drawable implements Drawable.Callback {
/**
* Padding mode used to nest each layer inside the padding of the previous
* layer.
*/
public static final int PADDING_MODE_NEST = 0;
/** Padding mode used to stack each layer directly atop the previous layer. */
public static final int PADDING_MODE_STACK = 1;
LayerState mLayerState;
private int mOpacityOverride = PixelFormat.UNKNOWN;
private int[] mPaddingL;
private int[] mPaddingT;
private int[] mPaddingR;
private int[] mPaddingB;
private final Rect mCachedPadding = new Rect();
private final Rect mTmpRect = new Rect();
private boolean mMutated;
private boolean mHasCachedPadding;
/**
* Create a new layer drawable with the list of specified layers.
*
* @param layers A list of drawables to use as layers in this new drawable.
*/
public LayerDrawable(Drawable[] layers) {
this(layers, null);
}
/**
* Create a new layer drawable with the specified list of layers and the specified
* constant state.
*
* @param layers The list of layers to add to this drawable.
* @param state The constant drawable state.
*/
LayerDrawable(Drawable[] layers, LayerState state) {
this(state, null);
int length = layers.length;
ChildDrawable[] r = new ChildDrawable[length];
for (int i = 0; i < length; i++) {
r[i] = new ChildDrawable();
r[i].mDrawable = layers[i];
layers[i].setCallback(this);
mLayerState.mChildrenChangingConfigurations |= layers[i].getChangingConfigurations();
}
mLayerState.mNum = length;
mLayerState.mChildren = r;
ensurePadding();
}
LayerDrawable() {
this((LayerState) null, null);
}
LayerDrawable(LayerState state, Resources res) {
LayerState as = createConstantState(state, res);
mLayerState = as;
if (as.mNum > 0) {
ensurePadding();
}
}
LayerState createConstantState(LayerState state, Resources res) {
return new LayerState(state, this, res);
}
@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
throws XmlPullParserException, IOException {
super.inflate(r, parser, attrs);
int type;
TypedArray a = r.obtainAttributes(attrs, com.android.internal.R.styleable.LayerDrawable);
mOpacityOverride = a.getInt(com.android.internal.R.styleable.LayerDrawable_opacity,
PixelFormat.UNKNOWN);
setAutoMirrored(a.getBoolean(com.android.internal.R.styleable.LayerDrawable_autoMirrored,
false));
mLayerState.mPaddingMode = a.getInteger(
com.android.internal.R.styleable.LayerDrawableItem_drawable, PADDING_MODE_NEST);
a.recycle();
final int innerDepth = parser.getDepth() + 1;
int depth;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth || !parser.getName().equals("item")) {
continue;
}
a = r.obtainAttributes(attrs,
com.android.internal.R.styleable.LayerDrawableItem);
int left = a.getDimensionPixelOffset(
com.android.internal.R.styleable.LayerDrawableItem_left, 0);
int top = a.getDimensionPixelOffset(
com.android.internal.R.styleable.LayerDrawableItem_top, 0);
int right = a.getDimensionPixelOffset(
com.android.internal.R.styleable.LayerDrawableItem_right, 0);
int bottom = a.getDimensionPixelOffset(
com.android.internal.R.styleable.LayerDrawableItem_bottom, 0);
int drawableRes = a.getResourceId(
com.android.internal.R.styleable.LayerDrawableItem_drawable, 0);
int id = a.getResourceId(com.android.internal.R.styleable.LayerDrawableItem_id,
View.NO_ID);
a.recycle();
Drawable dr;
if (drawableRes != 0) {
dr = r.getDrawable(drawableRes);
} else {
while ((type = parser.next()) == XmlPullParser.TEXT) {
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException(parser.getPositionDescription()
+ ": <item> tag requires a 'drawable' attribute or "
+ "child tag defining a drawable");
}
dr = Drawable.createFromXmlInner(r, parser, attrs);
}
addLayer(dr, id, left, top, right, bottom);
}
ensurePadding();
onStateChange(getState());
}
/**
* Add a new layer to this drawable. The new layer is identified by an id.
*
* @param layer The drawable to add as a layer.
* @param id The id of the new layer.
* @param left The left padding of the new layer.
* @param top The top padding of the new layer.
* @param right The right padding of the new layer.
* @param bottom The bottom padding of the new layer.
*/
private void addLayer(Drawable layer, int id, int left, int top, int right, int bottom) {
final LayerState st = mLayerState;
final int N = st.mChildren != null ? st.mChildren.length : 0;
final int i = st.mNum;
if (i >= N) {
final ChildDrawable[] nu = new ChildDrawable[N + 10];
if (i > 0) {
System.arraycopy(st.mChildren, 0, nu, 0, i);
}
st.mChildren = nu;
}
mLayerState.mChildrenChangingConfigurations |= layer.getChangingConfigurations();
final ChildDrawable childDrawable = new ChildDrawable();
st.mChildren[i] = childDrawable;
childDrawable.mId = id;
childDrawable.mDrawable = layer;
childDrawable.mDrawable.setAutoMirrored(isAutoMirrored());
childDrawable.mInsetL = left;
childDrawable.mInsetT = top;
childDrawable.mInsetR = right;
childDrawable.mInsetB = bottom;
st.mNum++;
layer.setCallback(this);
}
/**
* Look for a layer with the given id, and returns its {@link Drawable}.
*
* @param id The layer ID to search for.
* @return The {@link Drawable} of the layer that has the given id in the hierarchy or null.
*/
public Drawable findDrawableByLayerId(int id) {
final ChildDrawable[] layers = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
if (layers[i].mId == id) {
return layers[i].mDrawable;
}
}
return null;
}
/**
* Sets the ID of a layer.
*
* @param index The index of the layer which will received the ID.
* @param id The ID to assign to the layer.
*/
public void setId(int index, int id) {
mLayerState.mChildren[index].mId = id;
}
/**
* Returns the number of layers contained within this.
* @return The number of layers.
*/
public int getNumberOfLayers() {
return mLayerState.mNum;
}
/**
* Returns the drawable at the specified layer index.
*
* @param index The layer index of the drawable to retrieve.
*
* @return The {@link android.graphics.drawable.Drawable} at the specified layer index.
*/
public Drawable getDrawable(int index) {
return mLayerState.mChildren[index].mDrawable;
}
/**
* Returns the id of the specified layer.
*
* @param index The index of the layer.
*
* @return The id of the layer or {@link android.view.View#NO_ID} if the layer has no id.
*/
public int getId(int index) {
return mLayerState.mChildren[index].mId;
}
/**
* Sets (or replaces) the {@link Drawable} for the layer with the given id.
*
* @param id The layer ID to search for.
* @param drawable The replacement {@link Drawable}.
* @return Whether the {@link Drawable} was replaced (could return false if
* the id was not found).
*/
public boolean setDrawableByLayerId(int id, Drawable drawable) {
final ChildDrawable[] layers = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
final ChildDrawable childDrawable = layers[i];
if (childDrawable.mId == id) {
if (childDrawable.mDrawable != null) {
if (drawable != null) {
final Rect bounds = childDrawable.mDrawable.getBounds();
drawable.setBounds(bounds);
}
childDrawable.mDrawable.setCallback(null);
}
if (drawable != null) {
drawable.setCallback(this);
}
childDrawable.mDrawable = drawable;
if (refreshChildPadding(i, childDrawable)) {
invalidatePadding();
}
return true;
}
}
return false;
}
/**
* Specifies the insets in pixels for the drawable at the specified index.
* Insets are used to adjust the drawable bounds.
*
* @param index the index of the drawable to adjust
* @param l number of pixels to add to the left bound
* @param t number of pixels to add to the top bound
* @param r number of pixels to subtract from the right bound
* @param b number of pixels to subtract from the bottom bound
*/
public void setLayerInset(int index, int l, int t, int r, int b) {
final ChildDrawable childDrawable = mLayerState.mChildren[index];
childDrawable.mInsetL = l;
childDrawable.mInsetT = t;
childDrawable.mInsetR = r;
childDrawable.mInsetB = b;
invalidatePadding();
}
/**
* Specifies how layer padding should affect the bounds of subsequent
* layers. The default value is {@link #PADDING_MODE_NEST}.
*
* @param mode padding mode, one of:
* <ul>
* <li>{@link #PADDING_MODE_NEST}
* <li>{@link #PADDING_MODE_STACK}
* </ul>
*/
public void setPaddingMode(int mode) {
if (mLayerState.mPaddingMode != mode) {
mLayerState.mPaddingMode = mode;
invalidatePadding();
}
}
/**
* @return the current padding mode
* @see #setPaddingMode(int)
*/
public int getPaddingMode() {
return mLayerState.mPaddingMode;
}
/**
* Invalidates cached padding.
*/
private void invalidatePadding() {
mHasCachedPadding = false;
onBoundsChange(getBounds());
}
@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(Canvas canvas) {
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
array[i].mDrawable.draw(canvas);
}
}
@Override
public int getChangingConfigurations() {
return super.getChangingConfigurations()
| mLayerState.mChangingConfigurations
| mLayerState.mChildrenChangingConfigurations;
}
@Override
public boolean getPadding(Rect padding) {
final Rect cachedPadding = mCachedPadding;
if (!mHasCachedPadding) {
if (mLayerState.mPaddingMode == PADDING_MODE_NEST) {
computeNestedPadding(cachedPadding);
} else {
computeStackedPadding(cachedPadding);
}
mHasCachedPadding = true;
}
padding.set(cachedPadding);
return padding.left != 0 || padding.top != 0 || padding.right != 0 || padding.bottom != 0;
}
private void computeNestedPadding(Rect padding) {
padding.left = 0;
padding.top = 0;
padding.right = 0;
padding.bottom = 0;
// Add all the padding.
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
refreshChildPadding(i, array[i]);
padding.left += mPaddingL[i];
padding.top += mPaddingT[i];
padding.right += mPaddingR[i];
padding.bottom += mPaddingB[i];
}
}
private void computeStackedPadding(Rect padding) {
padding.left = 0;
padding.top = 0;
padding.right = 0;
padding.bottom = 0;
// Take the max padding.
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
refreshChildPadding(i, array[i]);
padding.left = Math.max(padding.left, mPaddingL[i]);
padding.top = Math.max(padding.top, mPaddingT[i]);
padding.right = Math.max(padding.right, mPaddingR[i]);
padding.bottom = Math.max(padding.bottom, mPaddingB[i]);
}
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
final boolean changed = super.setVisible(visible, restart);
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
array[i].mDrawable.setVisible(visible, restart);
}
return changed;
}
@Override
public void setDither(boolean dither) {
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
array[i].mDrawable.setDither(dither);
}
}
@Override
public void setAlpha(int alpha) {
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
array[i].mDrawable.setAlpha(alpha);
}
}
@Override
public int getAlpha() {
final ChildDrawable[] array = mLayerState.mChildren;
if (mLayerState.mNum > 0) {
// All layers should have the same alpha set on them - just return the first one
return array[0].mDrawable.getAlpha();
} else {
return super.getAlpha();
}
}
@Override
public void setColorFilter(ColorFilter cf) {
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
array[i].mDrawable.setColorFilter(cf);
}
}
/**
* Sets the opacity of this drawable directly, instead of collecting the states from
* the layers
*
* @param opacity The opacity to use, or {@link PixelFormat#UNKNOWN PixelFormat.UNKNOWN}
* for the default behavior
*
* @see PixelFormat#UNKNOWN
* @see PixelFormat#TRANSLUCENT
* @see PixelFormat#TRANSPARENT
* @see PixelFormat#OPAQUE
*/
public void setOpacity(int opacity) {
mOpacityOverride = opacity;
}
@Override
public int getOpacity() {
if (mOpacityOverride != PixelFormat.UNKNOWN) {
return mOpacityOverride;
}
return mLayerState.getOpacity();
}
@Override
public void setAutoMirrored(boolean mirrored) {
mLayerState.mAutoMirrored = mirrored;
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
array[i].mDrawable.setAutoMirrored(mirrored);
}
}
@Override
public boolean isAutoMirrored() {
return mLayerState.mAutoMirrored;
}
@Override
public boolean isStateful() {
return mLayerState.isStateful();
}
@Override
protected boolean onStateChange(int[] state) {
boolean paddingChanged = false;
boolean changed = false;
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
final ChildDrawable r = array[i];
if (r.mDrawable.setState(state)) {
changed = true;
}
if (refreshChildPadding(i, r)) {
paddingChanged = true;
}
}
if (paddingChanged) {
invalidatePadding();
}
return changed;
}
@Override
protected boolean onLevelChange(int level) {
boolean paddingChanged = false;
boolean changed = false;
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
final ChildDrawable r = array[i];
if (r.mDrawable.setLevel(level)) {
changed = true;
}
if (refreshChildPadding(i, r)) {
paddingChanged = true;
}
}
if (paddingChanged) {
invalidatePadding();
}
return changed;
}
@Override
protected void onBoundsChange(Rect bounds) {
int padL = 0;
int padT = 0;
int padR = 0;
int padB = 0;
final boolean nest = mLayerState.mPaddingMode == PADDING_MODE_NEST;
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
final ChildDrawable r = array[i];
r.mDrawable.setBounds(bounds.left + r.mInsetL + padL, bounds.top + r.mInsetT + padT,
bounds.right - r.mInsetR - padR, bounds.bottom - r.mInsetB - padB);
if (nest) {
padL += mPaddingL[i];
padR += mPaddingR[i];
padT += mPaddingT[i];
padB += mPaddingB[i];
}
}
}
@Override
public int getIntrinsicWidth() {
int width = -1;
int padL = 0;
int padR = 0;
final boolean nest = mLayerState.mPaddingMode == PADDING_MODE_NEST;
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
final ChildDrawable r = array[i];
final int w = r.mDrawable.getIntrinsicWidth() + r.mInsetL + r.mInsetR + padL + padR;
if (w > width) {
width = w;
}
if (nest) {
padL += mPaddingL[i];
padR += mPaddingR[i];
}
}
return width;
}
@Override
public int getIntrinsicHeight() {
int height = -1;
int padT = 0;
int padB = 0;
final boolean nest = mLayerState.mPaddingMode == PADDING_MODE_NEST;
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
final ChildDrawable r = array[i];
int h = r.mDrawable.getIntrinsicHeight() + r.mInsetT + r.mInsetB + padT + padB;
if (h > height) {
height = h;
}
if (nest) {
padT += mPaddingT[i];
padB += mPaddingB[i];
}
}
return height;
}
/**
* Refreshes the cached padding values for the specified child.
*
* @return true if the child's padding has changed
*/
private boolean refreshChildPadding(int i, ChildDrawable r) {
final Rect rect = mTmpRect;
r.mDrawable.getPadding(rect);
if (rect.left != mPaddingL[i] || rect.top != mPaddingT[i] ||
rect.right != mPaddingR[i] || rect.bottom != mPaddingB[i]) {
mPaddingL[i] = rect.left;
mPaddingT[i] = rect.top;
mPaddingR[i] = rect.right;
mPaddingB[i] = rect.bottom;
return true;
}
return false;
}
/**
* Ensures the child padding caches are large enough.
*/
private void ensurePadding() {
final int N = mLayerState.mNum;
if (mPaddingL != null && mPaddingL.length >= N) {
return;
}
mPaddingL = new int[N];
mPaddingT = new int[N];
mPaddingR = new int[N];
mPaddingB = new int[N];
}
@Override
public ConstantState getConstantState() {
if (mLayerState.canConstantState()) {
mLayerState.mChangingConfigurations = getChangingConfigurations();
return mLayerState;
}
return null;
}
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
mLayerState = createConstantState(mLayerState, null);
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
array[i].mDrawable.mutate();
}
mMutated = true;
}
return this;
}
/** @hide */
@Override
public void setLayoutDirection(int layoutDirection) {
final ChildDrawable[] array = mLayerState.mChildren;
final int N = mLayerState.mNum;
for (int i = 0; i < N; i++) {
array[i].mDrawable.setLayoutDirection(layoutDirection);
}
super.setLayoutDirection(layoutDirection);
}
static class ChildDrawable {
public Drawable mDrawable;
public int mInsetL, mInsetT, mInsetR, mInsetB;
public int mId;
}
static class LayerState extends ConstantState {
int mNum;
ChildDrawable[] mChildren;
int mChangingConfigurations;
int mChildrenChangingConfigurations;
private boolean mHaveOpacity = false;
private int mOpacity;
private boolean mHaveStateful = false;
private boolean mStateful;
private boolean mCheckedConstantState;
private boolean mCanConstantState;
private boolean mAutoMirrored;
private int mPaddingMode = PADDING_MODE_NEST;
LayerState(LayerState orig, LayerDrawable owner, Resources res) {
if (orig != null) {
final ChildDrawable[] origChildDrawable = orig.mChildren;
final int N = orig.mNum;
mNum = N;
mChildren = new ChildDrawable[N];
mChangingConfigurations = orig.mChangingConfigurations;
mChildrenChangingConfigurations = orig.mChildrenChangingConfigurations;
for (int i = 0; i < N; i++) {
final ChildDrawable r = mChildren[i] = new ChildDrawable();
final ChildDrawable or = origChildDrawable[i];
if (res != null) {
r.mDrawable = or.mDrawable.getConstantState().newDrawable(res);
} else {
r.mDrawable = or.mDrawable.getConstantState().newDrawable();
}
r.mDrawable.setCallback(owner);
r.mDrawable.setLayoutDirection(or.mDrawable.getLayoutDirection());
r.mInsetL = or.mInsetL;
r.mInsetT = or.mInsetT;
r.mInsetR = or.mInsetR;
r.mInsetB = or.mInsetB;
r.mId = or.mId;
}
mHaveOpacity = orig.mHaveOpacity;
mOpacity = orig.mOpacity;
mHaveStateful = orig.mHaveStateful;
mStateful = orig.mStateful;
mCheckedConstantState = mCanConstantState = true;
mAutoMirrored = orig.mAutoMirrored;
mPaddingMode = orig.mPaddingMode;
} else {
mNum = 0;
mChildren = null;
}
}
@Override
public Drawable newDrawable() {
return new LayerDrawable(this, null);
}
@Override
public Drawable newDrawable(Resources res) {
return new LayerDrawable(this, res);
}
@Override
public int getChangingConfigurations() {
return mChangingConfigurations;
}
public final int getOpacity() {
if (mHaveOpacity) {
return mOpacity;
}
final int N = mNum;
int op = N > 0 ? mChildren[0].mDrawable.getOpacity() : PixelFormat.TRANSPARENT;
for (int i = 1; i < N; i++) {
op = Drawable.resolveOpacity(op, mChildren[i].mDrawable.getOpacity());
}
mOpacity = op;
mHaveOpacity = true;
return op;
}
public final boolean isStateful() {
if (mHaveStateful) {
return mStateful;
}
boolean stateful = false;
final int N = mNum;
for (int i = 0; i < N; i++) {
if (mChildren[i].mDrawable.isStateful()) {
stateful = true;
break;
}
}
mStateful = stateful;
mHaveStateful = true;
return stateful;
}
public boolean canConstantState() {
if (!mCheckedConstantState && mChildren != null) {
mCanConstantState = true;
final int N = mNum;
for (int i = 0; i < N; i++) {
if (mChildren[i].mDrawable.getConstantState() == null) {
mCanConstantState = false;
break;
}
}
mCheckedConstantState = true;
}
return mCanConstantState;
}
}
}