| /* |
| * 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.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.Bitmap; |
| import android.graphics.BitmapShader; |
| import android.graphics.BlendMode; |
| import android.graphics.BlendModeColorFilter; |
| import android.graphics.Canvas; |
| import android.graphics.ColorFilter; |
| import android.graphics.ImageDecoder; |
| import android.graphics.Insets; |
| import android.graphics.Matrix; |
| import android.graphics.Outline; |
| import android.graphics.Paint; |
| import android.graphics.PixelFormat; |
| import android.graphics.PorterDuff.Mode; |
| 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.FileInputStream; |
| 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. |
| * <p>It can be defined in an XML file with the <code><bitmap></code> element. For more |
| * information, see the guide to <a |
| * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p> |
| * <p> |
| * 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}. |
| * </p> |
| * |
| * @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 |
| |
| @UnsupportedAppUsage |
| private BitmapState mBitmapState; |
| private BlendModeColorFilter mBlendModeFilter; |
| |
| @UnsupportedAppUsage |
| 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() { |
| init(new BitmapState((Bitmap) null), 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) { |
| init(new BitmapState((Bitmap) null), res); |
| } |
| |
| /** |
| * 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) { |
| init(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) { |
| init(new BitmapState(bitmap), res); |
| } |
| |
| /** |
| * 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(null, filepath); |
| } |
| |
| /** |
| * Create a drawable by opening a given file path and decoding the bitmap. |
| */ |
| @SuppressWarnings({ "unused", "ChainingConstructorIgnoresParameter" }) |
| public BitmapDrawable(Resources res, String filepath) { |
| Bitmap bitmap = null; |
| try (FileInputStream stream = new FileInputStream(filepath)) { |
| bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(res, stream), |
| (decoder, info, src) -> { |
| decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); |
| }); |
| } catch (Exception e) { |
| /* do nothing. This matches the behavior of BitmapFactory.decodeFile() |
| If the exception happened on decode, mBitmapState.mBitmap will be null. |
| */ |
| } finally { |
| init(new BitmapState(bitmap), res); |
| 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(null, is); |
| } |
| |
| /** |
| * Create a drawable by decoding a bitmap from the given input stream. |
| */ |
| @SuppressWarnings({ "unused", "ChainingConstructorIgnoresParameter" }) |
| public BitmapDrawable(Resources res, java.io.InputStream is) { |
| Bitmap bitmap = null; |
| try { |
| bitmap = ImageDecoder.decodeBitmap(ImageDecoder.createSource(res, is), |
| (decoder, info, src) -> { |
| decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); |
| }); |
| } catch (Exception e) { |
| /* do nothing. This matches the behavior of BitmapFactory.decodeStream() |
| If the exception happened on decode, mBitmapState.mBitmap will be null. |
| */ |
| } finally { |
| init(new BitmapState(bitmap), res); |
| 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 */ |
| @UnsupportedAppUsage |
| 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 (mBlendModeFilter != null && paint.getColorFilter() == null) { |
| paint.setColorFilter(mBlendModeFilter); |
| 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; |
| } |
| |
| @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; |
| mBlendModeFilter = updateBlendModeFilter(mBlendModeFilter, tint, |
| mBitmapState.mBlendMode); |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public void setTintBlendMode(@NonNull BlendMode blendMode) { |
| final BitmapState state = mBitmapState; |
| if (state.mBlendMode != blendMode) { |
| state.mBlendMode = blendMode; |
| mBlendModeFilter = updateBlendModeFilter(mBlendModeFilter, mBitmapState.mTint, |
| blendMode); |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * @hide only needed by a hack within ProgressBar |
| */ |
| @UnsupportedAppUsage |
| public ColorStateList getTint() { |
| return mBitmapState.mTint; |
| } |
| |
| /** |
| * @hide only needed by a hack within ProgressBar |
| */ |
| @UnsupportedAppUsage |
| public Mode getTintMode() { |
| return BlendMode.blendModeToPorterDuffMode(mBitmapState.mBlendMode); |
| } |
| |
| /** |
| * @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.mBlendMode != null) { |
| mBlendModeFilter = updateBlendModeFilter(mBlendModeFilter, state.mTint, |
| state.mBlendMode); |
| 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() + |
| ": <bitmap> 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; |
| } |
| } |
| |
| int density = Bitmap.DENSITY_NONE; |
| if (value.density == TypedValue.DENSITY_DEFAULT) { |
| density = DisplayMetrics.DENSITY_DEFAULT; |
| } else if (value.density != TypedValue.DENSITY_NONE) { |
| density = value.density; |
| } |
| |
| Bitmap bitmap = null; |
| try (InputStream is = r.openRawResource(srcResId, value)) { |
| ImageDecoder.Source source = ImageDecoder.createSource(r, is, density); |
| bitmap = ImageDecoder.decodeBitmap(source, (decoder, info, src) -> { |
| decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); |
| }); |
| } catch (Exception e) { |
| // Do nothing and pick up the error below. |
| } |
| |
| if (bitmap == null) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| ": <bitmap> 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.mBlendMode = Drawable.parseBlendMode(tintMode, BlendMode.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; |
| BlendMode mBlendMode = DEFAULT_BLEND_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; |
| mBlendMode = bitmapState.mBlendMode; |
| 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); |
| } |
| } |
| |
| private BitmapDrawable(BitmapState state, Resources res) { |
| init(state, res); |
| } |
| |
| /** |
| * The one helper to rule them all. This is called by all public & private |
| * constructors to set the state and initialize local properties. |
| */ |
| private void init(BitmapState state, Resources res) { |
| mBitmapState = state; |
| updateLocalState(res); |
| |
| if (mBitmapState != null && res != null) { |
| mBitmapState.mTargetDensity = mTargetDensity; |
| } |
| } |
| |
| /** |
| * 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); |
| mBlendModeFilter = updateBlendModeFilter(mBlendModeFilter, mBitmapState.mTint, |
| mBitmapState.mBlendMode); |
| computeBitmapSize(); |
| } |
| } |