/* * 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.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.Xfermode; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.LayoutDirection; import android.util.TypedValue; import android.view.Gravity; import com.android.internal.R; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; /** * A Drawable that wraps a bitmap and can be tiled, stretched, or aligned. You can create a * BitmapDrawable from a file path, an input stream, through XML inflation, or from * a {@link android.graphics.Bitmap} object. *

It can be defined in an XML file with the <bitmap> element. For more * information, see the guide to Drawable Resources.

*

* Also see the {@link android.graphics.Bitmap} class, which handles the management and * transformation of raw bitmap graphics, and should be used when drawing to a * {@link android.graphics.Canvas}. *

* * @attr ref android.R.styleable#BitmapDrawable_src * @attr ref android.R.styleable#BitmapDrawable_antialias * @attr ref android.R.styleable#BitmapDrawable_filter * @attr ref android.R.styleable#BitmapDrawable_dither * @attr ref android.R.styleable#BitmapDrawable_gravity * @attr ref android.R.styleable#BitmapDrawable_mipMap * @attr ref android.R.styleable#BitmapDrawable_tileMode */ public class BitmapDrawable extends Drawable { private static final int DEFAULT_PAINT_FLAGS = Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG; // Constants for {@link android.R.styleable#BitmapDrawable_tileMode}. private static final int TILE_MODE_UNDEFINED = -2; private static final int TILE_MODE_DISABLED = -1; private static final int TILE_MODE_CLAMP = 0; private static final int TILE_MODE_REPEAT = 1; private static final int TILE_MODE_MIRROR = 2; private final Rect mDstRect = new Rect(); // #updateDstRectAndInsetsIfDirty() sets this private BitmapState mBitmapState; private PorterDuffColorFilter mTintFilter; private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; private boolean mDstRectAndInsetsDirty = true; private boolean mMutated; // These are scaled to match the target density. private int mBitmapWidth; private int mBitmapHeight; /** Optical insets due to gravity. */ private Insets mOpticalInsets = Insets.NONE; // Mirroring matrix for using with Shaders private Matrix mMirrorMatrix; /** * Create an empty drawable, not dealing with density. * @deprecated Use {@link #BitmapDrawable(android.content.res.Resources, android.graphics.Bitmap)} * instead to specify a bitmap to draw with and ensure the correct density is set. */ @Deprecated public BitmapDrawable() { mBitmapState = new BitmapState((Bitmap) null); } /** * Create an empty drawable, setting initial target density based on * the display metrics of the resources. * * @deprecated Use {@link #BitmapDrawable(android.content.res.Resources, android.graphics.Bitmap)} * instead to specify a bitmap to draw with. */ @SuppressWarnings("unused") @Deprecated public BitmapDrawable(Resources res) { mBitmapState = new BitmapState((Bitmap) null); mBitmapState.mTargetDensity = mTargetDensity; } /** * Create drawable from a bitmap, not dealing with density. * @deprecated Use {@link #BitmapDrawable(Resources, Bitmap)} to ensure * that the drawable has correctly set its target density. */ @Deprecated public BitmapDrawable(Bitmap bitmap) { this(new BitmapState(bitmap), null); } /** * Create drawable from a bitmap, setting initial target density based on * the display metrics of the resources. */ public BitmapDrawable(Resources res, Bitmap bitmap) { this(new BitmapState(bitmap), res); mBitmapState.mTargetDensity = mTargetDensity; } /** * Create a drawable by opening a given file path and decoding the bitmap. * @deprecated Use {@link #BitmapDrawable(Resources, String)} to ensure * that the drawable has correctly set its target density. */ @Deprecated public BitmapDrawable(String filepath) { this(new BitmapState(BitmapFactory.decodeFile(filepath)), null); if (mBitmapState.mBitmap == null) { android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath); } } /** * Create a drawable by opening a given file path and decoding the bitmap. */ @SuppressWarnings("unused") public BitmapDrawable(Resources res, String filepath) { this(new BitmapState(BitmapFactory.decodeFile(filepath)), null); mBitmapState.mTargetDensity = mTargetDensity; if (mBitmapState.mBitmap == null) { android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath); } } /** * Create a drawable by decoding a bitmap from the given input stream. * @deprecated Use {@link #BitmapDrawable(Resources, java.io.InputStream)} to ensure * that the drawable has correctly set its target density. */ @Deprecated public BitmapDrawable(java.io.InputStream is) { this(new BitmapState(BitmapFactory.decodeStream(is)), null); if (mBitmapState.mBitmap == null) { android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is); } } /** * Create a drawable by decoding a bitmap from the given input stream. */ @SuppressWarnings("unused") public BitmapDrawable(Resources res, java.io.InputStream is) { this(new BitmapState(BitmapFactory.decodeStream(is)), null); mBitmapState.mTargetDensity = mTargetDensity; if (mBitmapState.mBitmap == null) { android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is); } } /** * Returns the paint used to render this drawable. */ public final Paint getPaint() { return mBitmapState.mPaint; } /** * Returns the bitmap used by this drawable to render. May be null. */ public final Bitmap getBitmap() { return mBitmapState.mBitmap; } private void computeBitmapSize() { final Bitmap bitmap = mBitmapState.mBitmap; if (bitmap != null) { mBitmapWidth = bitmap.getScaledWidth(mTargetDensity); mBitmapHeight = bitmap.getScaledHeight(mTargetDensity); } else { mBitmapWidth = mBitmapHeight = -1; } } /** @hide */ public void setBitmap(Bitmap bitmap) { if (mBitmapState.mBitmap != bitmap) { mBitmapState.mBitmap = bitmap; computeBitmapSize(); invalidateSelf(); } } /** * Set the density scale at which this drawable will be rendered. This * method assumes the drawable will be rendered at the same density as the * specified canvas. * * @param canvas The Canvas from which the density scale must be obtained. * * @see android.graphics.Bitmap#setDensity(int) * @see android.graphics.Bitmap#getDensity() */ public void setTargetDensity(Canvas canvas) { setTargetDensity(canvas.getDensity()); } /** * Set the density scale at which this drawable will be rendered. * * @param metrics The DisplayMetrics indicating the density scale for this drawable. * * @see android.graphics.Bitmap#setDensity(int) * @see android.graphics.Bitmap#getDensity() */ public void setTargetDensity(DisplayMetrics metrics) { setTargetDensity(metrics.densityDpi); } /** * Set the density at which this drawable will be rendered. * * @param density The density scale for this drawable. * * @see android.graphics.Bitmap#setDensity(int) * @see android.graphics.Bitmap#getDensity() */ public void setTargetDensity(int density) { if (mTargetDensity != density) { mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; if (mBitmapState.mBitmap != null) { computeBitmapSize(); } invalidateSelf(); } } /** Get the gravity used to position/stretch the bitmap within its bounds. * See android.view.Gravity * @return the gravity applied to the bitmap */ public int getGravity() { return mBitmapState.mGravity; } /** Set the gravity used to position/stretch the bitmap within its bounds. See android.view.Gravity * @param gravity the gravity */ public void setGravity(int gravity) { if (mBitmapState.mGravity != gravity) { mBitmapState.mGravity = gravity; mDstRectAndInsetsDirty = true; invalidateSelf(); } } /** * Enables or disables the mipmap hint for this drawable's bitmap. * See {@link Bitmap#setHasMipMap(boolean)} for more information. * * If the bitmap is null calling this method has no effect. * * @param mipMap True if the bitmap should use mipmaps, false otherwise. * * @see #hasMipMap() */ public void setMipMap(boolean mipMap) { if (mBitmapState.mBitmap != null) { mBitmapState.mBitmap.setHasMipMap(mipMap); invalidateSelf(); } } /** * Indicates whether the mipmap hint is enabled on this drawable's bitmap. * * @return True if the mipmap hint is set, false otherwise. If the bitmap * is null, this method always returns false. * * @see #setMipMap(boolean) * @attr ref android.R.styleable#BitmapDrawable_mipMap */ public boolean hasMipMap() { return mBitmapState.mBitmap != null && mBitmapState.mBitmap.hasMipMap(); } /** * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects * the edges of the bitmap only so it applies only when the drawable is rotated. * * @param aa True if the bitmap should be anti-aliased, false otherwise. * * @see #hasAntiAlias() */ public void setAntiAlias(boolean aa) { mBitmapState.mPaint.setAntiAlias(aa); invalidateSelf(); } /** * Indicates whether anti-aliasing is enabled for this drawable. * * @return True if anti-aliasing is enabled, false otherwise. * * @see #setAntiAlias(boolean) */ public boolean hasAntiAlias() { return mBitmapState.mPaint.isAntiAlias(); } @Override public void setFilterBitmap(boolean filter) { mBitmapState.mPaint.setFilterBitmap(filter); invalidateSelf(); } @Override public boolean isFilterBitmap() { return mBitmapState.mPaint.isFilterBitmap(); } @Override public void setDither(boolean dither) { mBitmapState.mPaint.setDither(dither); invalidateSelf(); } /** * Indicates the repeat behavior of this drawable on the X axis. * * @return {@link android.graphics.Shader.TileMode#CLAMP} if the bitmap does not repeat, * {@link android.graphics.Shader.TileMode#REPEAT} or * {@link android.graphics.Shader.TileMode#MIRROR} otherwise. */ public Shader.TileMode getTileModeX() { return mBitmapState.mTileModeX; } /** * Indicates the repeat behavior of this drawable on the Y axis. * * @return {@link android.graphics.Shader.TileMode#CLAMP} if the bitmap does not repeat, * {@link android.graphics.Shader.TileMode#REPEAT} or * {@link android.graphics.Shader.TileMode#MIRROR} otherwise. */ public Shader.TileMode getTileModeY() { return mBitmapState.mTileModeY; } /** * Sets the repeat behavior of this drawable on the X axis. By default, the drawable * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) * if the bitmap is smaller than this drawable. * * @param mode The repeat mode for this drawable. * * @see #setTileModeY(android.graphics.Shader.TileMode) * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode) * @attr ref android.R.styleable#BitmapDrawable_tileModeX */ public void setTileModeX(Shader.TileMode mode) { setTileModeXY(mode, mBitmapState.mTileModeY); } /** * Sets the repeat behavior of this drawable on the Y axis. By default, the drawable * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) * if the bitmap is smaller than this drawable. * * @param mode The repeat mode for this drawable. * * @see #setTileModeX(android.graphics.Shader.TileMode) * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode) * @attr ref android.R.styleable#BitmapDrawable_tileModeY */ public final void setTileModeY(Shader.TileMode mode) { setTileModeXY(mBitmapState.mTileModeX, mode); } /** * Sets the repeat behavior of this drawable on both axis. By default, the drawable * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) * if the bitmap is smaller than this drawable. * * @param xmode The X repeat mode for this drawable. * @param ymode The Y repeat mode for this drawable. * * @see #setTileModeX(android.graphics.Shader.TileMode) * @see #setTileModeY(android.graphics.Shader.TileMode) */ public void setTileModeXY(Shader.TileMode xmode, Shader.TileMode ymode) { final BitmapState state = mBitmapState; if (state.mTileModeX != xmode || state.mTileModeY != ymode) { state.mTileModeX = xmode; state.mTileModeY = ymode; state.mRebuildShader = true; mDstRectAndInsetsDirty = true; invalidateSelf(); } } @Override public void setAutoMirrored(boolean mirrored) { if (mBitmapState.mAutoMirrored != mirrored) { mBitmapState.mAutoMirrored = mirrored; invalidateSelf(); } } @Override public final boolean isAutoMirrored() { return mBitmapState.mAutoMirrored; } @Override public @Config int getChangingConfigurations() { return super.getChangingConfigurations() | mBitmapState.getChangingConfigurations(); } private boolean needMirroring() { return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL; } @Override protected void onBoundsChange(Rect bounds) { mDstRectAndInsetsDirty = true; final Bitmap bitmap = mBitmapState.mBitmap; final Shader shader = mBitmapState.mPaint.getShader(); if (bitmap != null && shader != null) { updateShaderMatrix(bitmap, mBitmapState.mPaint, shader, needMirroring()); } } @Override public void draw(Canvas canvas) { final Bitmap bitmap = mBitmapState.mBitmap; if (bitmap == null) { return; } final BitmapState state = mBitmapState; final Paint paint = state.mPaint; if (state.mRebuildShader) { final Shader.TileMode tmx = state.mTileModeX; final Shader.TileMode tmy = state.mTileModeY; if (tmx == null && tmy == null) { paint.setShader(null); } else { paint.setShader(new BitmapShader(bitmap, tmx == null ? Shader.TileMode.CLAMP : tmx, tmy == null ? Shader.TileMode.CLAMP : tmy)); } state.mRebuildShader = false; } final int restoreAlpha; if (state.mBaseAlpha != 1.0f) { final Paint p = getPaint(); restoreAlpha = p.getAlpha(); p.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f)); } else { restoreAlpha = -1; } final boolean clearColorFilter; if (mTintFilter != null && paint.getColorFilter() == null) { paint.setColorFilter(mTintFilter); clearColorFilter = true; } else { clearColorFilter = false; } updateDstRectAndInsetsIfDirty(); final Shader shader = paint.getShader(); final boolean needMirroring = needMirroring(); if (shader == null) { if (needMirroring) { canvas.save(); // Mirror the bitmap canvas.translate(mDstRect.right - mDstRect.left, 0); canvas.scale(-1.0f, 1.0f); } canvas.drawBitmap(bitmap, null, mDstRect, paint); if (needMirroring) { canvas.restore(); } } else { updateShaderMatrix(bitmap, paint, shader, needMirroring); canvas.drawRect(mDstRect, paint); } if (clearColorFilter) { paint.setColorFilter(null); } if (restoreAlpha >= 0) { paint.setAlpha(restoreAlpha); } } /** * Updates the {@code paint}'s shader matrix to be consistent with the * destination size and layout direction. * * @param bitmap the bitmap to be drawn * @param paint the paint used to draw the bitmap * @param shader the shader to set on the paint * @param needMirroring whether the bitmap should be mirrored */ private void updateShaderMatrix(@NonNull Bitmap bitmap, @NonNull Paint paint, @NonNull Shader shader, boolean needMirroring) { final int sourceDensity = bitmap.getDensity(); final int targetDensity = mTargetDensity; final boolean needScaling = sourceDensity != 0 && sourceDensity != targetDensity; if (needScaling || needMirroring) { final Matrix matrix = getOrCreateMirrorMatrix(); matrix.reset(); if (needMirroring) { final int dx = mDstRect.right - mDstRect.left; matrix.setTranslate(dx, 0); matrix.setScale(-1, 1); } if (needScaling) { final float densityScale = targetDensity / (float) sourceDensity; matrix.postScale(densityScale, densityScale); } shader.setLocalMatrix(matrix); } else { mMirrorMatrix = null; shader.setLocalMatrix(Matrix.IDENTITY_MATRIX); } paint.setShader(shader); } private Matrix getOrCreateMirrorMatrix() { if (mMirrorMatrix == null) { mMirrorMatrix = new Matrix(); } return mMirrorMatrix; } private void updateDstRectAndInsetsIfDirty() { if (mDstRectAndInsetsDirty) { if (mBitmapState.mTileModeX == null && mBitmapState.mTileModeY == null) { final Rect bounds = getBounds(); final int layoutDirection = getLayoutDirection(); Gravity.apply(mBitmapState.mGravity, mBitmapWidth, mBitmapHeight, bounds, mDstRect, layoutDirection); final int left = mDstRect.left - bounds.left; final int top = mDstRect.top - bounds.top; final int right = bounds.right - mDstRect.right; final int bottom = bounds.bottom - mDstRect.bottom; mOpticalInsets = Insets.of(left, top, right, bottom); } else { copyBounds(mDstRect); mOpticalInsets = Insets.NONE; } } mDstRectAndInsetsDirty = false; } /** * @hide */ @Override public Insets getOpticalInsets() { updateDstRectAndInsetsIfDirty(); return mOpticalInsets; } @Override public void getOutline(@NonNull Outline outline) { updateDstRectAndInsetsIfDirty(); outline.setRect(mDstRect); // Only opaque Bitmaps can report a non-0 alpha, // since only they are guaranteed to fill their bounds boolean opaqueOverShape = mBitmapState.mBitmap != null && !mBitmapState.mBitmap.hasAlpha(); outline.setAlpha(opaqueOverShape ? getAlpha() / 255.0f : 0.0f); } @Override public void setAlpha(int alpha) { final int oldAlpha = mBitmapState.mPaint.getAlpha(); if (alpha != oldAlpha) { mBitmapState.mPaint.setAlpha(alpha); invalidateSelf(); } } @Override public int getAlpha() { return mBitmapState.mPaint.getAlpha(); } @Override public void setColorFilter(ColorFilter colorFilter) { mBitmapState.mPaint.setColorFilter(colorFilter); invalidateSelf(); } @Override public ColorFilter getColorFilter() { return mBitmapState.mPaint.getColorFilter(); } @Override public void setTintList(ColorStateList tint) { final BitmapState state = mBitmapState; if (state.mTint != tint) { state.mTint = tint; mTintFilter = updateTintFilter(mTintFilter, tint, mBitmapState.mTintMode); invalidateSelf(); } } @Override public void setTintMode(PorterDuff.Mode tintMode) { final BitmapState state = mBitmapState; if (state.mTintMode != tintMode) { state.mTintMode = tintMode; mTintFilter = updateTintFilter(mTintFilter, mBitmapState.mTint, tintMode); invalidateSelf(); } } /** * @hide only needed by a hack within ProgressBar */ public ColorStateList getTint() { return mBitmapState.mTint; } /** * @hide only needed by a hack within ProgressBar */ public Mode getTintMode() { return mBitmapState.mTintMode; } /** * @hide Candidate for future API inclusion */ @Override public void setXfermode(Xfermode xfermode) { mBitmapState.mPaint.setXfermode(xfermode); invalidateSelf(); } /** * A mutable BitmapDrawable still shares its Bitmap with any other Drawable * that comes from the same resource. * * @return This drawable. */ @Override public Drawable mutate() { if (!mMutated && super.mutate() == this) { mBitmapState = new BitmapState(mBitmapState); mMutated = true; } return this; } /** * @hide */ public void clearMutated() { super.clearMutated(); mMutated = false; } @Override protected boolean onStateChange(int[] stateSet) { final BitmapState state = mBitmapState; if (state.mTint != null && state.mTintMode != null) { mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode); return true; } return false; } @Override public boolean isStateful() { return (mBitmapState.mTint != null && mBitmapState.mTint.isStateful()) || super.isStateful(); } /** @hide */ @Override public boolean hasFocusStateSpecified() { return mBitmapState.mTint != null && mBitmapState.mTint.hasFocusStateSpecified(); } @Override public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { super.inflate(r, parser, attrs, theme); final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.BitmapDrawable); updateStateFromTypedArray(a, mSrcDensityOverride); verifyRequiredAttributes(a); a.recycle(); // Update local properties. updateLocalState(r); } /** * Ensures all required attributes are set. * * @throws XmlPullParserException if any required attributes are missing */ private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { // If we're not waiting on a theme, verify required attributes. final BitmapState state = mBitmapState; if (state.mBitmap == null && (state.mThemeAttrs == null || state.mThemeAttrs[R.styleable.BitmapDrawable_src] == 0)) { throw new XmlPullParserException(a.getPositionDescription() + ": requires a valid 'src' attribute"); } } /** * Updates the constant state from the values in the typed array. */ private void updateStateFromTypedArray(TypedArray a, int srcDensityOverride) throws XmlPullParserException { final Resources r = a.getResources(); final BitmapState state = mBitmapState; // Account for any configuration changes. state.mChangingConfigurations |= a.getChangingConfigurations(); // Extract the theme attributes, if any. state.mThemeAttrs = a.extractThemeAttrs(); state.mSrcDensityOverride = srcDensityOverride; state.mTargetDensity = Drawable.resolveDensity(r, 0); final int srcResId = a.getResourceId(R.styleable.BitmapDrawable_src, 0); if (srcResId != 0) { final TypedValue value = new TypedValue(); r.getValueForDensity(srcResId, srcDensityOverride, value, true); // Pretend the requested density is actually the display density. If // the drawable returned is not the requested density, then force it // to be scaled later by dividing its density by the ratio of // requested density to actual device density. Drawables that have // undefined density or no density don't need to be handled here. if (srcDensityOverride > 0 && value.density > 0 && value.density != TypedValue.DENSITY_NONE) { if (value.density == srcDensityOverride) { value.density = r.getDisplayMetrics().densityDpi; } else { value.density = (value.density * r.getDisplayMetrics().densityDpi) / srcDensityOverride; } } Bitmap bitmap = null; try (InputStream is = r.openRawResource(srcResId, value)) { bitmap = BitmapFactory.decodeResourceStream(r, value, is, null, null); } catch (Exception e) { // Do nothing and pick up the error below. } if (bitmap == null) { throw new XmlPullParserException(a.getPositionDescription() + ": requires a valid 'src' attribute"); } state.mBitmap = bitmap; } final boolean defMipMap = state.mBitmap != null ? state.mBitmap.hasMipMap() : false; setMipMap(a.getBoolean(R.styleable.BitmapDrawable_mipMap, defMipMap)); state.mAutoMirrored = a.getBoolean( R.styleable.BitmapDrawable_autoMirrored, state.mAutoMirrored); state.mBaseAlpha = a.getFloat(R.styleable.BitmapDrawable_alpha, state.mBaseAlpha); final int tintMode = a.getInt(R.styleable.BitmapDrawable_tintMode, -1); if (tintMode != -1) { state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN); } final ColorStateList tint = a.getColorStateList(R.styleable.BitmapDrawable_tint); if (tint != null) { state.mTint = tint; } final Paint paint = mBitmapState.mPaint; paint.setAntiAlias(a.getBoolean( R.styleable.BitmapDrawable_antialias, paint.isAntiAlias())); paint.setFilterBitmap(a.getBoolean( R.styleable.BitmapDrawable_filter, paint.isFilterBitmap())); paint.setDither(a.getBoolean(R.styleable.BitmapDrawable_dither, paint.isDither())); setGravity(a.getInt(R.styleable.BitmapDrawable_gravity, state.mGravity)); final int tileMode = a.getInt(R.styleable.BitmapDrawable_tileMode, TILE_MODE_UNDEFINED); if (tileMode != TILE_MODE_UNDEFINED) { final Shader.TileMode mode = parseTileMode(tileMode); setTileModeXY(mode, mode); } final int tileModeX = a.getInt(R.styleable.BitmapDrawable_tileModeX, TILE_MODE_UNDEFINED); if (tileModeX != TILE_MODE_UNDEFINED) { setTileModeX(parseTileMode(tileModeX)); } final int tileModeY = a.getInt(R.styleable.BitmapDrawable_tileModeY, TILE_MODE_UNDEFINED); if (tileModeY != TILE_MODE_UNDEFINED) { setTileModeY(parseTileMode(tileModeY)); } } @Override public void applyTheme(Theme t) { super.applyTheme(t); final BitmapState state = mBitmapState; if (state == null) { return; } if (state.mThemeAttrs != null) { final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.BitmapDrawable); try { updateStateFromTypedArray(a, state.mSrcDensityOverride); } catch (XmlPullParserException e) { rethrowAsRuntimeException(e); } finally { a.recycle(); } } // Apply theme to contained color state list. if (state.mTint != null && state.mTint.canApplyTheme()) { state.mTint = state.mTint.obtainForTheme(t); } // Update local properties. updateLocalState(t.getResources()); } private static Shader.TileMode parseTileMode(int tileMode) { switch (tileMode) { case TILE_MODE_CLAMP: return Shader.TileMode.CLAMP; case TILE_MODE_REPEAT: return Shader.TileMode.REPEAT; case TILE_MODE_MIRROR: return Shader.TileMode.MIRROR; default: return null; } } @Override public boolean canApplyTheme() { return mBitmapState != null && mBitmapState.canApplyTheme(); } @Override public int getIntrinsicWidth() { return mBitmapWidth; } @Override public int getIntrinsicHeight() { return mBitmapHeight; } @Override public int getOpacity() { if (mBitmapState.mGravity != Gravity.FILL) { return PixelFormat.TRANSLUCENT; } final Bitmap bitmap = mBitmapState.mBitmap; return (bitmap == null || bitmap.hasAlpha() || mBitmapState.mPaint.getAlpha() < 255) ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; } @Override public final ConstantState getConstantState() { mBitmapState.mChangingConfigurations |= getChangingConfigurations(); return mBitmapState; } final static class BitmapState extends ConstantState { final Paint mPaint; // Values loaded during inflation. int[] mThemeAttrs = null; Bitmap mBitmap = null; ColorStateList mTint = null; Mode mTintMode = DEFAULT_TINT_MODE; int mGravity = Gravity.FILL; float mBaseAlpha = 1.0f; Shader.TileMode mTileModeX = null; Shader.TileMode mTileModeY = null; // The density to use when looking up the bitmap in Resources. A value of 0 means use // the system's density. int mSrcDensityOverride = 0; // The density at which to render the bitmap. int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; boolean mAutoMirrored = false; @Config int mChangingConfigurations; boolean mRebuildShader; BitmapState(Bitmap bitmap) { mBitmap = bitmap; mPaint = new Paint(DEFAULT_PAINT_FLAGS); } BitmapState(BitmapState bitmapState) { mBitmap = bitmapState.mBitmap; mTint = bitmapState.mTint; mTintMode = bitmapState.mTintMode; mThemeAttrs = bitmapState.mThemeAttrs; mChangingConfigurations = bitmapState.mChangingConfigurations; mGravity = bitmapState.mGravity; mTileModeX = bitmapState.mTileModeX; mTileModeY = bitmapState.mTileModeY; mSrcDensityOverride = bitmapState.mSrcDensityOverride; mTargetDensity = bitmapState.mTargetDensity; mBaseAlpha = bitmapState.mBaseAlpha; mPaint = new Paint(bitmapState.mPaint); mRebuildShader = bitmapState.mRebuildShader; mAutoMirrored = bitmapState.mAutoMirrored; } @Override public boolean canApplyTheme() { return mThemeAttrs != null || mTint != null && mTint.canApplyTheme(); } @Override public Drawable newDrawable() { return new BitmapDrawable(this, null); } @Override public Drawable newDrawable(Resources res) { return new BitmapDrawable(this, res); } @Override public @Config int getChangingConfigurations() { return mChangingConfigurations | (mTint != null ? mTint.getChangingConfigurations() : 0); } } /** * The one constructor to rule them all. This is called by all public * constructors to set the state and initialize local properties. */ private BitmapDrawable(BitmapState state, Resources res) { mBitmapState = state; updateLocalState(res); } /** * 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) { mTargetDensity = resolveDensity(res, mBitmapState.mTargetDensity); mTintFilter = updateTintFilter(mTintFilter, mBitmapState.mTint, mBitmapState.mTintMode); computeBitmapSize(); } }