| /* |
| * 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.ColorInt; |
| import android.annotation.FloatRange; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.Px; |
| import android.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.BlendMode; |
| import android.graphics.BlendModeColorFilter; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.ColorFilter; |
| import android.graphics.DashPathEffect; |
| import android.graphics.Insets; |
| import android.graphics.LinearGradient; |
| import android.graphics.Outline; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PixelFormat; |
| import android.graphics.RadialGradient; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.Shader; |
| import android.graphics.SweepGradient; |
| import android.graphics.Xfermode; |
| import android.os.Build; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.util.TypedValue; |
| |
| import com.android.internal.R; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** |
| * A Drawable with a color gradient for buttons, backgrounds, etc. |
| * |
| * <p>It can be defined in an XML file with the <code><shape></code> element. For more |
| * information, see the guide to <a |
| * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p> |
| * |
| * @attr ref android.R.styleable#GradientDrawable_visible |
| * @attr ref android.R.styleable#GradientDrawable_shape |
| * @attr ref android.R.styleable#GradientDrawable_innerRadiusRatio |
| * @attr ref android.R.styleable#GradientDrawable_innerRadius |
| * @attr ref android.R.styleable#GradientDrawable_thicknessRatio |
| * @attr ref android.R.styleable#GradientDrawable_thickness |
| * @attr ref android.R.styleable#GradientDrawable_useLevel |
| * @attr ref android.R.styleable#GradientDrawableSize_width |
| * @attr ref android.R.styleable#GradientDrawableSize_height |
| * @attr ref android.R.styleable#GradientDrawableGradient_startColor |
| * @attr ref android.R.styleable#GradientDrawableGradient_centerColor |
| * @attr ref android.R.styleable#GradientDrawableGradient_endColor |
| * @attr ref android.R.styleable#GradientDrawableGradient_useLevel |
| * @attr ref android.R.styleable#GradientDrawableGradient_angle |
| * @attr ref android.R.styleable#GradientDrawableGradient_type |
| * @attr ref android.R.styleable#GradientDrawableGradient_centerX |
| * @attr ref android.R.styleable#GradientDrawableGradient_centerY |
| * @attr ref android.R.styleable#GradientDrawableGradient_gradientRadius |
| * @attr ref android.R.styleable#GradientDrawableSolid_color |
| * @attr ref android.R.styleable#GradientDrawableStroke_width |
| * @attr ref android.R.styleable#GradientDrawableStroke_color |
| * @attr ref android.R.styleable#GradientDrawableStroke_dashWidth |
| * @attr ref android.R.styleable#GradientDrawableStroke_dashGap |
| * @attr ref android.R.styleable#GradientDrawablePadding_left |
| * @attr ref android.R.styleable#GradientDrawablePadding_top |
| * @attr ref android.R.styleable#GradientDrawablePadding_right |
| * @attr ref android.R.styleable#GradientDrawablePadding_bottom |
| */ |
| public class GradientDrawable extends Drawable { |
| /** |
| * Shape is a rectangle, possibly with rounded corners |
| */ |
| public static final int RECTANGLE = 0; |
| |
| /** |
| * Shape is an ellipse |
| */ |
| public static final int OVAL = 1; |
| |
| /** |
| * Shape is a line |
| */ |
| public static final int LINE = 2; |
| |
| /** |
| * Shape is a ring. |
| */ |
| public static final int RING = 3; |
| |
| /** @hide */ |
| @IntDef({RECTANGLE, OVAL, LINE, RING}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface Shape {} |
| |
| /** |
| * Gradient is linear (default.) |
| */ |
| public static final int LINEAR_GRADIENT = 0; |
| |
| /** |
| * Gradient is circular. |
| */ |
| public static final int RADIAL_GRADIENT = 1; |
| |
| /** |
| * Gradient is a sweep. |
| */ |
| public static final int SWEEP_GRADIENT = 2; |
| |
| /** @hide */ |
| @IntDef({LINEAR_GRADIENT, RADIAL_GRADIENT, SWEEP_GRADIENT}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface GradientType {} |
| |
| /** Radius is in pixels. */ |
| private static final int RADIUS_TYPE_PIXELS = 0; |
| |
| /** Radius is a fraction of the base size. */ |
| private static final int RADIUS_TYPE_FRACTION = 1; |
| |
| /** Radius is a fraction of the bounds size. */ |
| private static final int RADIUS_TYPE_FRACTION_PARENT = 2; |
| |
| /** @hide */ |
| @IntDef({RADIUS_TYPE_PIXELS, RADIUS_TYPE_FRACTION, RADIUS_TYPE_FRACTION_PARENT}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface RadiusType {} |
| |
| private static final float DEFAULT_INNER_RADIUS_RATIO = 3.0f; |
| private static final float DEFAULT_THICKNESS_RATIO = 9.0f; |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| private GradientState mGradientState; |
| |
| @UnsupportedAppUsage |
| private final Paint mFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124051827) |
| private Rect mPadding; |
| @UnsupportedAppUsage |
| private Paint mStrokePaint; // optional, set by the caller |
| private ColorFilter mColorFilter; // optional, set by the caller |
| private BlendModeColorFilter mBlendModeColorFilter; |
| private int mAlpha = 0xFF; // modified by the caller |
| |
| private final Path mPath = new Path(); |
| private final RectF mRect = new RectF(); |
| |
| private Paint mLayerPaint; // internal, used if we use saveLayer() |
| private boolean mGradientIsDirty; |
| private boolean mMutated; |
| private Path mRingPath; |
| private boolean mPathIsDirty = true; |
| |
| /** Current gradient radius, valid when {@link #mGradientIsDirty} is false. */ |
| private float mGradientRadius; |
| |
| /** |
| * Controls how the gradient is oriented relative to the drawable's bounds |
| */ |
| public enum Orientation { |
| /** draw the gradient from the top to the bottom */ |
| TOP_BOTTOM, |
| /** draw the gradient from the top-right to the bottom-left */ |
| TR_BL, |
| /** draw the gradient from the right to the left */ |
| RIGHT_LEFT, |
| /** draw the gradient from the bottom-right to the top-left */ |
| BR_TL, |
| /** draw the gradient from the bottom to the top */ |
| BOTTOM_TOP, |
| /** draw the gradient from the bottom-left to the top-right */ |
| BL_TR, |
| /** draw the gradient from the left to the right */ |
| LEFT_RIGHT, |
| /** draw the gradient from the top-left to the bottom-right */ |
| TL_BR, |
| } |
| |
| public GradientDrawable() { |
| this(new GradientState(Orientation.TOP_BOTTOM, null), null); |
| } |
| |
| /** |
| * Create a new gradient drawable given an orientation and an array |
| * of colors for the gradient. |
| */ |
| public GradientDrawable(Orientation orientation, @ColorInt int[] colors) { |
| this(new GradientState(orientation, colors), null); |
| } |
| |
| @Override |
| public boolean getPadding(Rect padding) { |
| if (mPadding != null) { |
| padding.set(mPadding); |
| return true; |
| } else { |
| return super.getPadding(padding); |
| } |
| } |
| |
| /** |
| * Specifies radii for each of the 4 corners. For each corner, the array |
| * contains 2 values, <code>[X_radius, Y_radius]</code>. The corners are |
| * ordered top-left, top-right, bottom-right, bottom-left. This property |
| * is honored only when the shape is of type {@link #RECTANGLE}. |
| * <p> |
| * <strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property. |
| * |
| * @param radii an array of length >= 8 containing 4 pairs of X and Y |
| * radius for each corner, specified in pixels |
| * |
| * @see #mutate() |
| * @see #setShape(int) |
| * @see #setCornerRadius(float) |
| */ |
| public void setCornerRadii(@Nullable float[] radii) { |
| mGradientState.setCornerRadii(radii); |
| mPathIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Returns the radii for each of the 4 corners. For each corner, the array |
| * contains 2 values, <code>[X_radius, Y_radius]</code>. The corners are |
| * ordered top-left, top-right, bottom-right, bottom-left. |
| * <p> |
| * If the radius was previously set with {@link #setCornerRadius(float)}, |
| * or if the corners are not rounded, this method will return {@code null}. |
| * |
| * @return an array containing the radii for each of the 4 corners, or |
| * {@code null} |
| * @see #setCornerRadii(float[]) |
| */ |
| @Nullable |
| public float[] getCornerRadii() { |
| return mGradientState.mRadiusArray.clone(); |
| } |
| |
| /** |
| * Specifies the radius for the corners of the gradient. If this is > 0, |
| * then the drawable is drawn in a round-rectangle, rather than a |
| * rectangle. This property is honored only when the shape is of type |
| * {@link #RECTANGLE}. |
| * <p> |
| * <strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property. |
| * |
| * @param radius The radius in pixels of the corners of the rectangle shape |
| * |
| * @see #mutate() |
| * @see #setCornerRadii(float[]) |
| * @see #setShape(int) |
| */ |
| public void setCornerRadius(float radius) { |
| mGradientState.setCornerRadius(radius); |
| mPathIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Returns the radius for the corners of the gradient, that was previously set with |
| * {@link #setCornerRadius(float)}. |
| * <p> |
| * If the radius was previously cleared via passing {@code null} |
| * to {@link #setCornerRadii(float[])}, this method will return 0. |
| * |
| * @return the radius in pixels of the corners of the rectangle shape, or 0 |
| * @see #setCornerRadius |
| */ |
| public float getCornerRadius() { |
| return mGradientState.mRadius; |
| } |
| |
| /** |
| * <p>Set the stroke width and color for the drawable. If width is zero, |
| * then no stroke is drawn.</p> |
| * <p><strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property.</p> |
| * |
| * @param width The width in pixels of the stroke |
| * @param color The color of the stroke |
| * |
| * @see #mutate() |
| * @see #setStroke(int, int, float, float) |
| */ |
| public void setStroke(int width, @ColorInt int color) { |
| setStroke(width, color, 0, 0); |
| } |
| |
| /** |
| * <p>Set the stroke width and color state list for the drawable. If width |
| * is zero, then no stroke is drawn.</p> |
| * <p><strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property.</p> |
| * |
| * @param width The width in pixels of the stroke |
| * @param colorStateList The color state list of the stroke |
| * |
| * @see #mutate() |
| * @see #setStroke(int, ColorStateList, float, float) |
| */ |
| public void setStroke(int width, ColorStateList colorStateList) { |
| setStroke(width, colorStateList, 0, 0); |
| } |
| |
| /** |
| * <p>Set the stroke width and color for the drawable. If width is zero, |
| * then no stroke is drawn. This method can also be used to dash the stroke.</p> |
| * <p><strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property.</p> |
| * |
| * @param width The width in pixels of the stroke |
| * @param color The color of the stroke |
| * @param dashWidth The length in pixels of the dashes, set to 0 to disable dashes |
| * @param dashGap The gap in pixels between dashes |
| * |
| * @see #mutate() |
| * @see #setStroke(int, int) |
| */ |
| public void setStroke(int width, @ColorInt int color, float dashWidth, float dashGap) { |
| mGradientState.setStroke(width, ColorStateList.valueOf(color), dashWidth, dashGap); |
| setStrokeInternal(width, color, dashWidth, dashGap); |
| } |
| |
| /** |
| * <p>Set the stroke width and color state list for the drawable. If width |
| * is zero, then no stroke is drawn. This method can also be used to dash |
| * the stroke.</p> |
| * <p><strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property.</p> |
| * |
| * @param width The width in pixels of the stroke |
| * @param colorStateList The color state list of the stroke |
| * @param dashWidth The length in pixels of the dashes, set to 0 to disable dashes |
| * @param dashGap The gap in pixels between dashes |
| * |
| * @see #mutate() |
| * @see #setStroke(int, ColorStateList) |
| */ |
| public void setStroke( |
| int width, ColorStateList colorStateList, float dashWidth, float dashGap) { |
| mGradientState.setStroke(width, colorStateList, dashWidth, dashGap); |
| final int color; |
| if (colorStateList == null) { |
| color = Color.TRANSPARENT; |
| } else { |
| final int[] stateSet = getState(); |
| color = colorStateList.getColorForState(stateSet, 0); |
| } |
| setStrokeInternal(width, color, dashWidth, dashGap); |
| } |
| |
| private void setStrokeInternal(int width, int color, float dashWidth, float dashGap) { |
| if (mStrokePaint == null) { |
| mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mStrokePaint.setStyle(Paint.Style.STROKE); |
| } |
| mStrokePaint.setStrokeWidth(width); |
| mStrokePaint.setColor(color); |
| |
| DashPathEffect e = null; |
| if (dashWidth > 0) { |
| e = new DashPathEffect(new float[] { dashWidth, dashGap }, 0); |
| } |
| mStrokePaint.setPathEffect(e); |
| mGradientIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| |
| /** |
| * <p>Sets the size of the shape drawn by this drawable.</p> |
| * <p><strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property.</p> |
| * |
| * @param width The width of the shape used by this drawable |
| * @param height The height of the shape used by this drawable |
| * |
| * @see #mutate() |
| * @see #setGradientType(int) |
| */ |
| public void setSize(int width, int height) { |
| mGradientState.setSize(width, height); |
| mPathIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * <p>Sets the type of shape used to draw the gradient.</p> |
| * <p><strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property.</p> |
| * |
| * @param shape The desired shape for this drawable: {@link #LINE}, |
| * {@link #OVAL}, {@link #RECTANGLE} or {@link #RING} |
| * |
| * @see #mutate() |
| */ |
| public void setShape(@Shape int shape) { |
| mRingPath = null; |
| mPathIsDirty = true; |
| mGradientState.setShape(shape); |
| invalidateSelf(); |
| } |
| |
| /** |
| * Returns the type of shape used by this drawable, one of {@link #LINE}, |
| * {@link #OVAL}, {@link #RECTANGLE} or {@link #RING}. |
| * |
| * @return the type of shape used by this drawable |
| * @see #setShape(int) |
| */ |
| @Shape |
| public int getShape() { |
| return mGradientState.mShape; |
| } |
| |
| /** |
| * Sets the type of gradient used by this drawable. |
| * <p> |
| * <strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property. |
| * |
| * @param gradient The type of the gradient: {@link #LINEAR_GRADIENT}, |
| * {@link #RADIAL_GRADIENT} or {@link #SWEEP_GRADIENT} |
| * |
| * @see #mutate() |
| * @see #getGradientType() |
| */ |
| public void setGradientType(@GradientType int gradient) { |
| mGradientState.setGradientType(gradient); |
| mGradientIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Returns the type of gradient used by this drawable, one of |
| * {@link #LINEAR_GRADIENT}, {@link #RADIAL_GRADIENT}, or |
| * {@link #SWEEP_GRADIENT}. |
| * |
| * @return the type of gradient used by this drawable |
| * @see #setGradientType(int) |
| */ |
| @GradientType |
| public int getGradientType() { |
| return mGradientState.mGradient; |
| } |
| |
| /** |
| * Sets the position of the center of the gradient as a fraction of the |
| * width and height. |
| * <p> |
| * The default value is (0.5, 0.5). |
| * <p> |
| * <strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property. |
| * |
| * @param x the X-position of the center of the gradient |
| * @param y the Y-position of the center of the gradient |
| * |
| * @see #mutate() |
| * @see #setGradientType(int) |
| * @see #getGradientCenterX() |
| * @see #getGradientCenterY() |
| */ |
| public void setGradientCenter(float x, float y) { |
| mGradientState.setGradientCenter(x, y); |
| mGradientIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Returns the X-position of the center of the gradient as a fraction of |
| * the width. |
| * |
| * @return the X-position of the center of the gradient |
| * @see #setGradientCenter(float, float) |
| */ |
| public float getGradientCenterX() { |
| return mGradientState.mCenterX; |
| } |
| |
| /** |
| * Returns the Y-position of the center of this gradient as a fraction of |
| * the height. |
| * |
| * @return the Y-position of the center of the gradient |
| * @see #setGradientCenter(float, float) |
| */ |
| public float getGradientCenterY() { |
| return mGradientState.mCenterY; |
| } |
| |
| /** |
| * Sets the radius of the gradient. The radius is honored only when the |
| * gradient type is set to {@link #RADIAL_GRADIENT}. |
| * <p> |
| * <strong>Note</strong>: changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property. |
| * |
| * @param gradientRadius the radius of the gradient in pixels |
| * |
| * @see #mutate() |
| * @see #setGradientType(int) |
| * @see #getGradientRadius() |
| */ |
| public void setGradientRadius(float gradientRadius) { |
| mGradientState.setGradientRadius(gradientRadius, TypedValue.COMPLEX_UNIT_PX); |
| mGradientIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Returns the radius of the gradient in pixels. The radius is valid only |
| * when the gradient type is set to {@link #RADIAL_GRADIENT}. |
| * |
| * @return the radius of the gradient in pixels |
| * @see #setGradientRadius(float) |
| */ |
| public float getGradientRadius() { |
| if (mGradientState.mGradient != RADIAL_GRADIENT) { |
| return 0; |
| } |
| |
| ensureValidRect(); |
| return mGradientRadius; |
| } |
| |
| /** |
| * Sets whether this drawable's {@code level} property will be used to |
| * scale the gradient. If a gradient is not used, this property has no |
| * effect. |
| * <p> |
| * Scaling behavior varies based on gradient type: |
| * <ul> |
| * <li>{@link #LINEAR_GRADIENT} adjusts the ending position along the |
| * gradient's axis of orientation (see {@link #getOrientation()}) |
| * <li>{@link #RADIAL_GRADIENT} adjusts the outer radius |
| * <li>{@link #SWEEP_GRADIENT} adjusts the ending angle |
| * <ul> |
| * <p> |
| * The default value for this property is {@code false}. |
| * <p> |
| * <strong>Note</strong>: This property corresponds to the |
| * {@code android:useLevel} attribute on the inner {@code <gradient>} |
| * tag, NOT the {@code android:useLevel} attribute on the outer |
| * {@code <shape>} tag. For example, |
| * <pre>{@code |
| * <shape ...> |
| * <gradient |
| * ... |
| * android:useLevel="true" /> |
| * </shape> |
| * }</pre><p> |
| * <strong>Note</strong>: Changing this property will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing this property. |
| * |
| * @param useLevel {@code true} if the gradient should be scaled based on |
| * level, {@code false} otherwise |
| * |
| * @see #mutate() |
| * @see #setLevel(int) |
| * @see #getLevel() |
| * @see #getUseLevel() |
| * @attr ref android.R.styleable#GradientDrawableGradient_useLevel |
| */ |
| public void setUseLevel(boolean useLevel) { |
| mGradientState.mUseLevel = useLevel; |
| mGradientIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Returns whether this drawable's {@code level} property will be used to |
| * scale the gradient. |
| * |
| * @return {@code true} if the gradient should be scaled based on level, |
| * {@code false} otherwise |
| * @see #setUseLevel(boolean) |
| * @attr ref android.R.styleable#GradientDrawableGradient_useLevel |
| */ |
| public boolean getUseLevel() { |
| return mGradientState.mUseLevel; |
| } |
| |
| private int modulateAlpha(int alpha) { |
| int scale = mAlpha + (mAlpha >> 7); |
| return alpha * scale >> 8; |
| } |
| |
| /** |
| * Returns the orientation of the gradient defined in this drawable. |
| * |
| * @return the orientation of the gradient defined in this drawable |
| * @see #setOrientation(Orientation) |
| */ |
| public Orientation getOrientation() { |
| return mGradientState.getOrientation(); |
| } |
| |
| /** |
| * Sets the orientation of the gradient defined in this drawable. |
| * <p> |
| * <strong>Note</strong>: changing orientation will affect all instances |
| * of a drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing the orientation. |
| * |
| * @param orientation the desired orientation (angle) of the gradient |
| * |
| * @see #mutate() |
| * @see #getOrientation() |
| */ |
| public void setOrientation(Orientation orientation) { |
| mGradientState.setOrientation(orientation); |
| mGradientIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Sets the colors used to draw the gradient. |
| * <p> |
| * Each color is specified as an ARGB integer and the array must contain at |
| * least 2 colors. |
| * <p> |
| * <strong>Note</strong>: changing colors will affect all instances of a |
| * drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing the colors. |
| * |
| * @param colors an array containing 2 or more ARGB colors |
| * @see #mutate() |
| * @see #setColor(int) |
| */ |
| public void setColors(@Nullable @ColorInt int[] colors) { |
| setColors(colors, null); |
| } |
| |
| /** |
| * Sets the colors and offsets used to draw the gradient. |
| * <p> |
| * Each color is specified as an ARGB integer and the array must contain at |
| * least 2 colors. |
| * <p> |
| * <strong>Note</strong>: changing colors will affect all instances of a |
| * drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing the colors. |
| * |
| * @param colors an array containing 2 or more ARGB colors |
| * @param offsets optional array of floating point parameters representing the positions |
| * of the colors. Null evenly disperses the colors |
| * @see #mutate() |
| * @see #setColors(int[]) |
| */ |
| public void setColors(@Nullable @ColorInt int[] colors, @Nullable float[] offsets) { |
| mGradientState.setGradientColors(colors); |
| mGradientState.mPositions = offsets; |
| mGradientIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Returns the colors used to draw the gradient, or {@code null} if the |
| * gradient is drawn using a single color or no colors. |
| * |
| * @return the colors used to draw the gradient, or {@code null} |
| * @see #setColors(int[] colors) |
| */ |
| @Nullable |
| public int[] getColors() { |
| return mGradientState.mGradientColors == null ? |
| null : mGradientState.mGradientColors.clone(); |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| if (!ensureValidRect()) { |
| // nothing to draw |
| return; |
| } |
| |
| // remember the alpha values, in case we temporarily overwrite them |
| // when we modulate them with mAlpha |
| final int prevFillAlpha = mFillPaint.getAlpha(); |
| final int prevStrokeAlpha = mStrokePaint != null ? mStrokePaint.getAlpha() : 0; |
| // compute the modulate alpha values |
| final int currFillAlpha = modulateAlpha(prevFillAlpha); |
| final int currStrokeAlpha = modulateAlpha(prevStrokeAlpha); |
| |
| final boolean haveStroke = currStrokeAlpha > 0 && mStrokePaint != null && |
| mStrokePaint.getStrokeWidth() > 0; |
| final boolean haveFill = currFillAlpha > 0; |
| final GradientState st = mGradientState; |
| final ColorFilter colorFilter = mColorFilter != null ? mColorFilter : mBlendModeColorFilter; |
| |
| /* we need a layer iff we're drawing both a fill and stroke, and the |
| stroke is non-opaque, and our shapetype actually supports |
| fill+stroke. Otherwise we can just draw the stroke (if any) on top |
| of the fill (if any) without worrying about blending artifacts. |
| */ |
| final boolean useLayer = haveStroke && haveFill && st.mShape != LINE && |
| currStrokeAlpha < 255 && (mAlpha < 255 || colorFilter != null); |
| |
| /* Drawing with a layer is slower than direct drawing, but it |
| allows us to apply paint effects like alpha and colorfilter to |
| the result of multiple separate draws. In our case, if the user |
| asks for a non-opaque alpha value (via setAlpha), and we're |
| stroking, then we need to apply the alpha AFTER we've drawn |
| both the fill and the stroke. |
| */ |
| if (useLayer) { |
| if (mLayerPaint == null) { |
| mLayerPaint = new Paint(); |
| } |
| mLayerPaint.setDither(st.mDither); |
| mLayerPaint.setAlpha(mAlpha); |
| mLayerPaint.setColorFilter(colorFilter); |
| |
| float rad = mStrokePaint.getStrokeWidth(); |
| canvas.saveLayer(mRect.left - rad, mRect.top - rad, |
| mRect.right + rad, mRect.bottom + rad, |
| mLayerPaint); |
| |
| // don't perform the filter in our individual paints |
| // since the layer will do it for us |
| mFillPaint.setColorFilter(null); |
| mStrokePaint.setColorFilter(null); |
| } else { |
| /* if we're not using a layer, apply the dither/filter to our |
| individual paints |
| */ |
| mFillPaint.setAlpha(currFillAlpha); |
| mFillPaint.setDither(st.mDither); |
| mFillPaint.setColorFilter(colorFilter); |
| if (colorFilter != null && st.mSolidColors == null) { |
| mFillPaint.setColor(mAlpha << 24); |
| } |
| if (haveStroke) { |
| mStrokePaint.setAlpha(currStrokeAlpha); |
| mStrokePaint.setDither(st.mDither); |
| mStrokePaint.setColorFilter(colorFilter); |
| } |
| } |
| |
| switch (st.mShape) { |
| case RECTANGLE: |
| if (st.mRadiusArray != null) { |
| buildPathIfDirty(); |
| canvas.drawPath(mPath, mFillPaint); |
| if (haveStroke) { |
| canvas.drawPath(mPath, mStrokePaint); |
| } |
| } else if (st.mRadius > 0.0f) { |
| // since the caller is only giving us 1 value, we will force |
| // it to be square if the rect is too small in one dimension |
| // to show it. If we did nothing, Skia would clamp the rad |
| // independently along each axis, giving us a thin ellipse |
| // if the rect were very wide but not very tall |
| float rad = Math.min(st.mRadius, |
| Math.min(mRect.width(), mRect.height()) * 0.5f); |
| canvas.drawRoundRect(mRect, rad, rad, mFillPaint); |
| if (haveStroke) { |
| canvas.drawRoundRect(mRect, rad, rad, mStrokePaint); |
| } |
| } else { |
| if (mFillPaint.getColor() != 0 || colorFilter != null || |
| mFillPaint.getShader() != null) { |
| canvas.drawRect(mRect, mFillPaint); |
| } |
| if (haveStroke) { |
| canvas.drawRect(mRect, mStrokePaint); |
| } |
| } |
| break; |
| case OVAL: |
| canvas.drawOval(mRect, mFillPaint); |
| if (haveStroke) { |
| canvas.drawOval(mRect, mStrokePaint); |
| } |
| break; |
| case LINE: { |
| RectF r = mRect; |
| float y = r.centerY(); |
| if (haveStroke) { |
| canvas.drawLine(r.left, y, r.right, y, mStrokePaint); |
| } |
| break; |
| } |
| case RING: |
| Path path = buildRing(st); |
| canvas.drawPath(path, mFillPaint); |
| if (haveStroke) { |
| canvas.drawPath(path, mStrokePaint); |
| } |
| break; |
| } |
| |
| if (useLayer) { |
| canvas.restore(); |
| } else { |
| mFillPaint.setAlpha(prevFillAlpha); |
| if (haveStroke) { |
| mStrokePaint.setAlpha(prevStrokeAlpha); |
| } |
| } |
| } |
| |
| /** |
| * @param mode to draw this drawable with |
| * @hide |
| */ |
| @Override |
| public void setXfermode(@Nullable Xfermode mode) { |
| super.setXfermode(mode); |
| mFillPaint.setXfermode(mode); |
| } |
| |
| /** |
| * @param aa to draw this drawable with |
| * @hide |
| */ |
| public void setAntiAlias(boolean aa) { |
| mFillPaint.setAntiAlias(aa); |
| } |
| |
| private void buildPathIfDirty() { |
| final GradientState st = mGradientState; |
| if (mPathIsDirty) { |
| ensureValidRect(); |
| mPath.reset(); |
| mPath.addRoundRect(mRect, st.mRadiusArray, Path.Direction.CW); |
| mPathIsDirty = false; |
| } |
| } |
| |
| /** |
| * Inner radius of the ring expressed as a ratio of the ring's width. |
| * |
| * @see #getInnerRadiusRatio() |
| * @attr ref android.R.styleable#GradientDrawable_innerRadiusRatio |
| */ |
| public void setInnerRadiusRatio( |
| @FloatRange(from = 0.0f, fromInclusive = false) float innerRadiusRatio) { |
| if (innerRadiusRatio <= 0) { |
| throw new IllegalArgumentException("Ratio must be greater than zero"); |
| } |
| mGradientState.mInnerRadiusRatio = innerRadiusRatio; |
| mPathIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Return the inner radius of the ring expressed as a ratio of the ring's width. |
| * |
| * @see #setInnerRadiusRatio(float) |
| * @attr ref android.R.styleable#GradientDrawable_innerRadiusRatio |
| */ |
| public float getInnerRadiusRatio() { |
| return mGradientState.mInnerRadiusRatio; |
| } |
| |
| /** |
| * Configure the inner radius of the ring. |
| * |
| * @see #getInnerRadius() |
| * @attr ref android.R.styleable#GradientDrawable_innerRadius |
| */ |
| public void setInnerRadius(@Px int innerRadius) { |
| mGradientState.mInnerRadius = innerRadius; |
| mPathIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Retrn the inner radius of the ring |
| * |
| * @see #setInnerRadius(int) |
| * @attr ref android.R.styleable#GradientDrawable_innerRadius |
| */ |
| public @Px int getInnerRadius() { |
| return mGradientState.mInnerRadius; |
| } |
| |
| /** |
| * Configure the thickness of the ring expressed as a ratio of the ring's width. |
| * |
| * @see #getThicknessRatio() |
| * @attr ref android.R.styleable#GradientDrawable_thicknessRatio |
| */ |
| public void setThicknessRatio( |
| @FloatRange(from = 0.0f, fromInclusive = false) float thicknessRatio) { |
| if (thicknessRatio <= 0) { |
| throw new IllegalArgumentException("Ratio must be greater than zero"); |
| } |
| mGradientState.mThicknessRatio = thicknessRatio; |
| mPathIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Return the thickness ratio of the ring expressed as a ratio of the ring's width. |
| * |
| * @see #setThicknessRatio(float) |
| * @attr ref android.R.styleable#GradientDrawable_thicknessRatio |
| */ |
| public float getThicknessRatio() { |
| return mGradientState.mThicknessRatio; |
| } |
| |
| /** |
| * Configure the thickness of the ring. |
| * |
| * @attr ref android.R.styleable#GradientDrawable_thickness |
| */ |
| public void setThickness(@Px int thickness) { |
| mGradientState.mThickness = thickness; |
| mPathIsDirty = true; |
| invalidateSelf(); |
| } |
| |
| /** |
| * Return the thickness of the ring |
| * |
| * @see #setThickness(int) |
| * @attr ref android.R.styleable#GradientDrawable_thickness |
| */ |
| public @Px int getThickness() { |
| return mGradientState.mThickness; |
| } |
| |
| /** |
| * Configure the padding of the gradient shape |
| * @param left Left padding of the gradient shape |
| * @param top Top padding of the gradient shape |
| * @param right Right padding of the gradient shape |
| * @param bottom Bottom padding of the gradient shape |
| * |
| * @attr ref android.R.styleable#GradientDrawablePadding_left |
| * @attr ref android.R.styleable#GradientDrawablePadding_top |
| * @attr ref android.R.styleable#GradientDrawablePadding_right |
| * @attr ref android.R.styleable#GradientDrawablePadding_bottom |
| */ |
| public void setPadding(@Px int left, @Px int top, @Px int right, @Px int bottom) { |
| if (mGradientState.mPadding == null) { |
| mGradientState.mPadding = new Rect(); |
| } |
| |
| mGradientState.mPadding.set(left, top, right, bottom); |
| mPadding = mGradientState.mPadding; |
| invalidateSelf(); |
| } |
| |
| private Path buildRing(GradientState st) { |
| if (mRingPath != null && (!st.mUseLevelForShape || !mPathIsDirty)) return mRingPath; |
| mPathIsDirty = false; |
| |
| float sweep = st.mUseLevelForShape ? (360.0f * getLevel() / 10000.0f) : 360f; |
| |
| RectF bounds = new RectF(mRect); |
| |
| float x = bounds.width() / 2.0f; |
| float y = bounds.height() / 2.0f; |
| |
| float thickness = st.mThickness != -1 ? |
| st.mThickness : bounds.width() / st.mThicknessRatio; |
| // inner radius |
| float radius = st.mInnerRadius != -1 ? |
| st.mInnerRadius : bounds.width() / st.mInnerRadiusRatio; |
| |
| RectF innerBounds = new RectF(bounds); |
| innerBounds.inset(x - radius, y - radius); |
| |
| bounds = new RectF(innerBounds); |
| bounds.inset(-thickness, -thickness); |
| |
| if (mRingPath == null) { |
| mRingPath = new Path(); |
| } else { |
| mRingPath.reset(); |
| } |
| |
| final Path ringPath = mRingPath; |
| // arcTo treats the sweep angle mod 360, so check for that, since we |
| // think 360 means draw the entire oval |
| if (sweep < 360 && sweep > -360) { |
| ringPath.setFillType(Path.FillType.EVEN_ODD); |
| // inner top |
| ringPath.moveTo(x + radius, y); |
| // outer top |
| ringPath.lineTo(x + radius + thickness, y); |
| // outer arc |
| ringPath.arcTo(bounds, 0.0f, sweep, false); |
| // inner arc |
| ringPath.arcTo(innerBounds, sweep, -sweep, false); |
| ringPath.close(); |
| } else { |
| // add the entire ovals |
| ringPath.addOval(bounds, Path.Direction.CW); |
| ringPath.addOval(innerBounds, Path.Direction.CCW); |
| } |
| |
| return ringPath; |
| } |
| |
| /** |
| * Changes this drawable to use a single color instead of a gradient. |
| * <p> |
| * <strong>Note</strong>: changing color will affect all instances of a |
| * drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing the color. |
| * |
| * @param argb The color used to fill the shape |
| * |
| * @see #mutate() |
| * @see #setColors(int[]) |
| * @see #getColor |
| */ |
| public void setColor(@ColorInt int argb) { |
| mGradientState.setSolidColors(ColorStateList.valueOf(argb)); |
| mFillPaint.setColor(argb); |
| invalidateSelf(); |
| } |
| |
| /** |
| * Changes this drawable to use a single color state list instead of a |
| * gradient. Calling this method with a null argument will clear the color |
| * and is equivalent to calling {@link #setColor(int)} with the argument |
| * {@link Color#TRANSPARENT}. |
| * <p> |
| * <strong>Note</strong>: changing color will affect all instances of a |
| * drawable loaded from a resource. It is recommended to invoke |
| * {@link #mutate()} before changing the color.</p> |
| * |
| * @param colorStateList The color state list used to fill the shape |
| * |
| * @see #mutate() |
| * @see #getColor |
| */ |
| public void setColor(@Nullable ColorStateList colorStateList) { |
| if (colorStateList == null) { |
| setColor(Color.TRANSPARENT); |
| } else { |
| final int[] stateSet = getState(); |
| final int color = colorStateList.getColorForState(stateSet, 0); |
| mGradientState.setSolidColors(colorStateList); |
| mFillPaint.setColor(color); |
| invalidateSelf(); |
| } |
| } |
| |
| /** |
| * Returns the color state list used to fill the shape, or {@code null} if |
| * the shape is filled with a gradient or has no fill color. |
| * |
| * @return the color state list used to fill this gradient, or {@code null} |
| * |
| * @see #setColor(int) |
| * @see #setColor(ColorStateList) |
| */ |
| @Nullable |
| public ColorStateList getColor() { |
| return mGradientState.mSolidColors; |
| } |
| |
| @Override |
| protected boolean onStateChange(int[] stateSet) { |
| boolean invalidateSelf = false; |
| |
| final GradientState s = mGradientState; |
| final ColorStateList solidColors = s.mSolidColors; |
| if (solidColors != null) { |
| final int newColor = solidColors.getColorForState(stateSet, 0); |
| final int oldColor = mFillPaint.getColor(); |
| if (oldColor != newColor) { |
| mFillPaint.setColor(newColor); |
| invalidateSelf = true; |
| } |
| } |
| |
| final Paint strokePaint = mStrokePaint; |
| if (strokePaint != null) { |
| final ColorStateList strokeColors = s.mStrokeColors; |
| if (strokeColors != null) { |
| final int newColor = strokeColors.getColorForState(stateSet, 0); |
| final int oldColor = strokePaint.getColor(); |
| if (oldColor != newColor) { |
| strokePaint.setColor(newColor); |
| invalidateSelf = true; |
| } |
| } |
| } |
| |
| if (s.mTint != null && s.mBlendMode != null) { |
| mBlendModeColorFilter = updateBlendModeFilter(mBlendModeColorFilter, s.mTint, |
| s.mBlendMode); |
| invalidateSelf = true; |
| } |
| |
| if (invalidateSelf) { |
| invalidateSelf(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public boolean isStateful() { |
| final GradientState s = mGradientState; |
| return super.isStateful() |
| || (s.mSolidColors != null && s.mSolidColors.isStateful()) |
| || (s.mStrokeColors != null && s.mStrokeColors.isStateful()) |
| || (s.mTint != null && s.mTint.isStateful()); |
| } |
| |
| /** @hide */ |
| @Override |
| public boolean hasFocusStateSpecified() { |
| final GradientState s = mGradientState; |
| return (s.mSolidColors != null && s.mSolidColors.hasFocusStateSpecified()) |
| || (s.mStrokeColors != null && s.mStrokeColors.hasFocusStateSpecified()) |
| || (s.mTint != null && s.mTint.hasFocusStateSpecified()); |
| } |
| |
| @Override |
| public @Config int getChangingConfigurations() { |
| return super.getChangingConfigurations() | mGradientState.getChangingConfigurations(); |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| if (alpha != mAlpha) { |
| mAlpha = alpha; |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public int getAlpha() { |
| return mAlpha; |
| } |
| |
| @Override |
| public void setDither(boolean dither) { |
| if (dither != mGradientState.mDither) { |
| mGradientState.mDither = dither; |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| @Nullable |
| public ColorFilter getColorFilter() { |
| return mColorFilter; |
| } |
| |
| @Override |
| public void setColorFilter(@Nullable ColorFilter colorFilter) { |
| if (colorFilter != mColorFilter) { |
| mColorFilter = colorFilter; |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public void setTintList(@Nullable ColorStateList tint) { |
| mGradientState.mTint = tint; |
| mBlendModeColorFilter = |
| updateBlendModeFilter(mBlendModeColorFilter, tint, mGradientState.mBlendMode); |
| invalidateSelf(); |
| } |
| |
| @Override |
| public void setTintBlendMode(@NonNull BlendMode blendMode) { |
| mGradientState.mBlendMode = blendMode; |
| mBlendModeColorFilter = updateBlendModeFilter(mBlendModeColorFilter, mGradientState.mTint, |
| blendMode); |
| invalidateSelf(); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return (mAlpha == 255 && mGradientState.mOpaqueOverBounds && isOpaqueForState()) ? |
| PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT; |
| } |
| |
| @Override |
| protected void onBoundsChange(Rect r) { |
| super.onBoundsChange(r); |
| mRingPath = null; |
| mPathIsDirty = true; |
| mGradientIsDirty = true; |
| } |
| |
| @Override |
| protected boolean onLevelChange(int level) { |
| super.onLevelChange(level); |
| mGradientIsDirty = true; |
| mPathIsDirty = true; |
| invalidateSelf(); |
| return true; |
| } |
| |
| /** |
| * This checks mGradientIsDirty, and if it is true, recomputes both our drawing |
| * rectangle (mRect) and the gradient itself, since it depends on our |
| * rectangle too. |
| * @return true if the resulting rectangle is not empty, false otherwise |
| */ |
| private boolean ensureValidRect() { |
| if (mGradientIsDirty) { |
| mGradientIsDirty = false; |
| |
| Rect bounds = getBounds(); |
| float inset = 0; |
| |
| if (mStrokePaint != null) { |
| inset = mStrokePaint.getStrokeWidth() * 0.5f; |
| } |
| |
| final GradientState st = mGradientState; |
| |
| mRect.set(bounds.left + inset, bounds.top + inset, |
| bounds.right - inset, bounds.bottom - inset); |
| |
| final int[] gradientColors = st.mGradientColors; |
| if (gradientColors != null) { |
| final RectF r = mRect; |
| final float x0, x1, y0, y1; |
| |
| if (st.mGradient == LINEAR_GRADIENT) { |
| final float level = st.mUseLevel ? getLevel() / 10000.0f : 1.0f; |
| switch (st.getOrientation()) { |
| case TOP_BOTTOM: |
| x0 = r.left; y0 = r.top; |
| x1 = x0; y1 = level * r.bottom; |
| break; |
| case TR_BL: |
| x0 = r.right; y0 = r.top; |
| x1 = level * r.left; y1 = level * r.bottom; |
| break; |
| case RIGHT_LEFT: |
| x0 = r.right; y0 = r.top; |
| x1 = level * r.left; y1 = y0; |
| break; |
| case BR_TL: |
| x0 = r.right; y0 = r.bottom; |
| x1 = level * r.left; y1 = level * r.top; |
| break; |
| case BOTTOM_TOP: |
| x0 = r.left; y0 = r.bottom; |
| x1 = x0; y1 = level * r.top; |
| break; |
| case BL_TR: |
| x0 = r.left; y0 = r.bottom; |
| x1 = level * r.right; y1 = level * r.top; |
| break; |
| case LEFT_RIGHT: |
| x0 = r.left; y0 = r.top; |
| x1 = level * r.right; y1 = y0; |
| break; |
| default:/* TL_BR */ |
| x0 = r.left; y0 = r.top; |
| x1 = level * r.right; y1 = level * r.bottom; |
| break; |
| } |
| |
| mFillPaint.setShader(new LinearGradient(x0, y0, x1, y1, |
| gradientColors, st.mPositions, Shader.TileMode.CLAMP)); |
| } else if (st.mGradient == RADIAL_GRADIENT) { |
| x0 = r.left + (r.right - r.left) * st.mCenterX; |
| y0 = r.top + (r.bottom - r.top) * st.mCenterY; |
| |
| float radius = st.mGradientRadius; |
| if (st.mGradientRadiusType == RADIUS_TYPE_FRACTION) { |
| // Fall back to parent width or height if intrinsic |
| // size is not specified. |
| final float width = st.mWidth >= 0 ? st.mWidth : r.width(); |
| final float height = st.mHeight >= 0 ? st.mHeight : r.height(); |
| radius *= Math.min(width, height); |
| } else if (st.mGradientRadiusType == RADIUS_TYPE_FRACTION_PARENT) { |
| radius *= Math.min(r.width(), r.height()); |
| } |
| |
| if (st.mUseLevel) { |
| radius *= getLevel() / 10000.0f; |
| } |
| |
| mGradientRadius = radius; |
| |
| if (radius <= 0) { |
| // We can't have a shader with non-positive radius, so |
| // let's have a very, very small radius. |
| radius = 0.001f; |
| } |
| |
| mFillPaint.setShader(new RadialGradient( |
| x0, y0, radius, gradientColors, null, Shader.TileMode.CLAMP)); |
| } else if (st.mGradient == SWEEP_GRADIENT) { |
| x0 = r.left + (r.right - r.left) * st.mCenterX; |
| y0 = r.top + (r.bottom - r.top) * st.mCenterY; |
| |
| int[] tempColors = gradientColors; |
| float[] tempPositions = null; |
| |
| if (st.mUseLevel) { |
| tempColors = st.mTempColors; |
| final int length = gradientColors.length; |
| if (tempColors == null || tempColors.length != length + 1) { |
| tempColors = st.mTempColors = new int[length + 1]; |
| } |
| System.arraycopy(gradientColors, 0, tempColors, 0, length); |
| tempColors[length] = gradientColors[length - 1]; |
| |
| tempPositions = st.mTempPositions; |
| final float fraction = 1.0f / (length - 1); |
| if (tempPositions == null || tempPositions.length != length + 1) { |
| tempPositions = st.mTempPositions = new float[length + 1]; |
| } |
| |
| final float level = getLevel() / 10000.0f; |
| for (int i = 0; i < length; i++) { |
| tempPositions[i] = i * fraction * level; |
| } |
| tempPositions[length] = 1.0f; |
| |
| } |
| mFillPaint.setShader(new SweepGradient(x0, y0, tempColors, tempPositions)); |
| } |
| |
| // If we don't have a solid color, the alpha channel must be |
| // maxed out so that alpha modulation works correctly. |
| if (st.mSolidColors == null) { |
| mFillPaint.setColor(Color.BLACK); |
| } |
| } |
| } |
| return !mRect.isEmpty(); |
| } |
| |
| @Override |
| public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, |
| @NonNull AttributeSet attrs, @Nullable Theme theme) |
| throws XmlPullParserException, IOException { |
| super.inflate(r, parser, attrs, theme); |
| |
| mGradientState.setDensity(Drawable.resolveDensity(r, 0)); |
| |
| final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawable); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| |
| inflateChildElements(r, parser, attrs, theme); |
| |
| updateLocalState(r); |
| } |
| |
| @Override |
| public void applyTheme(@NonNull Theme t) { |
| super.applyTheme(t); |
| |
| final GradientState state = mGradientState; |
| if (state == null) { |
| return; |
| } |
| |
| state.setDensity(Drawable.resolveDensity(t.getResources(), 0)); |
| |
| if (state.mThemeAttrs != null) { |
| final TypedArray a = t.resolveAttributes( |
| state.mThemeAttrs, R.styleable.GradientDrawable); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| } |
| |
| if (state.mTint != null && state.mTint.canApplyTheme()) { |
| state.mTint = state.mTint.obtainForTheme(t); |
| } |
| |
| if (state.mSolidColors != null && state.mSolidColors.canApplyTheme()) { |
| state.mSolidColors = state.mSolidColors.obtainForTheme(t); |
| } |
| |
| if (state.mStrokeColors != null && state.mStrokeColors.canApplyTheme()) { |
| state.mStrokeColors = state.mStrokeColors.obtainForTheme(t); |
| } |
| |
| applyThemeChildElements(t); |
| |
| updateLocalState(t.getResources()); |
| } |
| |
| /** |
| * Updates the constant state from the values in the typed array. |
| */ |
| private void updateStateFromTypedArray(TypedArray a) { |
| final GradientState state = mGradientState; |
| |
| // Account for any configuration changes. |
| state.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| state.mThemeAttrs = a.extractThemeAttrs(); |
| |
| state.mShape = a.getInt(R.styleable.GradientDrawable_shape, state.mShape); |
| state.mDither = a.getBoolean(R.styleable.GradientDrawable_dither, state.mDither); |
| |
| if (state.mShape == RING) { |
| state.mInnerRadius = a.getDimensionPixelSize( |
| R.styleable.GradientDrawable_innerRadius, state.mInnerRadius); |
| |
| if (state.mInnerRadius == -1) { |
| state.mInnerRadiusRatio = a.getFloat( |
| R.styleable.GradientDrawable_innerRadiusRatio, state.mInnerRadiusRatio); |
| } |
| |
| state.mThickness = a.getDimensionPixelSize( |
| R.styleable.GradientDrawable_thickness, state.mThickness); |
| |
| if (state.mThickness == -1) { |
| state.mThicknessRatio = a.getFloat( |
| R.styleable.GradientDrawable_thicknessRatio, state.mThicknessRatio); |
| } |
| |
| state.mUseLevelForShape = a.getBoolean( |
| R.styleable.GradientDrawable_useLevel, state.mUseLevelForShape); |
| } |
| |
| final int tintMode = a.getInt(R.styleable.GradientDrawable_tintMode, -1); |
| if (tintMode != -1) { |
| state.mBlendMode = Drawable.parseBlendMode(tintMode, BlendMode.SRC_IN); |
| } |
| |
| final ColorStateList tint = a.getColorStateList(R.styleable.GradientDrawable_tint); |
| if (tint != null) { |
| state.mTint = tint; |
| } |
| |
| final int insetLeft = a.getDimensionPixelSize( |
| R.styleable.GradientDrawable_opticalInsetLeft, state.mOpticalInsets.left); |
| final int insetTop = a.getDimensionPixelSize( |
| R.styleable.GradientDrawable_opticalInsetTop, state.mOpticalInsets.top); |
| final int insetRight = a.getDimensionPixelSize( |
| R.styleable.GradientDrawable_opticalInsetRight, state.mOpticalInsets.right); |
| final int insetBottom = a.getDimensionPixelSize( |
| R.styleable.GradientDrawable_opticalInsetBottom, state.mOpticalInsets.bottom); |
| state.mOpticalInsets = Insets.of(insetLeft, insetTop, insetRight, insetBottom); |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return (mGradientState != null && mGradientState.canApplyTheme()) || super.canApplyTheme(); |
| } |
| |
| private void applyThemeChildElements(Theme t) { |
| final GradientState st = mGradientState; |
| |
| if (st.mAttrSize != null) { |
| final TypedArray a = t.resolveAttributes( |
| st.mAttrSize, R.styleable.GradientDrawableSize); |
| updateGradientDrawableSize(a); |
| a.recycle(); |
| } |
| |
| if (st.mAttrGradient != null) { |
| final TypedArray a = t.resolveAttributes( |
| st.mAttrGradient, R.styleable.GradientDrawableGradient); |
| try { |
| updateGradientDrawableGradient(t.getResources(), a); |
| } finally { |
| a.recycle(); |
| } |
| } |
| |
| if (st.mAttrSolid != null) { |
| final TypedArray a = t.resolveAttributes( |
| st.mAttrSolid, R.styleable.GradientDrawableSolid); |
| updateGradientDrawableSolid(a); |
| a.recycle(); |
| } |
| |
| if (st.mAttrStroke != null) { |
| final TypedArray a = t.resolveAttributes( |
| st.mAttrStroke, R.styleable.GradientDrawableStroke); |
| updateGradientDrawableStroke(a); |
| a.recycle(); |
| } |
| |
| if (st.mAttrCorners != null) { |
| final TypedArray a = t.resolveAttributes( |
| st.mAttrCorners, R.styleable.DrawableCorners); |
| updateDrawableCorners(a); |
| a.recycle(); |
| } |
| |
| if (st.mAttrPadding != null) { |
| final TypedArray a = t.resolveAttributes( |
| st.mAttrPadding, R.styleable.GradientDrawablePadding); |
| updateGradientDrawablePadding(a); |
| a.recycle(); |
| } |
| } |
| |
| private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, |
| Theme theme) throws XmlPullParserException, IOException { |
| TypedArray a; |
| int type; |
| |
| final int innerDepth = parser.getDepth() + 1; |
| int depth; |
| while ((type=parser.next()) != XmlPullParser.END_DOCUMENT |
| && ((depth=parser.getDepth()) >= innerDepth |
| || type != XmlPullParser.END_TAG)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| if (depth > innerDepth) { |
| continue; |
| } |
| |
| String name = parser.getName(); |
| |
| if (name.equals("size")) { |
| a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableSize); |
| updateGradientDrawableSize(a); |
| a.recycle(); |
| } else if (name.equals("gradient")) { |
| a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableGradient); |
| updateGradientDrawableGradient(r, a); |
| a.recycle(); |
| } else if (name.equals("solid")) { |
| a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableSolid); |
| updateGradientDrawableSolid(a); |
| a.recycle(); |
| } else if (name.equals("stroke")) { |
| a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawableStroke); |
| updateGradientDrawableStroke(a); |
| a.recycle(); |
| } else if (name.equals("corners")) { |
| a = obtainAttributes(r, theme, attrs, R.styleable.DrawableCorners); |
| updateDrawableCorners(a); |
| a.recycle(); |
| } else if (name.equals("padding")) { |
| a = obtainAttributes(r, theme, attrs, R.styleable.GradientDrawablePadding); |
| updateGradientDrawablePadding(a); |
| a.recycle(); |
| } else { |
| Log.w("drawable", "Bad element under <shape>: " + name); |
| } |
| } |
| } |
| |
| private void updateGradientDrawablePadding(TypedArray a) { |
| final GradientState st = mGradientState; |
| |
| // Account for any configuration changes. |
| st.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| st.mAttrPadding = a.extractThemeAttrs(); |
| |
| if (st.mPadding == null) { |
| st.mPadding = new Rect(); |
| } |
| |
| final Rect pad = st.mPadding; |
| pad.set(a.getDimensionPixelOffset(R.styleable.GradientDrawablePadding_left, pad.left), |
| a.getDimensionPixelOffset(R.styleable.GradientDrawablePadding_top, pad.top), |
| a.getDimensionPixelOffset(R.styleable.GradientDrawablePadding_right, pad.right), |
| a.getDimensionPixelOffset(R.styleable.GradientDrawablePadding_bottom, pad.bottom)); |
| mPadding = pad; |
| } |
| |
| private void updateDrawableCorners(TypedArray a) { |
| final GradientState st = mGradientState; |
| |
| // Account for any configuration changes. |
| st.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| st.mAttrCorners = a.extractThemeAttrs(); |
| |
| final int radius = a.getDimensionPixelSize( |
| R.styleable.DrawableCorners_radius, (int) st.mRadius); |
| setCornerRadius(radius); |
| |
| // TODO: Update these to be themeable. |
| final int topLeftRadius = a.getDimensionPixelSize( |
| R.styleable.DrawableCorners_topLeftRadius, radius); |
| final int topRightRadius = a.getDimensionPixelSize( |
| R.styleable.DrawableCorners_topRightRadius, radius); |
| final int bottomLeftRadius = a.getDimensionPixelSize( |
| R.styleable.DrawableCorners_bottomLeftRadius, radius); |
| final int bottomRightRadius = a.getDimensionPixelSize( |
| R.styleable.DrawableCorners_bottomRightRadius, radius); |
| if (topLeftRadius != radius || topRightRadius != radius || |
| bottomLeftRadius != radius || bottomRightRadius != radius) { |
| // The corner radii are specified in clockwise order (see Path.addRoundRect()) |
| setCornerRadii(new float[] { |
| topLeftRadius, topLeftRadius, |
| topRightRadius, topRightRadius, |
| bottomRightRadius, bottomRightRadius, |
| bottomLeftRadius, bottomLeftRadius |
| }); |
| } |
| } |
| |
| private void updateGradientDrawableStroke(TypedArray a) { |
| final GradientState st = mGradientState; |
| |
| // Account for any configuration changes. |
| st.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| st.mAttrStroke = a.extractThemeAttrs(); |
| |
| // We have an explicit stroke defined, so the default stroke width |
| // must be at least 0 or the current stroke width. |
| final int defaultStrokeWidth = Math.max(0, st.mStrokeWidth); |
| final int width = a.getDimensionPixelSize( |
| R.styleable.GradientDrawableStroke_width, defaultStrokeWidth); |
| final float dashWidth = a.getDimension( |
| R.styleable.GradientDrawableStroke_dashWidth, st.mStrokeDashWidth); |
| |
| ColorStateList colorStateList = a.getColorStateList( |
| R.styleable.GradientDrawableStroke_color); |
| if (colorStateList == null) { |
| colorStateList = st.mStrokeColors; |
| } |
| |
| if (dashWidth != 0.0f) { |
| final float dashGap = a.getDimension( |
| R.styleable.GradientDrawableStroke_dashGap, st.mStrokeDashGap); |
| setStroke(width, colorStateList, dashWidth, dashGap); |
| } else { |
| setStroke(width, colorStateList); |
| } |
| } |
| |
| private void updateGradientDrawableSolid(TypedArray a) { |
| final GradientState st = mGradientState; |
| |
| // Account for any configuration changes. |
| st.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| st.mAttrSolid = a.extractThemeAttrs(); |
| |
| final ColorStateList colorStateList = a.getColorStateList( |
| R.styleable.GradientDrawableSolid_color); |
| if (colorStateList != null) { |
| setColor(colorStateList); |
| } |
| } |
| |
| private void updateGradientDrawableGradient(Resources r, TypedArray a) { |
| final GradientState st = mGradientState; |
| |
| // Account for any configuration changes. |
| st.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| st.mAttrGradient = a.extractThemeAttrs(); |
| |
| st.mCenterX = getFloatOrFraction( |
| a, R.styleable.GradientDrawableGradient_centerX, st.mCenterX); |
| st.mCenterY = getFloatOrFraction( |
| a, R.styleable.GradientDrawableGradient_centerY, st.mCenterY); |
| st.mUseLevel = a.getBoolean( |
| R.styleable.GradientDrawableGradient_useLevel, st.mUseLevel); |
| st.mGradient = a.getInt( |
| R.styleable.GradientDrawableGradient_type, st.mGradient); |
| |
| final boolean hasGradientColors = st.mGradientColors != null; |
| final boolean hasGradientCenter = st.hasCenterColor(); |
| final int prevStart = hasGradientColors ? st.mGradientColors[0] : 0; |
| final int prevCenter = hasGradientCenter ? st.mGradientColors[1] : 0; |
| final int prevEnd; |
| |
| if (st.hasCenterColor()) { |
| // if there is a center color, the end color is the last of the 3 values |
| prevEnd = st.mGradientColors[2]; |
| } else if (hasGradientColors) { |
| // if there is not a center color but there are already colors configured, then |
| // the end color is the 2nd value in the array |
| prevEnd = st.mGradientColors[1]; |
| } else { |
| // otherwise, there isn't a previously configured end color |
| prevEnd = 0; |
| } |
| |
| final int startColor = a.getColor( |
| R.styleable.GradientDrawableGradient_startColor, prevStart); |
| final boolean hasCenterColor = a.hasValue( |
| R.styleable.GradientDrawableGradient_centerColor) || hasGradientCenter; |
| final int centerColor = a.getColor( |
| R.styleable.GradientDrawableGradient_centerColor, prevCenter); |
| final int endColor = a.getColor( |
| R.styleable.GradientDrawableGradient_endColor, prevEnd); |
| |
| if (hasCenterColor) { |
| st.mGradientColors = new int[3]; |
| st.mGradientColors[0] = startColor; |
| st.mGradientColors[1] = centerColor; |
| st.mGradientColors[2] = endColor; |
| |
| st.mPositions = new float[3]; |
| st.mPositions[0] = 0.0f; |
| // Since 0.5f is default value, try to take the one that isn't 0.5f |
| st.mPositions[1] = st.mCenterX != 0.5f ? st.mCenterX : st.mCenterY; |
| st.mPositions[2] = 1f; |
| } else { |
| st.mGradientColors = new int[2]; |
| st.mGradientColors[0] = startColor; |
| st.mGradientColors[1] = endColor; |
| } |
| |
| int angle = (int) a.getFloat(R.styleable.GradientDrawableGradient_angle, st.mAngle); |
| st.mAngle = angle % 360; |
| |
| final TypedValue tv = a.peekValue(R.styleable.GradientDrawableGradient_gradientRadius); |
| if (tv != null) { |
| final float radius; |
| final @RadiusType int radiusType; |
| if (tv.type == TypedValue.TYPE_FRACTION) { |
| radius = tv.getFraction(1.0f, 1.0f); |
| |
| final int unit = (tv.data >> TypedValue.COMPLEX_UNIT_SHIFT) |
| & TypedValue.COMPLEX_UNIT_MASK; |
| if (unit == TypedValue.COMPLEX_UNIT_FRACTION_PARENT) { |
| radiusType = RADIUS_TYPE_FRACTION_PARENT; |
| } else { |
| radiusType = RADIUS_TYPE_FRACTION; |
| } |
| } else if (tv.type == TypedValue.TYPE_DIMENSION) { |
| radius = tv.getDimension(r.getDisplayMetrics()); |
| radiusType = RADIUS_TYPE_PIXELS; |
| } else { |
| radius = tv.getFloat(); |
| radiusType = RADIUS_TYPE_PIXELS; |
| } |
| |
| st.mGradientRadius = radius; |
| st.mGradientRadiusType = radiusType; |
| } |
| } |
| |
| private void updateGradientDrawableSize(TypedArray a) { |
| final GradientState st = mGradientState; |
| |
| // Account for any configuration changes. |
| st.mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| st.mAttrSize = a.extractThemeAttrs(); |
| |
| st.mWidth = a.getDimensionPixelSize(R.styleable.GradientDrawableSize_width, st.mWidth); |
| st.mHeight = a.getDimensionPixelSize(R.styleable.GradientDrawableSize_height, st.mHeight); |
| } |
| |
| private static float getFloatOrFraction(TypedArray a, int index, float defaultValue) { |
| TypedValue tv = a.peekValue(index); |
| float v = defaultValue; |
| if (tv != null) { |
| boolean vIsFraction = tv.type == TypedValue.TYPE_FRACTION; |
| v = vIsFraction ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); |
| } |
| return v; |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return mGradientState.mWidth; |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return mGradientState.mHeight; |
| } |
| |
| @Override |
| public Insets getOpticalInsets() { |
| return mGradientState.mOpticalInsets; |
| } |
| |
| @Override |
| public ConstantState getConstantState() { |
| mGradientState.mChangingConfigurations = getChangingConfigurations(); |
| return mGradientState; |
| } |
| |
| private boolean isOpaqueForState() { |
| if (mGradientState.mStrokeWidth >= 0 && mStrokePaint != null |
| && !isOpaque(mStrokePaint.getColor())) { |
| return false; |
| } |
| |
| // Don't check opacity if we're using a gradient, as we've already |
| // checked the gradient opacity in mOpaqueOverShape. |
| if (mGradientState.mGradientColors == null && !isOpaque(mFillPaint.getColor())) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public void getOutline(Outline outline) { |
| final GradientState st = mGradientState; |
| final Rect bounds = getBounds(); |
| // only report non-zero alpha if shape being drawn has consistent opacity over shape. Must |
| // either not have a stroke, or have same stroke/fill opacity |
| boolean useFillOpacity = st.mOpaqueOverShape && (mGradientState.mStrokeWidth <= 0 |
| || mStrokePaint == null |
| || mStrokePaint.getAlpha() == mFillPaint.getAlpha()); |
| outline.setAlpha(useFillOpacity |
| ? modulateAlpha(mFillPaint.getAlpha()) / 255.0f |
| : 0.0f); |
| |
| switch (st.mShape) { |
| case RECTANGLE: |
| if (st.mRadiusArray != null) { |
| buildPathIfDirty(); |
| outline.setConvexPath(mPath); |
| return; |
| } |
| |
| float rad = 0; |
| if (st.mRadius > 0.0f) { |
| // clamp the radius based on width & height, matching behavior in draw() |
| rad = Math.min(st.mRadius, |
| Math.min(bounds.width(), bounds.height()) * 0.5f); |
| } |
| outline.setRoundRect(bounds, rad); |
| return; |
| case OVAL: |
| outline.setOval(bounds); |
| return; |
| case LINE: |
| // Hairlines (0-width stroke) must have a non-empty outline for |
| // shadows to draw correctly, so we'll use a very small width. |
| final float halfStrokeWidth = mStrokePaint == null ? |
| 0.0001f : mStrokePaint.getStrokeWidth() * 0.5f; |
| final float centerY = bounds.centerY(); |
| final int top = (int) Math.floor(centerY - halfStrokeWidth); |
| final int bottom = (int) Math.ceil(centerY + halfStrokeWidth); |
| |
| outline.setRect(bounds.left, top, bounds.right, bottom); |
| return; |
| default: |
| // TODO: support more complex shapes |
| } |
| } |
| |
| @Override |
| public Drawable mutate() { |
| if (!mMutated && super.mutate() == this) { |
| mGradientState = new GradientState(mGradientState, null); |
| updateLocalState(null); |
| mMutated = true; |
| } |
| return this; |
| } |
| |
| /** |
| * @hide |
| */ |
| public void clearMutated() { |
| super.clearMutated(); |
| mMutated = false; |
| } |
| |
| final static class GradientState extends ConstantState { |
| public @Config int mChangingConfigurations; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public @Shape int mShape = RECTANGLE; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public @GradientType int mGradient = LINEAR_GRADIENT; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public int mAngle = 0; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public Orientation mOrientation; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public ColorStateList mSolidColors; |
| public ColorStateList mStrokeColors; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public @ColorInt int[] mGradientColors; |
| public @ColorInt int[] mTempColors; // no need to copy |
| public float[] mTempPositions; // no need to copy |
| @UnsupportedAppUsage |
| public float[] mPositions; |
| @UnsupportedAppUsage(trackingBug = 124050917) |
| public int mStrokeWidth = -1; // if >= 0 use stroking. |
| @UnsupportedAppUsage(trackingBug = 124050917) |
| public float mStrokeDashWidth = 0.0f; |
| @UnsupportedAppUsage(trackingBug = 124050917) |
| public float mStrokeDashGap = 0.0f; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public float mRadius = 0.0f; // use this if mRadiusArray is null |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public float[] mRadiusArray = null; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public Rect mPadding = null; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public int mWidth = -1; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public int mHeight = -1; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public float mInnerRadiusRatio = DEFAULT_INNER_RADIUS_RATIO; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050218) |
| public float mThicknessRatio = DEFAULT_THICKNESS_RATIO; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050917) |
| public int mInnerRadius = -1; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124050218) |
| public int mThickness = -1; |
| public boolean mDither = false; |
| public Insets mOpticalInsets = Insets.NONE; |
| |
| float mCenterX = 0.5f; |
| float mCenterY = 0.5f; |
| float mGradientRadius = 0.5f; |
| @RadiusType int mGradientRadiusType = RADIUS_TYPE_PIXELS; |
| boolean mUseLevel = false; |
| boolean mUseLevelForShape = true; |
| |
| boolean mOpaqueOverBounds; |
| boolean mOpaqueOverShape; |
| |
| ColorStateList mTint = null; |
| BlendMode mBlendMode = DEFAULT_BLEND_MODE; |
| |
| int mDensity = DisplayMetrics.DENSITY_DEFAULT; |
| |
| int[] mThemeAttrs; |
| int[] mAttrSize; |
| int[] mAttrGradient; |
| int[] mAttrSolid; |
| int[] mAttrStroke; |
| int[] mAttrCorners; |
| int[] mAttrPadding; |
| |
| public GradientState(Orientation orientation, int[] gradientColors) { |
| setOrientation(orientation); |
| setGradientColors(gradientColors); |
| } |
| |
| public GradientState(@NonNull GradientState orig, @Nullable Resources res) { |
| mChangingConfigurations = orig.mChangingConfigurations; |
| mShape = orig.mShape; |
| mGradient = orig.mGradient; |
| mAngle = orig.mAngle; |
| mOrientation = orig.mOrientation; |
| mSolidColors = orig.mSolidColors; |
| if (orig.mGradientColors != null) { |
| mGradientColors = orig.mGradientColors.clone(); |
| } |
| if (orig.mPositions != null) { |
| mPositions = orig.mPositions.clone(); |
| } |
| mStrokeColors = orig.mStrokeColors; |
| mStrokeWidth = orig.mStrokeWidth; |
| mStrokeDashWidth = orig.mStrokeDashWidth; |
| mStrokeDashGap = orig.mStrokeDashGap; |
| mRadius = orig.mRadius; |
| if (orig.mRadiusArray != null) { |
| mRadiusArray = orig.mRadiusArray.clone(); |
| } |
| if (orig.mPadding != null) { |
| mPadding = new Rect(orig.mPadding); |
| } |
| mWidth = orig.mWidth; |
| mHeight = orig.mHeight; |
| mInnerRadiusRatio = orig.mInnerRadiusRatio; |
| mThicknessRatio = orig.mThicknessRatio; |
| mInnerRadius = orig.mInnerRadius; |
| mThickness = orig.mThickness; |
| mDither = orig.mDither; |
| mOpticalInsets = orig.mOpticalInsets; |
| mCenterX = orig.mCenterX; |
| mCenterY = orig.mCenterY; |
| mGradientRadius = orig.mGradientRadius; |
| mGradientRadiusType = orig.mGradientRadiusType; |
| mUseLevel = orig.mUseLevel; |
| mUseLevelForShape = orig.mUseLevelForShape; |
| mOpaqueOverBounds = orig.mOpaqueOverBounds; |
| mOpaqueOverShape = orig.mOpaqueOverShape; |
| mTint = orig.mTint; |
| mBlendMode = orig.mBlendMode; |
| mThemeAttrs = orig.mThemeAttrs; |
| mAttrSize = orig.mAttrSize; |
| mAttrGradient = orig.mAttrGradient; |
| mAttrSolid = orig.mAttrSolid; |
| mAttrStroke = orig.mAttrStroke; |
| mAttrCorners = orig.mAttrCorners; |
| mAttrPadding = orig.mAttrPadding; |
| |
| mDensity = Drawable.resolveDensity(res, orig.mDensity); |
| if (orig.mDensity != mDensity) { |
| applyDensityScaling(orig.mDensity, mDensity); |
| } |
| } |
| |
| /** |
| * Sets the constant state density. |
| * <p> |
| * If the density has been previously set, dispatches the change to |
| * subclasses so that density-dependent properties may be scaled as |
| * necessary. |
| * |
| * @param targetDensity the new constant state density |
| */ |
| public final void setDensity(int targetDensity) { |
| if (mDensity != targetDensity) { |
| final int sourceDensity = mDensity; |
| mDensity = targetDensity; |
| |
| applyDensityScaling(sourceDensity, targetDensity); |
| } |
| } |
| |
| public boolean hasCenterColor() { |
| return mGradientColors != null && mGradientColors.length == 3; |
| } |
| |
| private void applyDensityScaling(int sourceDensity, int targetDensity) { |
| if (mInnerRadius > 0) { |
| mInnerRadius = Drawable.scaleFromDensity( |
| mInnerRadius, sourceDensity, targetDensity, true); |
| } |
| if (mThickness > 0) { |
| mThickness = Drawable.scaleFromDensity( |
| mThickness, sourceDensity, targetDensity, true); |
| } |
| if (mOpticalInsets != Insets.NONE) { |
| final int left = Drawable.scaleFromDensity( |
| mOpticalInsets.left, sourceDensity, targetDensity, true); |
| final int top = Drawable.scaleFromDensity( |
| mOpticalInsets.top, sourceDensity, targetDensity, true); |
| final int right = Drawable.scaleFromDensity( |
| mOpticalInsets.right, sourceDensity, targetDensity, true); |
| final int bottom = Drawable.scaleFromDensity( |
| mOpticalInsets.bottom, sourceDensity, targetDensity, true); |
| mOpticalInsets = Insets.of(left, top, right, bottom); |
| } |
| if (mPadding != null) { |
| mPadding.left = Drawable.scaleFromDensity( |
| mPadding.left, sourceDensity, targetDensity, false); |
| mPadding.top = Drawable.scaleFromDensity( |
| mPadding.top, sourceDensity, targetDensity, false); |
| mPadding.right = Drawable.scaleFromDensity( |
| mPadding.right, sourceDensity, targetDensity, false); |
| mPadding.bottom = Drawable.scaleFromDensity( |
| mPadding.bottom, sourceDensity, targetDensity, false); |
| } |
| if (mRadius > 0) { |
| mRadius = Drawable.scaleFromDensity(mRadius, sourceDensity, targetDensity); |
| } |
| if (mRadiusArray != null) { |
| mRadiusArray[0] = Drawable.scaleFromDensity( |
| (int) mRadiusArray[0], sourceDensity, targetDensity, true); |
| mRadiusArray[1] = Drawable.scaleFromDensity( |
| (int) mRadiusArray[1], sourceDensity, targetDensity, true); |
| mRadiusArray[2] = Drawable.scaleFromDensity( |
| (int) mRadiusArray[2], sourceDensity, targetDensity, true); |
| mRadiusArray[3] = Drawable.scaleFromDensity( |
| (int) mRadiusArray[3], sourceDensity, targetDensity, true); |
| } |
| if (mStrokeWidth > 0) { |
| mStrokeWidth = Drawable.scaleFromDensity( |
| mStrokeWidth, sourceDensity, targetDensity, true); |
| } |
| if (mStrokeDashWidth > 0) { |
| mStrokeDashWidth = Drawable.scaleFromDensity( |
| mStrokeDashGap, sourceDensity, targetDensity); |
| } |
| if (mStrokeDashGap > 0) { |
| mStrokeDashGap = Drawable.scaleFromDensity( |
| mStrokeDashGap, sourceDensity, targetDensity); |
| } |
| if (mGradientRadiusType == RADIUS_TYPE_PIXELS) { |
| mGradientRadius = Drawable.scaleFromDensity( |
| mGradientRadius, sourceDensity, targetDensity); |
| } |
| if (mWidth > 0) { |
| mWidth = Drawable.scaleFromDensity(mWidth, sourceDensity, targetDensity, true); |
| } |
| if (mHeight > 0) { |
| mHeight = Drawable.scaleFromDensity(mHeight, sourceDensity, targetDensity, true); |
| } |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return mThemeAttrs != null |
| || mAttrSize != null || mAttrGradient != null |
| || mAttrSolid != null || mAttrStroke != null |
| || mAttrCorners != null || mAttrPadding != null |
| || (mTint != null && mTint.canApplyTheme()) |
| || (mStrokeColors != null && mStrokeColors.canApplyTheme()) |
| || (mSolidColors != null && mSolidColors.canApplyTheme()) |
| || super.canApplyTheme(); |
| } |
| |
| @Override |
| public Drawable newDrawable() { |
| return new GradientDrawable(this, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(@Nullable Resources res) { |
| // If this drawable is being created for a different density, |
| // just create a new constant state and call it a day. |
| final GradientState state; |
| final int density = Drawable.resolveDensity(res, mDensity); |
| if (density != mDensity) { |
| state = new GradientState(this, res); |
| } else { |
| state = this; |
| } |
| |
| return new GradientDrawable(state, res); |
| } |
| |
| @Override |
| public @Config int getChangingConfigurations() { |
| return mChangingConfigurations |
| | (mStrokeColors != null ? mStrokeColors.getChangingConfigurations() : 0) |
| | (mSolidColors != null ? mSolidColors.getChangingConfigurations() : 0) |
| | (mTint != null ? mTint.getChangingConfigurations() : 0); |
| } |
| |
| public void setShape(@Shape int shape) { |
| mShape = shape; |
| computeOpacity(); |
| } |
| |
| public void setGradientType(@GradientType int gradient) { |
| mGradient = gradient; |
| } |
| |
| public void setGradientCenter(float x, float y) { |
| mCenterX = x; |
| mCenterY = y; |
| } |
| |
| public void setOrientation(Orientation orientation) { |
| // Update the angle here so that subsequent attempts to obtain the orientation |
| // from the angle overwrite previously configured values during inflation |
| mAngle = getAngleFromOrientation(orientation); |
| mOrientation = orientation; |
| } |
| |
| @NonNull |
| public Orientation getOrientation() { |
| updateGradientStateOrientation(); |
| return mOrientation; |
| } |
| |
| /** |
| * Update the orientation of the gradient based on the given angle only if the type is |
| * {@link #LINEAR_GRADIENT} |
| */ |
| private void updateGradientStateOrientation() { |
| if (mGradient == LINEAR_GRADIENT) { |
| int angle = mAngle; |
| if (angle % 45 != 0) { |
| throw new IllegalArgumentException("Linear gradient requires 'angle' attribute " |
| + "to be a multiple of 45"); |
| } |
| |
| Orientation orientation; |
| switch (angle) { |
| case 0: |
| orientation = Orientation.LEFT_RIGHT; |
| break; |
| case 45: |
| orientation = Orientation.BL_TR; |
| break; |
| case 90: |
| orientation = Orientation.BOTTOM_TOP; |
| break; |
| case 135: |
| orientation = Orientation.BR_TL; |
| break; |
| case 180: |
| orientation = Orientation.RIGHT_LEFT; |
| break; |
| case 225: |
| orientation = Orientation.TR_BL; |
| break; |
| case 270: |
| orientation = Orientation.TOP_BOTTOM; |
| break; |
| case 315: |
| orientation = Orientation.TL_BR; |
| break; |
| default: |
| // Should not get here as exception is thrown above if angle is not multiple |
| // of 45 degrees |
| orientation = Orientation.LEFT_RIGHT; |
| break; |
| } |
| mOrientation = orientation; |
| } |
| } |
| |
| private int getAngleFromOrientation(@Nullable Orientation orientation) { |
| if (orientation != null) { |
| switch (orientation) { |
| default: |
| case LEFT_RIGHT: |
| return 0; |
| case BL_TR: |
| return 45; |
| case BOTTOM_TOP: |
| return 90; |
| case BR_TL: |
| return 135; |
| case RIGHT_LEFT: |
| return 180; |
| case TR_BL: |
| return 225; |
| case TOP_BOTTOM: |
| return 270; |
| case TL_BR: |
| return 315; |
| } |
| } else { |
| return 0; |
| } |
| } |
| |
| public void setGradientColors(@Nullable int[] colors) { |
| mGradientColors = colors; |
| mSolidColors = null; |
| computeOpacity(); |
| } |
| |
| public void setSolidColors(@Nullable ColorStateList colors) { |
| mGradientColors = null; |
| mSolidColors = colors; |
| computeOpacity(); |
| } |
| |
| private void computeOpacity() { |
| mOpaqueOverBounds = false; |
| mOpaqueOverShape = false; |
| |
| if (mGradientColors != null) { |
| for (int i = 0; i < mGradientColors.length; i++) { |
| if (!isOpaque(mGradientColors[i])) { |
| return; |
| } |
| } |
| } |
| |
| // An unfilled shape is not opaque over bounds or shape |
| if (mGradientColors == null && mSolidColors == null) { |
| return; |
| } |
| |
| // Colors are opaque, so opaqueOverShape=true, |
| mOpaqueOverShape = true; |
| // and opaqueOverBounds=true if shape fills bounds |
| mOpaqueOverBounds = mShape == RECTANGLE |
| && mRadius <= 0 |
| && mRadiusArray == null; |
| } |
| |
| public void setStroke(int width, @Nullable ColorStateList colors, float dashWidth, |
| float dashGap) { |
| mStrokeWidth = width; |
| mStrokeColors = colors; |
| mStrokeDashWidth = dashWidth; |
| mStrokeDashGap = dashGap; |
| computeOpacity(); |
| } |
| |
| public void setCornerRadius(float radius) { |
| if (radius < 0) { |
| radius = 0; |
| } |
| mRadius = radius; |
| mRadiusArray = null; |
| computeOpacity(); |
| } |
| |
| public void setCornerRadii(float[] radii) { |
| mRadiusArray = radii; |
| if (radii == null) { |
| mRadius = 0; |
| } |
| computeOpacity(); |
| } |
| |
| public void setSize(int width, int height) { |
| mWidth = width; |
| mHeight = height; |
| } |
| |
| public void setGradientRadius(float gradientRadius, @RadiusType int type) { |
| mGradientRadius = gradientRadius; |
| mGradientRadiusType = type; |
| } |
| } |
| |
| static boolean isOpaque(int color) { |
| return ((color >> 24) & 0xff) == 0xff; |
| } |
| |
| /** |
| * Creates a new themed GradientDrawable based on the specified constant state. |
| * <p> |
| * The resulting drawable is guaranteed to have a new constant state. |
| * |
| * @param state Constant state from which the drawable inherits |
| */ |
| private GradientDrawable(@NonNull GradientState state, @Nullable Resources res) { |
| mGradientState = state; |
| |
| updateLocalState(res); |
| } |
| |
| private void updateLocalState(Resources res) { |
| final GradientState state = mGradientState; |
| |
| if (state.mSolidColors != null) { |
| final int[] currentState = getState(); |
| final int stateColor = state.mSolidColors.getColorForState(currentState, 0); |
| mFillPaint.setColor(stateColor); |
| } else if (state.mGradientColors == null) { |
| // If we don't have a solid color and we don't have a gradient, |
| // the app is stroking the shape, set the color to the default |
| // value of state.mSolidColor |
| mFillPaint.setColor(0); |
| } else { |
| // Otherwise, make sure the fill alpha is maxed out. |
| mFillPaint.setColor(Color.BLACK); |
| } |
| |
| mPadding = state.mPadding; |
| |
| if (state.mStrokeWidth >= 0) { |
| mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mStrokePaint.setStyle(Paint.Style.STROKE); |
| mStrokePaint.setStrokeWidth(state.mStrokeWidth); |
| |
| if (state.mStrokeColors != null) { |
| final int[] currentState = getState(); |
| final int strokeStateColor = state.mStrokeColors.getColorForState( |
| currentState, 0); |
| mStrokePaint.setColor(strokeStateColor); |
| } |
| |
| if (state.mStrokeDashWidth != 0.0f) { |
| final DashPathEffect e = new DashPathEffect( |
| new float[] { state.mStrokeDashWidth, state.mStrokeDashGap }, 0); |
| mStrokePaint.setPathEffect(e); |
| } |
| } |
| |
| mBlendModeColorFilter = updateBlendModeFilter(mBlendModeColorFilter, state.mTint, |
| state.mBlendMode); |
| mGradientIsDirty = true; |
| |
| state.computeOpacity(); |
| } |
| } |