/* * 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 android.annotation.NonNull; import android.content.pm.ActivityInfo.Config; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.Resources.Theme; 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.Rect; import android.graphics.PorterDuff.Mode; import android.os.SystemClock; import android.util.DisplayMetrics; import android.util.LayoutDirection; import android.util.SparseArray; import android.view.View; import java.util.Collection; /** * A helper class that contains several {@link Drawable}s and selects which one to use. * * You can subclass it to create your own DrawableContainers or directly use one its child classes. */ public class DrawableContainer extends Drawable implements Drawable.Callback { private static final boolean DEBUG = false; private static final String TAG = "DrawableContainer"; /** * To be proper, we should have a getter for dither (and alpha, etc.) * so that proxy classes like this can save/restore their delegates' * values, but we don't have getters. Since we do have setters * (e.g. setDither), which this proxy forwards on, we have to have some * default/initial setting. * * The initial setting for dither is now true, since it almost always seems * to improve the quality at negligible cost. */ private static final boolean DEFAULT_DITHER = true; private DrawableContainerState mDrawableContainerState; private Rect mHotspotBounds; private Drawable mCurrDrawable; private Drawable mLastDrawable; private int mAlpha = 0xFF; /** Whether setAlpha() has been called at least once. */ private boolean mHasAlpha; private int mCurIndex = -1; private int mLastIndex = -1; private boolean mMutated; // Animations. private Runnable mAnimationRunnable; private long mEnterAnimationEnd; private long mExitAnimationEnd; /** Callback that blocks invalidation. Used for drawable initialization. */ private BlockInvalidateCallback mBlockInvalidateCallback; // overrides from Drawable @Override public void draw(Canvas canvas) { if (mCurrDrawable != null) { mCurrDrawable.draw(canvas); } if (mLastDrawable != null) { mLastDrawable.draw(canvas); } } @Override public @Config int getChangingConfigurations() { return super.getChangingConfigurations() | mDrawableContainerState.getChangingConfigurations(); } private boolean needsMirroring() { return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL; } @Override public boolean getPadding(Rect padding) { final Rect r = mDrawableContainerState.getConstantPadding(); boolean result; if (r != null) { padding.set(r); result = (r.left | r.top | r.bottom | r.right) != 0; } else { if (mCurrDrawable != null) { result = mCurrDrawable.getPadding(padding); } else { result = super.getPadding(padding); } } if (needsMirroring()) { final int left = padding.left; final int right = padding.right; padding.left = right; padding.right = left; } return result; } /** * @hide */ @Override public Insets getOpticalInsets() { if (mCurrDrawable != null) { return mCurrDrawable.getOpticalInsets(); } return Insets.NONE; } @Override public void getOutline(@NonNull Outline outline) { if (mCurrDrawable != null) { mCurrDrawable.getOutline(outline); } } @Override public void setAlpha(int alpha) { if (!mHasAlpha || mAlpha != alpha) { mHasAlpha = true; mAlpha = alpha; if (mCurrDrawable != null) { if (mEnterAnimationEnd == 0) { mCurrDrawable.setAlpha(alpha); } else { animate(false); } } } } @Override public int getAlpha() { return mAlpha; } @Override public void setDither(boolean dither) { if (mDrawableContainerState.mDither != dither) { mDrawableContainerState.mDither = dither; if (mCurrDrawable != null) { mCurrDrawable.setDither(mDrawableContainerState.mDither); } } } @Override public void setColorFilter(ColorFilter colorFilter) { mDrawableContainerState.mHasColorFilter = true; if (mDrawableContainerState.mColorFilter != colorFilter) { mDrawableContainerState.mColorFilter = colorFilter; if (mCurrDrawable != null) { mCurrDrawable.setColorFilter(colorFilter); } } } @Override public void setTintList(ColorStateList tint) { mDrawableContainerState.mHasTintList = true; if (mDrawableContainerState.mTintList != tint) { mDrawableContainerState.mTintList = tint; if (mCurrDrawable != null) { mCurrDrawable.setTintList(tint); } } } @Override public void setTintMode(Mode tintMode) { mDrawableContainerState.mHasTintMode = true; if (mDrawableContainerState.mTintMode != tintMode) { mDrawableContainerState.mTintMode = tintMode; if (mCurrDrawable != null) { mCurrDrawable.setTintMode(tintMode); } } } /** * Change the global fade duration when a new drawable is entering * the scene. * @param ms The amount of time to fade in milliseconds. */ public void setEnterFadeDuration(int ms) { mDrawableContainerState.mEnterFadeDuration = ms; } /** * Change the global fade duration when a new drawable is leaving * the scene. * @param ms The amount of time to fade in milliseconds. */ public void setExitFadeDuration(int ms) { mDrawableContainerState.mExitFadeDuration = ms; } @Override protected void onBoundsChange(Rect bounds) { if (mLastDrawable != null) { mLastDrawable.setBounds(bounds); } if (mCurrDrawable != null) { mCurrDrawable.setBounds(bounds); } } @Override public boolean isStateful() { return mDrawableContainerState.isStateful(); } @Override public void setAutoMirrored(boolean mirrored) { if (mDrawableContainerState.mAutoMirrored != mirrored) { mDrawableContainerState.mAutoMirrored = mirrored; if (mCurrDrawable != null) { mCurrDrawable.setAutoMirrored(mDrawableContainerState.mAutoMirrored); } } } @Override public boolean isAutoMirrored() { return mDrawableContainerState.mAutoMirrored; } @Override public void jumpToCurrentState() { boolean changed = false; if (mLastDrawable != null) { mLastDrawable.jumpToCurrentState(); mLastDrawable = null; mLastIndex = -1; changed = true; } if (mCurrDrawable != null) { mCurrDrawable.jumpToCurrentState(); if (mHasAlpha) { mCurrDrawable.setAlpha(mAlpha); } } if (mExitAnimationEnd != 0) { mExitAnimationEnd = 0; changed = true; } if (mEnterAnimationEnd != 0) { mEnterAnimationEnd = 0; changed = true; } if (changed) { invalidateSelf(); } } @Override public void setHotspot(float x, float y) { if (mCurrDrawable != null) { mCurrDrawable.setHotspot(x, y); } } @Override public void setHotspotBounds(int left, int top, int right, int bottom) { if (mHotspotBounds == null) { mHotspotBounds = new Rect(left, top, right, bottom); } else { mHotspotBounds.set(left, top, right, bottom); } if (mCurrDrawable != null) { mCurrDrawable.setHotspotBounds(left, top, right, bottom); } } @Override public void getHotspotBounds(Rect outRect) { if (mHotspotBounds != null) { outRect.set(mHotspotBounds); } else { super.getHotspotBounds(outRect); } } @Override protected boolean onStateChange(int[] state) { if (mLastDrawable != null) { return mLastDrawable.setState(state); } if (mCurrDrawable != null) { return mCurrDrawable.setState(state); } return false; } @Override protected boolean onLevelChange(int level) { if (mLastDrawable != null) { return mLastDrawable.setLevel(level); } if (mCurrDrawable != null) { return mCurrDrawable.setLevel(level); } return false; } @Override public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) { // Let the container handle setting its own layout direction. Otherwise, // we're accessing potentially unused states. return mDrawableContainerState.setLayoutDirection(layoutDirection, getCurrentIndex()); } @Override public int getIntrinsicWidth() { if (mDrawableContainerState.isConstantSize()) { return mDrawableContainerState.getConstantWidth(); } return mCurrDrawable != null ? mCurrDrawable.getIntrinsicWidth() : -1; } @Override public int getIntrinsicHeight() { if (mDrawableContainerState.isConstantSize()) { return mDrawableContainerState.getConstantHeight(); } return mCurrDrawable != null ? mCurrDrawable.getIntrinsicHeight() : -1; } @Override public int getMinimumWidth() { if (mDrawableContainerState.isConstantSize()) { return mDrawableContainerState.getConstantMinimumWidth(); } return mCurrDrawable != null ? mCurrDrawable.getMinimumWidth() : 0; } @Override public int getMinimumHeight() { if (mDrawableContainerState.isConstantSize()) { return mDrawableContainerState.getConstantMinimumHeight(); } return mCurrDrawable != null ? mCurrDrawable.getMinimumHeight() : 0; } @Override public void invalidateDrawable(@NonNull Drawable who) { if (who == mCurrDrawable && getCallback() != null) { getCallback().invalidateDrawable(this); } } @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { if (who == mCurrDrawable && getCallback() != null) { getCallback().scheduleDrawable(this, what, when); } } @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { if (who == mCurrDrawable && getCallback() != null) { getCallback().unscheduleDrawable(this, what); } } @Override public boolean setVisible(boolean visible, boolean restart) { boolean changed = super.setVisible(visible, restart); if (mLastDrawable != null) { mLastDrawable.setVisible(visible, restart); } if (mCurrDrawable != null) { mCurrDrawable.setVisible(visible, restart); } return changed; } @Override public int getOpacity() { return mCurrDrawable == null || !mCurrDrawable.isVisible() ? PixelFormat.TRANSPARENT : mDrawableContainerState.getOpacity(); } /** @hide */ public void setCurrentIndex(int index) { selectDrawable(index); } /** @hide */ public int getCurrentIndex() { return mCurIndex; } /** * Sets the currently displayed drawable by index. *
* If an invalid index is specified, the current drawable will be set to
* {@code null} and the index will be set to {@code -1}.
*
* @param index the index of the drawable to display
* @return {@code true} if the drawable changed, {@code false} otherwise
*/
public boolean selectDrawable(int index) {
if (index == mCurIndex) {
return false;
}
final long now = SystemClock.uptimeMillis();
if (DEBUG) android.util.Log.i(TAG, toString() + " from " + mCurIndex + " to " + index
+ ": exit=" + mDrawableContainerState.mExitFadeDuration
+ " enter=" + mDrawableContainerState.mEnterFadeDuration);
if (mDrawableContainerState.mExitFadeDuration > 0) {
if (mLastDrawable != null) {
mLastDrawable.setVisible(false, false);
}
if (mCurrDrawable != null) {
mLastDrawable = mCurrDrawable;
mLastIndex = mCurIndex;
mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration;
} else {
mLastDrawable = null;
mLastIndex = -1;
mExitAnimationEnd = 0;
}
} else if (mCurrDrawable != null) {
mCurrDrawable.setVisible(false, false);
}
if (index >= 0 && index < mDrawableContainerState.mNumChildren) {
final Drawable d = mDrawableContainerState.getChild(index);
mCurrDrawable = d;
mCurIndex = index;
if (d != null) {
if (mDrawableContainerState.mEnterFadeDuration > 0) {
mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
}
initializeDrawableForDisplay(d);
}
} else {
mCurrDrawable = null;
mCurIndex = -1;
}
if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
if (mAnimationRunnable == null) {
mAnimationRunnable = new Runnable() {
@Override public void run() {
animate(true);
invalidateSelf();
}
};
} else {
unscheduleSelf(mAnimationRunnable);
}
// Compute first frame and schedule next animation.
animate(true);
}
invalidateSelf();
return true;
}
/**
* Initializes a drawable for display in this container.
*
* @param d The drawable to initialize.
*/
private void initializeDrawableForDisplay(Drawable d) {
if (mBlockInvalidateCallback == null) {
mBlockInvalidateCallback = new BlockInvalidateCallback();
}
// Temporary fix for suspending callbacks during initialization. We
// don't want any of these setters causing an invalidate() since that
// may call back into DrawableContainer.
d.setCallback(mBlockInvalidateCallback.wrap(d.getCallback()));
try {
if (mDrawableContainerState.mEnterFadeDuration <= 0 && mHasAlpha) {
d.setAlpha(mAlpha);
}
if (mDrawableContainerState.mHasColorFilter) {
// Color filter always overrides tint.
d.setColorFilter(mDrawableContainerState.mColorFilter);
} else {
if (mDrawableContainerState.mHasTintList) {
d.setTintList(mDrawableContainerState.mTintList);
}
if (mDrawableContainerState.mHasTintMode) {
d.setTintMode(mDrawableContainerState.mTintMode);
}
}
d.setVisible(isVisible(), true);
d.setDither(mDrawableContainerState.mDither);
d.setState(getState());
d.setLevel(getLevel());
d.setBounds(getBounds());
d.setLayoutDirection(getLayoutDirection());
d.setAutoMirrored(mDrawableContainerState.mAutoMirrored);
final Rect hotspotBounds = mHotspotBounds;
if (hotspotBounds != null) {
d.setHotspotBounds(hotspotBounds.left, hotspotBounds.top,
hotspotBounds.right, hotspotBounds.bottom);
}
} finally {
d.setCallback(mBlockInvalidateCallback.unwrap());
}
}
void animate(boolean schedule) {
mHasAlpha = true;
final long now = SystemClock.uptimeMillis();
boolean animating = false;
if (mCurrDrawable != null) {
if (mEnterAnimationEnd != 0) {
if (mEnterAnimationEnd <= now) {
mCurrDrawable.setAlpha(mAlpha);
mEnterAnimationEnd = 0;
} else {
int animAlpha = (int)((mEnterAnimationEnd-now)*255)
/ mDrawableContainerState.mEnterFadeDuration;
mCurrDrawable.setAlpha(((255-animAlpha)*mAlpha)/255);
animating = true;
}
}
} else {
mEnterAnimationEnd = 0;
}
if (mLastDrawable != null) {
if (mExitAnimationEnd != 0) {
if (mExitAnimationEnd <= now) {
mLastDrawable.setVisible(false, false);
mLastDrawable = null;
mLastIndex = -1;
mExitAnimationEnd = 0;
} else {
int animAlpha = (int)((mExitAnimationEnd-now)*255)
/ mDrawableContainerState.mExitFadeDuration;
mLastDrawable.setAlpha((animAlpha*mAlpha)/255);
animating = true;
}
}
} else {
mExitAnimationEnd = 0;
}
if (schedule && animating) {
scheduleSelf(mAnimationRunnable, now + 1000 / 60);
}
}
@Override
public Drawable getCurrent() {
return mCurrDrawable;
}
/**
* Updates the source density based on the resources used to inflate
* density-dependent values. Implementing classes should call this method
* during inflation.
*
* @param res the resources used to inflate density-dependent values
*/
final void updateDensity(Resources res) {
mDrawableContainerState.updateDensity(res);
}
@Override
public void applyTheme(Theme theme) {
mDrawableContainerState.applyTheme(theme);
}
@Override
public boolean canApplyTheme() {
return mDrawableContainerState.canApplyTheme();
}
@Override
public ConstantState getConstantState() {
if (mDrawableContainerState.canConstantState()) {
mDrawableContainerState.mChangingConfigurations = getChangingConfigurations();
return mDrawableContainerState;
}
return null;
}
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
final DrawableContainerState clone = cloneConstantState();
clone.mutate();
setConstantState(clone);
mMutated = true;
}
return this;
}
/**
* Returns a shallow copy of the container's constant state to be used as
* the base state for {@link #mutate()}.
*
* @return a shallow copy of the constant state
*/
DrawableContainerState cloneConstantState() {
return mDrawableContainerState;
}
/**
* @hide
*/
public void clearMutated() {
super.clearMutated();
mDrawableContainerState.clearMutated();
mMutated = false;
}
/**
* A ConstantState that can contain several {@link Drawable}s.
*
* This class was made public to enable testing, and its visibility may change in a future
* release.
*/
public abstract static class DrawableContainerState extends ConstantState {
final DrawableContainer mOwner;
Resources mSourceRes;
int mDensity = DisplayMetrics.DENSITY_DEFAULT;
@Config int mChangingConfigurations;
@Config int mChildrenChangingConfigurations;
SparseArray