NOTE: Linear blending is currently disabled in this CL as the
feature is still a work in progress
Android currently performs all blending (any kind of linear math
on colors really) on gamma-encoded colors. Since Android assumes
that the default color space is sRGB, all bitmaps and colors
are encoded with the sRGB Opto-Electronic Conversion Function
(OECF, which can be approximated with a power function). Since
the power curve is not linear, our linear math is incorrect.
The result is that we generate colors that tend to be too dark;
this affects blending but also anti-aliasing, gradients, blurs,
etc.
The solution is to convert gamma-encoded colors back to linear
space before doing any math on them, using the sRGB Electo-Optical
Conversion Function (EOCF). This is achieved in different
ways in different parts of the pipeline:
- Using hardware conversions when sampling from OpenGL textures
or writing into OpenGL frame buffers
- Using software conversion functions, to translate app-supplied
colors to and from sRGB
- Using Skia's color spaces
Any type of processing on colors must roughly ollow these steps:
[sRGB input]->EOCF->[linear data]->[processing]->OECF->[sRGB output]
For the sRGB color space, the conversion functions are defined as
follows:
OECF(linear) :=
linear <= 0.0031308 ? linear * 12.92 : (pow(linear, 1/2.4) * 1.055) - 0.055
EOCF(srgb) :=
srgb <= 0.04045 ? srgb / 12.92 : pow((srgb + 0.055) / 1.055, 2.4)
The EOCF is simply the reciprocal of the OECF.
While it is highly recommended to use the exact sRGB conversion
functions everywhere possible, it is sometimes useful or beneficial
to rely on approximations:
- pow(x,2.2) and pow(x,1/2.2)
- x^2 and sqrt(x)
The latter is particularly useful in fragment shaders (for instance
to apply dithering in sRGB space), especially if the sqrt() can be
replaced with an inversesqrt().
Here is a fairly exhaustive list of modifications implemented
in this CL:
- Set TARGET_ENABLE_LINEAR_BLENDING := false in BoardConfig.mk
to disable linear blending. This is only for GLES 2.0 GPUs
with no hardware sRGB support. This flag is currently assumed
to be false (see note above)
- sRGB writes are disabled when entering a functor (WebView).
This will need to be fixed at some point
- Skia bitmaps are created with the sRGB color space
- Bitmaps using a 565 config are expanded to 888
- Linear blending is disabled when entering a functor
- External textures are not properly sampled (see below)
- Gradients are interpolated in linear space
- Texture-based dithering was replaced with analytical dithering
- Dithering is done in the quantization color space, which is
why we must do EOCF(OECF(color)+dither)
- Text is now gamma corrected differently depending on the luminance
of the source pixel. The asumption is that a bright pixel will be
blended on a dark background and the other way around. The source
alpha is gamma corrected to thicken dark on bright and thin
bright on dark to match the intended design of fonts. This also
matches the behavior of popular design/drawing applications
- Removed the asset atlas. It did not contain anything useful and
could not be sampled in sRGB without a yet-to-be-defined GL
extension
- The last column of color matrices is converted to linear space
because its value are added to linear colors
Missing features:
- Resource qualifier?
- Regeneration of goldeng images for automated tests
- Handle alpha8/grey8 properly
- Disable sRGB write for layers with external textures
Test: Manual testing while work in progress
Bug: 29940137
Change-Id: I6a07b15ab49b554377cd33a36b6d9971a15e9a0b
996 lines
33 KiB
Java
996 lines
33 KiB
Java
/*
|
|
* 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.view.Gravity;
|
|
|
|
import com.android.internal.R;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
|
|
import java.io.IOException;
|
|
|
|
/**
|
|
* 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
|
|
|
|
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();
|
|
}
|
|
|
|
@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);
|
|
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) 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();
|
|
|
|
final int srcResId = a.getResourceId(R.styleable.BitmapDrawable_src, 0);
|
|
if (srcResId != 0) {
|
|
final Bitmap bitmap = BitmapFactory.decodeResource(r, srcResId);
|
|
if (bitmap == null) {
|
|
throw new XmlPullParserException(a.getPositionDescription() +
|
|
": <bitmap> requires a valid 'src' attribute");
|
|
}
|
|
|
|
state.mBitmap = bitmap;
|
|
}
|
|
|
|
state.mTargetDensity = r.getDisplayMetrics().densityDpi;
|
|
|
|
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));
|
|
}
|
|
|
|
state.mTargetDensity = Drawable.resolveDensity(r, 0);
|
|
}
|
|
|
|
@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);
|
|
} 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;
|
|
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;
|
|
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();
|
|
}
|
|
}
|