/*
* Copyright (C) 2014 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.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo.Config;
import android.content.res.ColorStateList;
import android.content.res.ComplexColor;
import android.content.res.GradientColor;
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.ColorFilter;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.Shader;
import android.os.Build;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.FloatProperty;
import android.util.IntProperty;
import android.util.LayoutDirection;
import android.util.Log;
import android.util.PathParser;
import android.util.Property;
import android.util.Xml;
import com.android.internal.R;
import com.android.internal.util.VirtualRefBasePtr;
import dalvik.annotation.optimization.FastNative;
import dalvik.system.VMRuntime;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Stack;
/**
* This lets you create a drawable based on an XML vector graphic.
*
* Note: To optimize for the re-drawing performance, one bitmap cache is created
* for each VectorDrawable. Therefore, referring to the same VectorDrawable means sharing the same
* bitmap cache. If these references don't agree upon on the same size, the bitmap will be recreated
* and redrawn every time size is changed. In other words, if a VectorDrawable is used for
* different sizes, it is more efficient to create multiple VectorDrawables, one for each size.
*
* VectorDrawable can be defined in an XML file with the <vector>
element.
*
* The vector drawable has the following elements:
*
* <vector>
*
* - Used to define a vector drawable
*
* android:name
* - Defines the name of this vector drawable.
* android:width
* - Used to define the intrinsic width of the drawable.
* This supports all the dimension units, normally specified with dp.
* android:height
* - Used to define the intrinsic height of the drawable.
* This supports all the dimension units, normally specified with dp.
* android:viewportWidth
* - Used to define the width of the viewport space. Viewport is basically
* the virtual canvas where the paths are drawn on.
* android:viewportHeight
* - Used to define the height of the viewport space. Viewport is basically
* the virtual canvas where the paths are drawn on.
* android:tint
* - The color to apply to the drawable as a tint. By default, no tint is applied.
* android:tintMode
* - The Porter-Duff blending mode for the tint color. Default is src_in.
* android:autoMirrored
* - Indicates if the drawable needs to be mirrored when its layout direction is
* RTL (right-to-left). Default is false.
* android:alpha
* - The opacity of this drawable. Default is 1.0.
*
*
*
*
* <group>
* - Defines a group of paths or subgroups, plus transformation information.
* The transformations are defined in the same coordinates as the viewport.
* And the transformations are applied in the order of scale, rotate then translate.
*
* android:name
* - Defines the name of the group.
* android:rotation
* - The degrees of rotation of the group. Default is 0.
* android:pivotX
* - The X coordinate of the pivot for the scale and rotation of the group.
* This is defined in the viewport space. Default is 0.
* android:pivotY
* - The Y coordinate of the pivot for the scale and rotation of the group.
* This is defined in the viewport space. Default is 0.
* android:scaleX
* - The amount of scale on the X Coordinate. Default is 1.
* android:scaleY
* - The amount of scale on the Y coordinate. Default is 1.
* android:translateX
* - The amount of translation on the X coordinate.
* This is defined in the viewport space. Default is 0.
* android:translateY
* - The amount of translation on the Y coordinate.
* This is defined in the viewport space. Default is 0.
*
*
*
*
* <path>
* - Defines paths to be drawn.
*
* android:name
* - Defines the name of the path.
* android:pathData
* - Defines path data using exactly same format as "d" attribute
* in the SVG's path data. This is defined in the viewport space.
* android:fillColor
* - Specifies the color used to fill the path. May be a color or, for SDK 24+, a color state list
* or a gradient color (See {@link android.R.styleable#GradientColor}
* and {@link android.R.styleable#GradientColorItem}).
* If this property is animated, any value set by the animation will override the original value.
* No path fill is drawn if this property is not specified.
* android:strokeColor
* - Specifies the color used to draw the path outline. May be a color or, for SDK 24+, a color
* state list or a gradient color (See {@link android.R.styleable#GradientColor}
* and {@link android.R.styleable#GradientColorItem}).
* If this property is animated, any value set by the animation will override the original value.
* No path outline is drawn if this property is not specified.
* android:strokeWidth
* - The width a path stroke. Default is 0.
* android:strokeAlpha
* - The opacity of a path stroke. Default is 1.
* android:fillAlpha
* - The opacity to fill the path with. Default is 1.
* android:trimPathStart
* - The fraction of the path to trim from the start, in the range from 0 to 1. Default is 0.
* android:trimPathEnd
* - The fraction of the path to trim from the end, in the range from 0 to 1. Default is 1.
* android:trimPathOffset
* - Shift trim region (allows showed region to include the start and end), in the range
* from 0 to 1. Default is 0.
* android:strokeLineCap
* - Sets the linecap for a stroked path: butt, round, square. Default is butt.
* android:strokeLineJoin
* - Sets the lineJoin for a stroked path: miter,round,bevel. Default is miter.
* android:strokeMiterLimit
* - Sets the Miter limit for a stroked path. Default is 4.
* android:fillType
* - For SDK 24+, sets the fillType for a path. The types can be either "evenOdd" or "nonZero". They behave the
* same as SVG's "fill-rule" properties. Default is nonZero. For more details, see
* FillRuleProperty
*
*
*
*
*
* <clip-path>
* - Defines path to be the current clip. Note that the clip path only apply to
* the current group and its children.
*
* android:name
* - Defines the name of the clip path.
* - Animatable : No.
* android:pathData
* - Defines clip path using the same format as "d" attribute
* in the SVG's path data.
* - Animatable : Yes.
*
*
* Here is a simple VectorDrawable in this vectordrawable.xml file.
*
* <vector xmlns:android="http://schemas.android.com/apk/res/android"
* android:height="64dp"
* android:width="64dp"
* android:viewportHeight="600"
* android:viewportWidth="600" >
* <group
* android:name="rotationGroup"
* android:pivotX="300.0"
* android:pivotY="300.0"
* android:rotation="45.0" >
* <path
* android:name="v"
* android:fillColor="#000000"
* android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
* </group>
* </vector>
*
*
* Gradient support
* We support 3 types of gradients: {@link android.graphics.LinearGradient},
* {@link android.graphics.RadialGradient}, or {@link android.graphics.SweepGradient}.
*
* And we support all of 3 types of tile modes {@link android.graphics.Shader.TileMode}:
* CLAMP, REPEAT, MIRROR.
*
* All of the attributes are listed in {@link android.R.styleable#GradientColor}.
* Note that different attributes are relevant for different types of gradient.
*
*
*
* LinearGradient |
* RadialGradient |
* SweepGradient |
*
*
*
* startColor |
* startColor |
* startColor |
*
*
* centerColor |
* centerColor |
* centerColor |
*
*
* endColor |
* endColor |
* endColor |
*
*
* type |
* type |
* type |
*
*
* tileMode |
* tileMode |
* tileMode |
*
*
* startX |
* centerX |
* centerX |
*
*
* startY |
* centerY |
* centerY |
*
*
* endX |
* gradientRadius |
* |
*
*
* endY |
* |
* |
*
*
*
* Also note that if any color item {@link android.R.styleable#GradientColorItem} is defined, then
* startColor, centerColor and endColor will be ignored.
*
* See more details in {@link android.R.styleable#GradientColor} and
* {@link android.R.styleable#GradientColorItem}.
*
* Here is a simple example that defines a linear gradient.
*
* <gradient xmlns:android="http://schemas.android.com/apk/res/android"
* android:startColor="?android:attr/colorPrimary"
* android:endColor="?android:attr/colorControlActivated"
* android:centerColor="#f00"
* android:startX="0"
* android:startY="0"
* android:endX="100"
* android:endY="100"
* android:type="linear">
* </gradient>
*
* And here is a simple example that defines a radial gradient using color items.
*
* <gradient xmlns:android="http://schemas.android.com/apk/res/android"
* android:centerX="300"
* android:centerY="300"
* android:gradientRadius="100"
* android:type="radial">
* <item android:offset="0.1" android:color="#0ff"/>
* <item android:offset="0.4" android:color="#fff"/>
* <item android:offset="0.9" android:color="#ff0"/>
* </gradient>
*
*
*/
public class VectorDrawable extends Drawable {
private static final String LOGTAG = VectorDrawable.class.getSimpleName();
private static final String SHAPE_CLIP_PATH = "clip-path";
private static final String SHAPE_GROUP = "group";
private static final String SHAPE_PATH = "path";
private static final String SHAPE_VECTOR = "vector";
private VectorDrawableState mVectorState;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private PorterDuffColorFilter mTintFilter;
private BlendModeColorFilter mBlendModeColorFilter;
private ColorFilter mColorFilter;
private boolean mMutated;
/** The density of the display on which this drawable will be rendered. */
private int mTargetDensity;
// Given the virtual display setup, the dpi can be different than the inflation's dpi.
// Therefore, we need to scale the values we got from the getDimension*().
private int mDpiScaledWidth = 0;
private int mDpiScaledHeight = 0;
private Insets mDpiScaledInsets = Insets.NONE;
/** Whether DPI-scaled width, height, and insets need to be updated. */
private boolean mDpiScaledDirty = true;
// Temp variable, only for saving "new" operation at the draw() time.
private final Rect mTmpBounds = new Rect();
public VectorDrawable() {
this(null, null);
}
/**
* The one constructor to rule them all. This is called by all public
* constructors to set the state and initialize local properties.
*/
private VectorDrawable(@Nullable VectorDrawableState state, @Nullable Resources res) {
// As the mutable, not-thread-safe native instance is stored in VectorDrawableState, we
// need to always do a defensive copy even if mutate() isn't called. Otherwise
// draw() being called on 2 different VectorDrawable instances could still hit the same
// underlying native object.
mVectorState = new VectorDrawableState(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.
*
* @param res resources of the context in which the drawable will be
* displayed, or {@code null} to use the constant state defaults
*/
private void updateLocalState(Resources res) {
final int density = Drawable.resolveDensity(res, mVectorState.mDensity);
if (mTargetDensity != density) {
mTargetDensity = density;
mDpiScaledDirty = true;
}
updateColorFilters(mVectorState.mBlendMode, mVectorState.mTint);
}
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
mVectorState = new VectorDrawableState(mVectorState);
mMutated = true;
}
return this;
}
/**
* @hide
*/
public void clearMutated() {
super.clearMutated();
mMutated = false;
}
@UnsupportedAppUsage
Object getTargetByName(String name) {
return mVectorState.mVGTargetsMap.get(name);
}
@Override
public ConstantState getConstantState() {
mVectorState.mChangingConfigurations = getChangingConfigurations();
return mVectorState;
}
@Override
public void draw(Canvas canvas) {
// We will offset the bounds for drawBitmap, so copyBounds() here instead
// of getBounds().
copyBounds(mTmpBounds);
if (mTmpBounds.width() <= 0 || mTmpBounds.height() <= 0) {
// Nothing to draw
return;
}
// Color filters always override tint filters.
final ColorFilter colorFilter = (mColorFilter == null ? mBlendModeColorFilter :
mColorFilter);
final long colorFilterNativeInstance = colorFilter == null ? 0 :
colorFilter.getNativeInstance();
boolean canReuseCache = mVectorState.canReuseCache();
int pixelCount = nDraw(mVectorState.getNativeRenderer(), canvas.getNativeCanvasWrapper(),
colorFilterNativeInstance, mTmpBounds, needMirroring(),
canReuseCache);
if (pixelCount == 0) {
// Invalid canvas matrix or drawable bounds. This would not affect existing bitmap
// cache, if any.
return;
}
int deltaInBytes;
// Track different bitmap cache based whether the canvas is hw accelerated. By doing so,
// we don't over count bitmap cache allocation: if the input canvas is always of the same
// type, only one bitmap cache is allocated.
if (canvas.isHardwareAccelerated()) {
// Each pixel takes 4 bytes.
deltaInBytes = (pixelCount - mVectorState.mLastHWCachePixelCount) * 4;
mVectorState.mLastHWCachePixelCount = pixelCount;
} else {
// Each pixel takes 4 bytes.
deltaInBytes = (pixelCount - mVectorState.mLastSWCachePixelCount) * 4;
mVectorState.mLastSWCachePixelCount = pixelCount;
}
if (deltaInBytes > 0) {
VMRuntime.getRuntime().registerNativeAllocation(deltaInBytes);
} else if (deltaInBytes < 0) {
VMRuntime.getRuntime().registerNativeFree(-deltaInBytes);
}
}
@Override
public int getAlpha() {
return (int) (mVectorState.getAlpha() * 255);
}
@Override
public void setAlpha(int alpha) {
if (mVectorState.setAlpha(alpha / 255f)) {
invalidateSelf();
}
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mColorFilter = colorFilter;
invalidateSelf();
}
@Override
public ColorFilter getColorFilter() {
return mColorFilter;
}
@Override
public void setTintList(ColorStateList tint) {
final VectorDrawableState state = mVectorState;
if (state.mTint != tint) {
state.mTint = tint;
updateColorFilters(mVectorState.mBlendMode, tint);
invalidateSelf();
}
}
@Override
public void setTintBlendMode(@NonNull BlendMode blendMode) {
final VectorDrawableState state = mVectorState;
if (state.mBlendMode != blendMode) {
state.mBlendMode = blendMode;
updateColorFilters(state.mBlendMode, state.mTint);
invalidateSelf();
}
}
@Override
public boolean isStateful() {
return super.isStateful() || (mVectorState != null && mVectorState.isStateful());
}
@Override
public boolean hasFocusStateSpecified() {
return mVectorState != null && mVectorState.hasFocusStateSpecified();
}
@Override
protected boolean onStateChange(int[] stateSet) {
boolean changed = false;
// When the VD is stateful, we need to mutate the drawable such that we don't share the
// cache bitmap with others. Such that the state change only affect this new cached bitmap.
if (isStateful()) {
mutate();
}
final VectorDrawableState state = mVectorState;
if (state.onStateChange(stateSet)) {
changed = true;
state.mCacheDirty = true;
}
if (state.mTint != null && state.mBlendMode != null) {
BlendMode blendMode = state.mBlendMode;
ColorStateList tint = state.mTint;
updateColorFilters(blendMode, tint);
changed = true;
}
return changed;
}
private void updateColorFilters(@Nullable BlendMode blendMode, ColorStateList tint) {
PorterDuff.Mode mode = BlendMode.blendModeToPorterDuffMode(blendMode);
mTintFilter = updateTintFilter(mTintFilter, tint, mode);
mBlendModeColorFilter = updateBlendModeFilter(mBlendModeColorFilter, tint, blendMode);
}
@Override
public int getOpacity() {
// We can't tell whether the drawable is fully opaque unless we examine all the pixels,
// but we could tell it is transparent if the root alpha is 0.
return getAlpha() == 0 ? PixelFormat.TRANSPARENT : PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicWidth() {
if (mDpiScaledDirty) {
computeVectorSize();
}
return mDpiScaledWidth;
}
@Override
public int getIntrinsicHeight() {
if (mDpiScaledDirty) {
computeVectorSize();
}
return mDpiScaledHeight;
}
@Override
public Insets getOpticalInsets() {
if (mDpiScaledDirty) {
computeVectorSize();
}
return mDpiScaledInsets;
}
/*
* Update local dimensions to adjust for a target density that may differ
* from the source density against which the constant state was loaded.
*/
void computeVectorSize() {
final Insets opticalInsets = mVectorState.mOpticalInsets;
final int sourceDensity = mVectorState.mDensity;
final int targetDensity = mTargetDensity;
if (targetDensity != sourceDensity) {
mDpiScaledWidth = Drawable.scaleFromDensity(mVectorState.mBaseWidth, sourceDensity,
targetDensity, true);
mDpiScaledHeight = Drawable.scaleFromDensity(mVectorState.mBaseHeight,sourceDensity,
targetDensity, true);
final int left = Drawable.scaleFromDensity(
opticalInsets.left, sourceDensity, targetDensity, false);
final int right = Drawable.scaleFromDensity(
opticalInsets.right, sourceDensity, targetDensity, false);
final int top = Drawable.scaleFromDensity(
opticalInsets.top, sourceDensity, targetDensity, false);
final int bottom = Drawable.scaleFromDensity(
opticalInsets.bottom, sourceDensity, targetDensity, false);
mDpiScaledInsets = Insets.of(left, top, right, bottom);
} else {
mDpiScaledWidth = mVectorState.mBaseWidth;
mDpiScaledHeight = mVectorState.mBaseHeight;
mDpiScaledInsets = opticalInsets;
}
mDpiScaledDirty = false;
}
@Override
public boolean canApplyTheme() {
return (mVectorState != null && mVectorState.canApplyTheme()) || super.canApplyTheme();
}
@Override
public void applyTheme(Theme t) {
super.applyTheme(t);
final VectorDrawableState state = mVectorState;
if (state == null) {
return;
}
final boolean changedDensity = mVectorState.setDensity(
Drawable.resolveDensity(t.getResources(), 0));
mDpiScaledDirty |= changedDensity;
if (state.mThemeAttrs != null) {
final TypedArray a = t.resolveAttributes(
state.mThemeAttrs, R.styleable.VectorDrawable);
try {
state.mCacheDirty = true;
updateStateFromTypedArray(a);
} catch (XmlPullParserException e) {
throw new RuntimeException(e);
} finally {
a.recycle();
}
// May have changed size.
mDpiScaledDirty = true;
}
// Apply theme to contained color state list.
if (state.mTint != null && state.mTint.canApplyTheme()) {
state.mTint = state.mTint.obtainForTheme(t);
}
if (mVectorState != null && mVectorState.canApplyTheme()) {
mVectorState.applyTheme(t);
}
// Update local properties.
updateLocalState(t.getResources());
}
/**
* The size of a pixel when scaled from the intrinsic dimension to the viewport dimension.
* This is used to calculate the path animation accuracy.
*
* @hide
*/
public float getPixelSize() {
if (mVectorState == null ||
mVectorState.mBaseWidth == 0 ||
mVectorState.mBaseHeight == 0 ||
mVectorState.mViewportHeight == 0 ||
mVectorState.mViewportWidth == 0) {
return 1; // fall back to 1:1 pixel mapping.
}
float intrinsicWidth = mVectorState.mBaseWidth;
float intrinsicHeight = mVectorState.mBaseHeight;
float viewportWidth = mVectorState.mViewportWidth;
float viewportHeight = mVectorState.mViewportHeight;
float scaleX = viewportWidth / intrinsicWidth;
float scaleY = viewportHeight / intrinsicHeight;
return Math.min(scaleX, scaleY);
}
/** @hide */
public static VectorDrawable create(Resources resources, int rid) {
try {
final XmlPullParser parser = resources.getXml(rid);
final AttributeSet attrs = Xml.asAttributeSet(parser);
int type;
while ((type=parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty loop
}
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
final VectorDrawable drawable = new VectorDrawable();
drawable.inflate(resources, parser, attrs);
return drawable;
} catch (XmlPullParserException e) {
Log.e(LOGTAG, "parser error", e);
} catch (IOException e) {
Log.e(LOGTAG, "parser error", e);
}
return null;
}
@Override
public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "VectorDrawable#inflate");
if (mVectorState.mRootGroup != null || mVectorState.mNativeTree != null) {
// This VD has been used to display other VD resource content, clean up.
if (mVectorState.mRootGroup != null) {
// Subtract the native allocation for all the nodes.
VMRuntime.getRuntime().registerNativeFree(
mVectorState.mRootGroup.getNativeSize());
// Remove child nodes' reference to tree
mVectorState.mRootGroup.setTree(null);
}
mVectorState.mRootGroup = new VGroup();
if (mVectorState.mNativeTree != null) {
// Subtract the native allocation for the tree wrapper, which contains root node
// as well as rendering related data.
VMRuntime.getRuntime().registerNativeFree(mVectorState.NATIVE_ALLOCATION_SIZE);
mVectorState.mNativeTree.release();
}
mVectorState.createNativeTree(mVectorState.mRootGroup);
}
final VectorDrawableState state = mVectorState;
state.setDensity(Drawable.resolveDensity(r, 0));
final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawable);
updateStateFromTypedArray(a);
a.recycle();
mDpiScaledDirty = true;
state.mCacheDirty = true;
inflateChildElements(r, parser, attrs, theme);
state.onTreeConstructionFinished();
// Update local properties.
updateLocalState(r);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
final VectorDrawableState state = mVectorState;
// Account for any configuration changes.
state.mChangingConfigurations |= a.getChangingConfigurations();
// Extract the theme attributes, if any.
state.mThemeAttrs = a.extractThemeAttrs();
final int tintMode = a.getInt(R.styleable.VectorDrawable_tintMode, -1);
if (tintMode != -1) {
state.mBlendMode = Drawable.parseBlendMode(tintMode, BlendMode.SRC_IN);
}
final ColorStateList tint = a.getColorStateList(R.styleable.VectorDrawable_tint);
if (tint != null) {
state.mTint = tint;
}
state.mAutoMirrored = a.getBoolean(
R.styleable.VectorDrawable_autoMirrored, state.mAutoMirrored);
float viewportWidth = a.getFloat(
R.styleable.VectorDrawable_viewportWidth, state.mViewportWidth);
float viewportHeight = a.getFloat(
R.styleable.VectorDrawable_viewportHeight, state.mViewportHeight);
state.setViewportSize(viewportWidth, viewportHeight);
if (state.mViewportWidth <= 0) {
throw new XmlPullParserException(a.getPositionDescription() +
" tag requires viewportWidth > 0");
} else if (state.mViewportHeight <= 0) {
throw new XmlPullParserException(a.getPositionDescription() +
" tag requires viewportHeight > 0");
}
state.mBaseWidth = a.getDimensionPixelSize(
R.styleable.VectorDrawable_width, state.mBaseWidth);
state.mBaseHeight = a.getDimensionPixelSize(
R.styleable.VectorDrawable_height, state.mBaseHeight);
if (state.mBaseWidth <= 0) {
throw new XmlPullParserException(a.getPositionDescription() +
" tag requires width > 0");
} else if (state.mBaseHeight <= 0) {
throw new XmlPullParserException(a.getPositionDescription() +
" tag requires height > 0");
}
final int insetLeft = a.getDimensionPixelOffset(
R.styleable.VectorDrawable_opticalInsetLeft, state.mOpticalInsets.left);
final int insetTop = a.getDimensionPixelOffset(
R.styleable.VectorDrawable_opticalInsetTop, state.mOpticalInsets.top);
final int insetRight = a.getDimensionPixelOffset(
R.styleable.VectorDrawable_opticalInsetRight, state.mOpticalInsets.right);
final int insetBottom = a.getDimensionPixelOffset(
R.styleable.VectorDrawable_opticalInsetBottom, state.mOpticalInsets.bottom);
state.mOpticalInsets = Insets.of(insetLeft, insetTop, insetRight, insetBottom);
final float alphaInFloat = a.getFloat(
R.styleable.VectorDrawable_alpha, state.getAlpha());
state.setAlpha(alphaInFloat);
final String name = a.getString(R.styleable.VectorDrawable_name);
if (name != null) {
state.mRootName = name;
state.mVGTargetsMap.put(name, state);
}
}
private void inflateChildElements(Resources res, XmlPullParser parser, AttributeSet attrs,
Theme theme) throws XmlPullParserException, IOException {
final VectorDrawableState state = mVectorState;
boolean noPathTag = true;
// Use a stack to help to build the group tree.
// The top of the stack is always the current group.
final Stack groupStack = new Stack();
groupStack.push(state.mRootGroup);
int eventType = parser.getEventType();
final int innerDepth = parser.getDepth() + 1;
// Parse everything until the end of the vector element.
while (eventType != XmlPullParser.END_DOCUMENT
&& (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
if (eventType == XmlPullParser.START_TAG) {
final String tagName = parser.getName();
final VGroup currentGroup = groupStack.peek();
if (SHAPE_PATH.equals(tagName)) {
final VFullPath path = new VFullPath();
path.inflate(res, attrs, theme);
currentGroup.addChild(path);
if (path.getPathName() != null) {
state.mVGTargetsMap.put(path.getPathName(), path);
}
noPathTag = false;
state.mChangingConfigurations |= path.mChangingConfigurations;
} else if (SHAPE_CLIP_PATH.equals(tagName)) {
final VClipPath path = new VClipPath();
path.inflate(res, attrs, theme);
currentGroup.addChild(path);
if (path.getPathName() != null) {
state.mVGTargetsMap.put(path.getPathName(), path);
}
state.mChangingConfigurations |= path.mChangingConfigurations;
} else if (SHAPE_GROUP.equals(tagName)) {
VGroup newChildGroup = new VGroup();
newChildGroup.inflate(res, attrs, theme);
currentGroup.addChild(newChildGroup);
groupStack.push(newChildGroup);
if (newChildGroup.getGroupName() != null) {
state.mVGTargetsMap.put(newChildGroup.getGroupName(),
newChildGroup);
}
state.mChangingConfigurations |= newChildGroup.mChangingConfigurations;
}
} else if (eventType == XmlPullParser.END_TAG) {
final String tagName = parser.getName();
if (SHAPE_GROUP.equals(tagName)) {
groupStack.pop();
}
}
eventType = parser.next();
}
if (noPathTag) {
final StringBuffer tag = new StringBuffer();
if (tag.length() > 0) {
tag.append(" or ");
}
tag.append(SHAPE_PATH);
throw new XmlPullParserException("no " + tag + " defined");
}
}
@Override
public @Config int getChangingConfigurations() {
return super.getChangingConfigurations() | mVectorState.getChangingConfigurations();
}
@UnsupportedAppUsage
void setAllowCaching(boolean allowCaching) {
nSetAllowCaching(mVectorState.getNativeRenderer(), allowCaching);
}
private boolean needMirroring() {
return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
}
@Override
public void setAutoMirrored(boolean mirrored) {
if (mVectorState.mAutoMirrored != mirrored) {
mVectorState.mAutoMirrored = mirrored;
invalidateSelf();
}
}
@Override
public boolean isAutoMirrored() {
return mVectorState.mAutoMirrored;
}
/**
* @hide
*/
public long getNativeTree() {
return mVectorState.getNativeRenderer();
}
/**
* @hide
*/
public void setAntiAlias(boolean aa) {
nSetAntiAlias(mVectorState.mNativeTree.get(), aa);
}
static class VectorDrawableState extends ConstantState {
// Variables below need to be copied (deep copy if applicable) for mutation.
int[] mThemeAttrs;
@Config int mChangingConfigurations;
ColorStateList mTint = null;
BlendMode mBlendMode = DEFAULT_BLEND_MODE;
boolean mAutoMirrored;
int mBaseWidth = 0;
int mBaseHeight = 0;
float mViewportWidth = 0;
float mViewportHeight = 0;
Insets mOpticalInsets = Insets.NONE;
String mRootName = null;
VGroup mRootGroup;
VirtualRefBasePtr mNativeTree = null;
int mDensity = DisplayMetrics.DENSITY_DEFAULT;
final ArrayMap mVGTargetsMap = new ArrayMap<>();
// Fields for cache
int[] mCachedThemeAttrs;
ColorStateList mCachedTint;
BlendMode mCachedBlendMode;
boolean mCachedAutoMirrored;
boolean mCacheDirty;
// Since sw canvas and hw canvas uses different bitmap caches, we track the allocation of
// these bitmaps separately.
int mLastSWCachePixelCount = 0;
int mLastHWCachePixelCount = 0;
final static Property ALPHA =
new FloatProperty("alpha") {
@Override
public void setValue(VectorDrawableState state, float value) {
state.setAlpha(value);
}
@Override
public Float get(VectorDrawableState state) {
return state.getAlpha();
}
};
Property getProperty(String propertyName) {
if (ALPHA.getName().equals(propertyName)) {
return ALPHA;
}
return null;
}
// This tracks the total native allocation for all the nodes.
private int mAllocationOfAllNodes = 0;
private static final int NATIVE_ALLOCATION_SIZE = 316;
// If copy is not null, deep copy the given VectorDrawableState. Otherwise, create a
// native vector drawable tree with an empty root group.
public VectorDrawableState(VectorDrawableState copy) {
if (copy != null) {
mThemeAttrs = copy.mThemeAttrs;
mChangingConfigurations = copy.mChangingConfigurations;
mTint = copy.mTint;
mBlendMode = copy.mBlendMode;
mAutoMirrored = copy.mAutoMirrored;
mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap);
createNativeTreeFromCopy(copy, mRootGroup);
mBaseWidth = copy.mBaseWidth;
mBaseHeight = copy.mBaseHeight;
setViewportSize(copy.mViewportWidth, copy.mViewportHeight);
mOpticalInsets = copy.mOpticalInsets;
mRootName = copy.mRootName;
mDensity = copy.mDensity;
if (copy.mRootName != null) {
mVGTargetsMap.put(copy.mRootName, this);
}
} else {
mRootGroup = new VGroup();
createNativeTree(mRootGroup);
}
onTreeConstructionFinished();
}
private void createNativeTree(VGroup rootGroup) {
mNativeTree = new VirtualRefBasePtr(nCreateTree(rootGroup.mNativePtr));
// Register tree size
VMRuntime.getRuntime().registerNativeAllocation(NATIVE_ALLOCATION_SIZE);
}
// Create a new native tree with the given root group, and copy the properties from the
// given VectorDrawableState's native tree.
private void createNativeTreeFromCopy(VectorDrawableState copy, VGroup rootGroup) {
mNativeTree = new VirtualRefBasePtr(nCreateTreeFromCopy(
copy.mNativeTree.get(), rootGroup.mNativePtr));
// Register tree size
VMRuntime.getRuntime().registerNativeAllocation(NATIVE_ALLOCATION_SIZE);
}
// This should be called every time after a new RootGroup and all its subtrees are created
// (i.e. in constructors of VectorDrawableState and in inflate).
void onTreeConstructionFinished() {
mRootGroup.setTree(mNativeTree);
mAllocationOfAllNodes = mRootGroup.getNativeSize();
VMRuntime.getRuntime().registerNativeAllocation(mAllocationOfAllNodes);
}
long getNativeRenderer() {
if (mNativeTree == null) {
return 0;
}
return mNativeTree.get();
}
public boolean canReuseCache() {
if (!mCacheDirty
&& mCachedThemeAttrs == mThemeAttrs
&& mCachedTint == mTint
&& mCachedBlendMode == mBlendMode
&& mCachedAutoMirrored == mAutoMirrored) {
return true;
}
updateCacheStates();
return false;
}
public void updateCacheStates() {
// Use shallow copy here and shallow comparison in canReuseCache(),
// likely hit cache miss more, but practically not much difference.
mCachedThemeAttrs = mThemeAttrs;
mCachedTint = mTint;
mCachedBlendMode = mBlendMode;
mCachedAutoMirrored = mAutoMirrored;
mCacheDirty = false;
}
public void applyTheme(Theme t) {
mRootGroup.applyTheme(t);
}
@Override
public boolean canApplyTheme() {
return mThemeAttrs != null
|| (mRootGroup != null && mRootGroup.canApplyTheme())
|| (mTint != null && mTint.canApplyTheme())
|| super.canApplyTheme();
}
@Override
public Drawable newDrawable() {
return new VectorDrawable(this, null);
}
@Override
public Drawable newDrawable(Resources res) {
return new VectorDrawable(this, res);
}
@Override
public @Config int getChangingConfigurations() {
return mChangingConfigurations
| (mTint != null ? mTint.getChangingConfigurations() : 0);
}
public boolean isStateful() {
return (mTint != null && mTint.isStateful())
|| (mRootGroup != null && mRootGroup.isStateful());
}
public boolean hasFocusStateSpecified() {
return mTint != null && mTint.hasFocusStateSpecified()
|| (mRootGroup != null && mRootGroup.hasFocusStateSpecified());
}
void setViewportSize(float viewportWidth, float viewportHeight) {
mViewportWidth = viewportWidth;
mViewportHeight = viewportHeight;
nSetRendererViewportSize(getNativeRenderer(), viewportWidth, viewportHeight);
}
public final boolean setDensity(int targetDensity) {
if (mDensity != targetDensity) {
final int sourceDensity = mDensity;
mDensity = targetDensity;
applyDensityScaling(sourceDensity, targetDensity);
return true;
}
return false;
}
private void applyDensityScaling(int sourceDensity, int targetDensity) {
mBaseWidth = Drawable.scaleFromDensity(mBaseWidth, sourceDensity, targetDensity, true);
mBaseHeight = Drawable.scaleFromDensity(mBaseHeight, sourceDensity, targetDensity,
true);
final int insetLeft = Drawable.scaleFromDensity(
mOpticalInsets.left, sourceDensity, targetDensity, false);
final int insetTop = Drawable.scaleFromDensity(
mOpticalInsets.top, sourceDensity, targetDensity, false);
final int insetRight = Drawable.scaleFromDensity(
mOpticalInsets.right, sourceDensity, targetDensity, false);
final int insetBottom = Drawable.scaleFromDensity(
mOpticalInsets.bottom, sourceDensity, targetDensity, false);
mOpticalInsets = Insets.of(insetLeft, insetTop, insetRight, insetBottom);
}
public boolean onStateChange(int[] stateSet) {
return mRootGroup.onStateChange(stateSet);
}
@Override
public void finalize() throws Throwable {
super.finalize();
int bitmapCacheSize = mLastHWCachePixelCount * 4 + mLastSWCachePixelCount * 4;
VMRuntime.getRuntime().registerNativeFree(NATIVE_ALLOCATION_SIZE
+ mAllocationOfAllNodes + bitmapCacheSize);
}
/**
* setAlpha() and getAlpha() are used mostly for animation purpose. Return true if alpha
* has changed.
*/
public boolean setAlpha(float alpha) {
return nSetRootAlpha(mNativeTree.get(), alpha);
}
@SuppressWarnings("unused")
public float getAlpha() {
return nGetRootAlpha(mNativeTree.get());
}
}
static class VGroup extends VObject {
private static final int ROTATION_INDEX = 0;
private static final int PIVOT_X_INDEX = 1;
private static final int PIVOT_Y_INDEX = 2;
private static final int SCALE_X_INDEX = 3;
private static final int SCALE_Y_INDEX = 4;
private static final int TRANSLATE_X_INDEX = 5;
private static final int TRANSLATE_Y_INDEX = 6;
private static final int TRANSFORM_PROPERTY_COUNT = 7;
private static final int NATIVE_ALLOCATION_SIZE = 100;
private static final HashMap sPropertyIndexMap =
new HashMap() {
{
put("translateX", TRANSLATE_X_INDEX);
put("translateY", TRANSLATE_Y_INDEX);
put("scaleX", SCALE_X_INDEX);
put("scaleY", SCALE_Y_INDEX);
put("pivotX", PIVOT_X_INDEX);
put("pivotY", PIVOT_Y_INDEX);
put("rotation", ROTATION_INDEX);
}
};
static int getPropertyIndex(String propertyName) {
if (sPropertyIndexMap.containsKey(propertyName)) {
return sPropertyIndexMap.get(propertyName);
} else {
// property not found
return -1;
}
}
// Below are the Properties that wrap the setters to avoid reflection overhead in animations
private static final Property TRANSLATE_X =
new FloatProperty ("translateX") {
@Override
public void setValue(VGroup object, float value) {
object.setTranslateX(value);
}
@Override
public Float get(VGroup object) {
return object.getTranslateX();
}
};
private static final Property TRANSLATE_Y =
new FloatProperty ("translateY") {
@Override
public void setValue(VGroup object, float value) {
object.setTranslateY(value);
}
@Override
public Float get(VGroup object) {
return object.getTranslateY();
}
};
private static final Property SCALE_X =
new FloatProperty ("scaleX") {
@Override
public void setValue(VGroup object, float value) {
object.setScaleX(value);
}
@Override
public Float get(VGroup object) {
return object.getScaleX();
}
};
private static final Property SCALE_Y =
new FloatProperty ("scaleY") {
@Override
public void setValue(VGroup object, float value) {
object.setScaleY(value);
}
@Override
public Float get(VGroup object) {
return object.getScaleY();
}
};
private static final Property PIVOT_X =
new FloatProperty ("pivotX") {
@Override
public void setValue(VGroup object, float value) {
object.setPivotX(value);
}
@Override
public Float get(VGroup object) {
return object.getPivotX();
}
};
private static final Property PIVOT_Y =
new FloatProperty ("pivotY") {
@Override
public void setValue(VGroup object, float value) {
object.setPivotY(value);
}
@Override
public Float get(VGroup object) {
return object.getPivotY();
}
};
private static final Property ROTATION =
new FloatProperty ("rotation") {
@Override
public void setValue(VGroup object, float value) {
object.setRotation(value);
}
@Override
public Float get(VGroup object) {
return object.getRotation();
}
};
private static final HashMap sPropertyMap =
new HashMap() {
{
put("translateX", TRANSLATE_X);
put("translateY", TRANSLATE_Y);
put("scaleX", SCALE_X);
put("scaleY", SCALE_Y);
put("pivotX", PIVOT_X);
put("pivotY", PIVOT_Y);
put("rotation", ROTATION);
}
};
// Temp array to store transform values obtained from native.
private float[] mTransform;
/////////////////////////////////////////////////////
// Variables below need to be copied (deep copy if applicable) for mutation.
private final ArrayList mChildren = new ArrayList<>();
private boolean mIsStateful;
// mLocalMatrix is updated based on the update of transformation information,
// either parsed from the XML or by animation.
private @Config int mChangingConfigurations;
private int[] mThemeAttrs;
private String mGroupName = null;
// The native object will be created in the constructor and will be destroyed in native
// when the neither java nor native has ref to the tree. This pointer should be valid
// throughout this VGroup Java object's life.
private final long mNativePtr;
public VGroup(VGroup copy, ArrayMap targetsMap) {
mIsStateful = copy.mIsStateful;
mThemeAttrs = copy.mThemeAttrs;
mGroupName = copy.mGroupName;
mChangingConfigurations = copy.mChangingConfigurations;
if (mGroupName != null) {
targetsMap.put(mGroupName, this);
}
mNativePtr = nCreateGroup(copy.mNativePtr);
final ArrayList children = copy.mChildren;
for (int i = 0; i < children.size(); i++) {
final VObject copyChild = children.get(i);
if (copyChild instanceof VGroup) {
final VGroup copyGroup = (VGroup) copyChild;
addChild(new VGroup(copyGroup, targetsMap));
} else {
final VPath newPath;
if (copyChild instanceof VFullPath) {
newPath = new VFullPath((VFullPath) copyChild);
} else if (copyChild instanceof VClipPath) {
newPath = new VClipPath((VClipPath) copyChild);
} else {
throw new IllegalStateException("Unknown object in the tree!");
}
addChild(newPath);
if (newPath.mPathName != null) {
targetsMap.put(newPath.mPathName, newPath);
}
}
}
}
public VGroup() {
mNativePtr = nCreateGroup();
}
Property getProperty(String propertyName) {
if (sPropertyMap.containsKey(propertyName)) {
return sPropertyMap.get(propertyName);
} else {
// property not found
return null;
}
}
public String getGroupName() {
return mGroupName;
}
public void addChild(VObject child) {
nAddChild(mNativePtr, child.getNativePtr());
mChildren.add(child);
mIsStateful |= child.isStateful();
}
@Override
public void setTree(VirtualRefBasePtr treeRoot) {
super.setTree(treeRoot);
for (int i = 0; i < mChildren.size(); i++) {
mChildren.get(i).setTree(treeRoot);
}
}
@Override
public long getNativePtr() {
return mNativePtr;
}
@Override
public void inflate(Resources res, AttributeSet attrs, Theme theme) {
final TypedArray a = obtainAttributes(res, theme, attrs,
R.styleable.VectorDrawableGroup);
updateStateFromTypedArray(a);
a.recycle();
}
void updateStateFromTypedArray(TypedArray a) {
// Account for any configuration changes.
mChangingConfigurations |= a.getChangingConfigurations();
// Extract the theme attributes, if any.
mThemeAttrs = a.extractThemeAttrs();
if (mTransform == null) {
// Lazy initialization: If the group is created through copy constructor, this may
// never get called.
mTransform = new float[TRANSFORM_PROPERTY_COUNT];
}
boolean success = nGetGroupProperties(mNativePtr, mTransform, TRANSFORM_PROPERTY_COUNT);
if (!success) {
throw new RuntimeException("Error: inconsistent property count");
}
float rotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation,
mTransform[ROTATION_INDEX]);
float pivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX,
mTransform[PIVOT_X_INDEX]);
float pivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY,
mTransform[PIVOT_Y_INDEX]);
float scaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX,
mTransform[SCALE_X_INDEX]);
float scaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY,
mTransform[SCALE_Y_INDEX]);
float translateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX,
mTransform[TRANSLATE_X_INDEX]);
float translateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY,
mTransform[TRANSLATE_Y_INDEX]);
final String groupName = a.getString(R.styleable.VectorDrawableGroup_name);
if (groupName != null) {
mGroupName = groupName;
nSetName(mNativePtr, mGroupName);
}
nUpdateGroupProperties(mNativePtr, rotate, pivotX, pivotY, scaleX, scaleY,
translateX, translateY);
}
@Override
public boolean onStateChange(int[] stateSet) {
boolean changed = false;
final ArrayList children = mChildren;
for (int i = 0, count = children.size(); i < count; i++) {
final VObject child = children.get(i);
if (child.isStateful()) {
changed |= child.onStateChange(stateSet);
}
}
return changed;
}
@Override
public boolean isStateful() {
return mIsStateful;
}
@Override
public boolean hasFocusStateSpecified() {
boolean result = false;
final ArrayList children = mChildren;
for (int i = 0, count = children.size(); i < count; i++) {
final VObject child = children.get(i);
if (child.isStateful()) {
result |= child.hasFocusStateSpecified();
}
}
return result;
}
@Override
int getNativeSize() {
// Return the native allocation needed for the subtree.
int size = NATIVE_ALLOCATION_SIZE;
for (int i = 0; i < mChildren.size(); i++) {
size += mChildren.get(i).getNativeSize();
}
return size;
}
@Override
public boolean canApplyTheme() {
if (mThemeAttrs != null) {
return true;
}
final ArrayList children = mChildren;
for (int i = 0, count = children.size(); i < count; i++) {
final VObject child = children.get(i);
if (child.canApplyTheme()) {
return true;
}
}
return false;
}
@Override
public void applyTheme(Theme t) {
if (mThemeAttrs != null) {
final TypedArray a = t.resolveAttributes(mThemeAttrs,
R.styleable.VectorDrawableGroup);
updateStateFromTypedArray(a);
a.recycle();
}
final ArrayList children = mChildren;
for (int i = 0, count = children.size(); i < count; i++) {
final VObject child = children.get(i);
if (child.canApplyTheme()) {
child.applyTheme(t);
// Applying a theme may have made the child stateful.
mIsStateful |= child.isStateful();
}
}
}
/* Setters and Getters, used by animator from AnimatedVectorDrawable. */
@SuppressWarnings("unused")
public float getRotation() {
return isTreeValid() ? nGetRotation(mNativePtr) : 0;
}
@SuppressWarnings("unused")
@UnsupportedAppUsage
public void setRotation(float rotation) {
if (isTreeValid()) {
nSetRotation(mNativePtr, rotation);
}
}
@SuppressWarnings("unused")
public float getPivotX() {
return isTreeValid() ? nGetPivotX(mNativePtr) : 0;
}
@SuppressWarnings("unused")
@UnsupportedAppUsage
public void setPivotX(float pivotX) {
if (isTreeValid()) {
nSetPivotX(mNativePtr, pivotX);
}
}
@SuppressWarnings("unused")
public float getPivotY() {
return isTreeValid() ? nGetPivotY(mNativePtr) : 0;
}
@SuppressWarnings("unused")
@UnsupportedAppUsage
public void setPivotY(float pivotY) {
if (isTreeValid()) {
nSetPivotY(mNativePtr, pivotY);
}
}
@SuppressWarnings("unused")
public float getScaleX() {
return isTreeValid() ? nGetScaleX(mNativePtr) : 0;
}
@SuppressWarnings("unused")
public void setScaleX(float scaleX) {
if (isTreeValid()) {
nSetScaleX(mNativePtr, scaleX);
}
}
@SuppressWarnings("unused")
public float getScaleY() {
return isTreeValid() ? nGetScaleY(mNativePtr) : 0;
}
@SuppressWarnings("unused")
public void setScaleY(float scaleY) {
if (isTreeValid()) {
nSetScaleY(mNativePtr, scaleY);
}
}
@SuppressWarnings("unused")
public float getTranslateX() {
return isTreeValid() ? nGetTranslateX(mNativePtr) : 0;
}
@SuppressWarnings("unused")
@UnsupportedAppUsage
public void setTranslateX(float translateX) {
if (isTreeValid()) {
nSetTranslateX(mNativePtr, translateX);
}
}
@SuppressWarnings("unused")
public float getTranslateY() {
return isTreeValid() ? nGetTranslateY(mNativePtr) : 0;
}
@SuppressWarnings("unused")
@UnsupportedAppUsage
public void setTranslateY(float translateY) {
if (isTreeValid()) {
nSetTranslateY(mNativePtr, translateY);
}
}
}
/**
* Common Path information for clip path and normal path.
*/
static abstract class VPath extends VObject {
protected PathParser.PathData mPathData = null;
String mPathName;
@Config int mChangingConfigurations;
private static final Property PATH_DATA =
new Property(PathParser.PathData.class, "pathData") {
@Override
public void set(VPath object, PathParser.PathData data) {
object.setPathData(data);
}
@Override
public PathParser.PathData get(VPath object) {
return object.getPathData();
}
};
Property getProperty(String propertyName) {
if (PATH_DATA.getName().equals(propertyName)) {
return PATH_DATA;
}
// property not found
return null;
}
public VPath() {
// Empty constructor.
}
public VPath(VPath copy) {
mPathName = copy.mPathName;
mChangingConfigurations = copy.mChangingConfigurations;
mPathData = copy.mPathData == null ? null : new PathParser.PathData(copy.mPathData);
}
public String getPathName() {
return mPathName;
}
/* Setters and Getters, used by animator from AnimatedVectorDrawable. */
@SuppressWarnings("unused")
public PathParser.PathData getPathData() {
return mPathData;
}
// TODO: Move the PathEvaluator and this setter and the getter above into native.
@SuppressWarnings("unused")
public void setPathData(PathParser.PathData pathData) {
mPathData.setPathData(pathData);
if (isTreeValid()) {
nSetPathData(getNativePtr(), mPathData.getNativePtr());
}
}
}
/**
* Clip path, which only has name and pathData.
*/
private static class VClipPath extends VPath {
private final long mNativePtr;
private static final int NATIVE_ALLOCATION_SIZE = 120;
public VClipPath() {
mNativePtr = nCreateClipPath();
}
public VClipPath(VClipPath copy) {
super(copy);
mNativePtr = nCreateClipPath(copy.mNativePtr);
}
@Override
public long getNativePtr() {
return mNativePtr;
}
@Override
public void inflate(Resources r, AttributeSet attrs, Theme theme) {
final TypedArray a = obtainAttributes(r, theme, attrs,
R.styleable.VectorDrawableClipPath);
updateStateFromTypedArray(a);
a.recycle();
}
@Override
public boolean canApplyTheme() {
return false;
}
@Override
public void applyTheme(Theme theme) {
// No-op.
}
@Override
public boolean onStateChange(int[] stateSet) {
return false;
}
@Override
public boolean isStateful() {
return false;
}
@Override
public boolean hasFocusStateSpecified() {
return false;
}
@Override
int getNativeSize() {
return NATIVE_ALLOCATION_SIZE;
}
private void updateStateFromTypedArray(TypedArray a) {
// Account for any configuration changes.
mChangingConfigurations |= a.getChangingConfigurations();
final String pathName = a.getString(R.styleable.VectorDrawableClipPath_name);
if (pathName != null) {
mPathName = pathName;
nSetName(mNativePtr, mPathName);
}
final String pathDataString = a.getString(R.styleable.VectorDrawableClipPath_pathData);
if (pathDataString != null) {
mPathData = new PathParser.PathData(pathDataString);
nSetPathString(mNativePtr, pathDataString, pathDataString.length());
}
}
}
/**
* Normal path, which contains all the fill / paint information.
*/
static class VFullPath extends VPath {
private static final int STROKE_WIDTH_INDEX = 0;
private static final int STROKE_COLOR_INDEX = 1;
private static final int STROKE_ALPHA_INDEX = 2;
private static final int FILL_COLOR_INDEX = 3;
private static final int FILL_ALPHA_INDEX = 4;
private static final int TRIM_PATH_START_INDEX = 5;
private static final int TRIM_PATH_END_INDEX = 6;
private static final int TRIM_PATH_OFFSET_INDEX = 7;
private static final int STROKE_LINE_CAP_INDEX = 8;
private static final int STROKE_LINE_JOIN_INDEX = 9;
private static final int STROKE_MITER_LIMIT_INDEX = 10;
private static final int FILL_TYPE_INDEX = 11;
private static final int TOTAL_PROPERTY_COUNT = 12;
private static final int NATIVE_ALLOCATION_SIZE = 264;
// Property map for animatable attributes.
private final static HashMap sPropertyIndexMap
= new HashMap () {
{
put("strokeWidth", STROKE_WIDTH_INDEX);
put("strokeColor", STROKE_COLOR_INDEX);
put("strokeAlpha", STROKE_ALPHA_INDEX);
put("fillColor", FILL_COLOR_INDEX);
put("fillAlpha", FILL_ALPHA_INDEX);
put("trimPathStart", TRIM_PATH_START_INDEX);
put("trimPathEnd", TRIM_PATH_END_INDEX);
put("trimPathOffset", TRIM_PATH_OFFSET_INDEX);
}
};
// Below are the Properties that wrap the setters to avoid reflection overhead in animations
private static final Property STROKE_WIDTH =
new FloatProperty ("strokeWidth") {
@Override
public void setValue(VFullPath object, float value) {
object.setStrokeWidth(value);
}
@Override
public Float get(VFullPath object) {
return object.getStrokeWidth();
}
};
private static final Property STROKE_COLOR =
new IntProperty ("strokeColor") {
@Override
public void setValue(VFullPath object, int value) {
object.setStrokeColor(value);
}
@Override
public Integer get(VFullPath object) {
return object.getStrokeColor();
}
};
private static final Property STROKE_ALPHA =
new FloatProperty ("strokeAlpha") {
@Override
public void setValue(VFullPath object, float value) {
object.setStrokeAlpha(value);
}
@Override
public Float get(VFullPath object) {
return object.getStrokeAlpha();
}
};
private static final Property FILL_COLOR =
new IntProperty("fillColor") {
@Override
public void setValue(VFullPath object, int value) {
object.setFillColor(value);
}
@Override
public Integer get(VFullPath object) {
return object.getFillColor();
}
};
private static final Property FILL_ALPHA =
new FloatProperty ("fillAlpha") {
@Override
public void setValue(VFullPath object, float value) {
object.setFillAlpha(value);
}
@Override
public Float get(VFullPath object) {
return object.getFillAlpha();
}
};
private static final Property TRIM_PATH_START =
new FloatProperty ("trimPathStart") {
@Override
public void setValue(VFullPath object, float value) {
object.setTrimPathStart(value);
}
@Override
public Float get(VFullPath object) {
return object.getTrimPathStart();
}
};
private static final Property TRIM_PATH_END =
new FloatProperty ("trimPathEnd") {
@Override
public void setValue(VFullPath object, float value) {
object.setTrimPathEnd(value);
}
@Override
public Float get(VFullPath object) {
return object.getTrimPathEnd();
}
};
private static final Property TRIM_PATH_OFFSET =
new FloatProperty ("trimPathOffset") {
@Override
public void setValue(VFullPath object, float value) {
object.setTrimPathOffset(value);
}
@Override
public Float get(VFullPath object) {
return object.getTrimPathOffset();
}
};
private final static HashMap sPropertyMap
= new HashMap () {
{
put("strokeWidth", STROKE_WIDTH);
put("strokeColor", STROKE_COLOR);
put("strokeAlpha", STROKE_ALPHA);
put("fillColor", FILL_COLOR);
put("fillAlpha", FILL_ALPHA);
put("trimPathStart", TRIM_PATH_START);
put("trimPathEnd", TRIM_PATH_END);
put("trimPathOffset", TRIM_PATH_OFFSET);
}
};
// Temp array to store property data obtained from native getter.
private byte[] mPropertyData;
/////////////////////////////////////////////////////
// Variables below need to be copied (deep copy if applicable) for mutation.
private int[] mThemeAttrs;
ComplexColor mStrokeColors = null;
ComplexColor mFillColors = null;
private final long mNativePtr;
public VFullPath() {
mNativePtr = nCreateFullPath();
}
public VFullPath(VFullPath copy) {
super(copy);
mNativePtr = nCreateFullPath(copy.mNativePtr);
mThemeAttrs = copy.mThemeAttrs;
mStrokeColors = copy.mStrokeColors;
mFillColors = copy.mFillColors;
}
Property getProperty(String propertyName) {
Property p = super.getProperty(propertyName);
if (p != null) {
return p;
}
if (sPropertyMap.containsKey(propertyName)) {
return sPropertyMap.get(propertyName);
} else {
// property not found
return null;
}
}
int getPropertyIndex(String propertyName) {
if (!sPropertyIndexMap.containsKey(propertyName)) {
return -1;
} else {
return sPropertyIndexMap.get(propertyName);
}
}
@Override
public boolean onStateChange(int[] stateSet) {
boolean changed = false;
if (mStrokeColors != null && mStrokeColors instanceof ColorStateList) {
final int oldStrokeColor = getStrokeColor();
final int newStrokeColor =
((ColorStateList) mStrokeColors).getColorForState(stateSet, oldStrokeColor);
changed |= oldStrokeColor != newStrokeColor;
if (oldStrokeColor != newStrokeColor) {
nSetStrokeColor(mNativePtr, newStrokeColor);
}
}
if (mFillColors != null && mFillColors instanceof ColorStateList) {
final int oldFillColor = getFillColor();
final int newFillColor = ((ColorStateList) mFillColors).getColorForState(stateSet, oldFillColor);
changed |= oldFillColor != newFillColor;
if (oldFillColor != newFillColor) {
nSetFillColor(mNativePtr, newFillColor);
}
}
return changed;
}
@Override
public boolean isStateful() {
return mStrokeColors != null || mFillColors != null;
}
@Override
public boolean hasFocusStateSpecified() {
return (mStrokeColors != null && mStrokeColors instanceof ColorStateList &&
((ColorStateList) mStrokeColors).hasFocusStateSpecified()) &&
(mFillColors != null && mFillColors instanceof ColorStateList &&
((ColorStateList) mFillColors).hasFocusStateSpecified());
}
@Override
int getNativeSize() {
return NATIVE_ALLOCATION_SIZE;
}
@Override
public long getNativePtr() {
return mNativePtr;
}
@Override
public void inflate(Resources r, AttributeSet attrs, Theme theme) {
final TypedArray a = obtainAttributes(r, theme, attrs,
R.styleable.VectorDrawablePath);
updateStateFromTypedArray(a);
a.recycle();
}
private void updateStateFromTypedArray(TypedArray a) {
int byteCount = TOTAL_PROPERTY_COUNT * 4;
if (mPropertyData == null) {
// Lazy initialization: If the path is created through copy constructor, this may
// never get called.
mPropertyData = new byte[byteCount];
}
// The bulk getters/setters of property data (e.g. stroke width, color, etc) allows us
// to pull current values from native and store modifications with only two methods,
// minimizing JNI overhead.
boolean success = nGetFullPathProperties(mNativePtr, mPropertyData, byteCount);
if (!success) {
throw new RuntimeException("Error: inconsistent property count");
}
ByteBuffer properties = ByteBuffer.wrap(mPropertyData);
properties.order(ByteOrder.nativeOrder());
float strokeWidth = properties.getFloat(STROKE_WIDTH_INDEX * 4);
int strokeColor = properties.getInt(STROKE_COLOR_INDEX * 4);
float strokeAlpha = properties.getFloat(STROKE_ALPHA_INDEX * 4);
int fillColor = properties.getInt(FILL_COLOR_INDEX * 4);
float fillAlpha = properties.getFloat(FILL_ALPHA_INDEX * 4);
float trimPathStart = properties.getFloat(TRIM_PATH_START_INDEX * 4);
float trimPathEnd = properties.getFloat(TRIM_PATH_END_INDEX * 4);
float trimPathOffset = properties.getFloat(TRIM_PATH_OFFSET_INDEX * 4);
int strokeLineCap = properties.getInt(STROKE_LINE_CAP_INDEX * 4);
int strokeLineJoin = properties.getInt(STROKE_LINE_JOIN_INDEX * 4);
float strokeMiterLimit = properties.getFloat(STROKE_MITER_LIMIT_INDEX * 4);
int fillType = properties.getInt(FILL_TYPE_INDEX * 4);
Shader fillGradient = null;
Shader strokeGradient = null;
// Account for any configuration changes.
mChangingConfigurations |= a.getChangingConfigurations();
// Extract the theme attributes, if any.
mThemeAttrs = a.extractThemeAttrs();
final String pathName = a.getString(R.styleable.VectorDrawablePath_name);
if (pathName != null) {
mPathName = pathName;
nSetName(mNativePtr, mPathName);
}
final String pathString = a.getString(R.styleable.VectorDrawablePath_pathData);
if (pathString != null) {
mPathData = new PathParser.PathData(pathString);
nSetPathString(mNativePtr, pathString, pathString.length());
}
final ComplexColor fillColors = a.getComplexColor(
R.styleable.VectorDrawablePath_fillColor);
if (fillColors != null) {
// If the colors is a gradient color, or the color state list is stateful, keep the
// colors information. Otherwise, discard the colors and keep the default color.
if (fillColors instanceof GradientColor) {
mFillColors = fillColors;
fillGradient = ((GradientColor) fillColors).getShader();
} else if (fillColors.isStateful() || fillColors.canApplyTheme()) {
mFillColors = fillColors;
} else {
mFillColors = null;
}
fillColor = fillColors.getDefaultColor();
}
final ComplexColor strokeColors = a.getComplexColor(
R.styleable.VectorDrawablePath_strokeColor);
if (strokeColors != null) {
// If the colors is a gradient color, or the color state list is stateful, keep the
// colors information. Otherwise, discard the colors and keep the default color.
if (strokeColors instanceof GradientColor) {
mStrokeColors = strokeColors;
strokeGradient = ((GradientColor) strokeColors).getShader();
} else if (strokeColors.isStateful() || strokeColors.canApplyTheme()) {
mStrokeColors = strokeColors;
} else {
mStrokeColors = null;
}
strokeColor = strokeColors.getDefaultColor();
}
// Update the gradient info, even if the gradiet is null.
nUpdateFullPathFillGradient(mNativePtr,
fillGradient != null ? fillGradient.getNativeInstance() : 0);
nUpdateFullPathStrokeGradient(mNativePtr,
strokeGradient != null ? strokeGradient.getNativeInstance() : 0);
fillAlpha = a.getFloat(R.styleable.VectorDrawablePath_fillAlpha, fillAlpha);
strokeLineCap = a.getInt(
R.styleable.VectorDrawablePath_strokeLineCap, strokeLineCap);
strokeLineJoin = a.getInt(
R.styleable.VectorDrawablePath_strokeLineJoin, strokeLineJoin);
strokeMiterLimit = a.getFloat(
R.styleable.VectorDrawablePath_strokeMiterLimit, strokeMiterLimit);
strokeAlpha = a.getFloat(R.styleable.VectorDrawablePath_strokeAlpha,
strokeAlpha);
strokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth,
strokeWidth);
trimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd,
trimPathEnd);
trimPathOffset = a.getFloat(
R.styleable.VectorDrawablePath_trimPathOffset, trimPathOffset);
trimPathStart = a.getFloat(
R.styleable.VectorDrawablePath_trimPathStart, trimPathStart);
fillType = a.getInt(R.styleable.VectorDrawablePath_fillType, fillType);
nUpdateFullPathProperties(mNativePtr, strokeWidth, strokeColor, strokeAlpha,
fillColor, fillAlpha, trimPathStart, trimPathEnd, trimPathOffset,
strokeMiterLimit, strokeLineCap, strokeLineJoin, fillType);
}
@Override
public boolean canApplyTheme() {
if (mThemeAttrs != null) {
return true;
}
boolean fillCanApplyTheme = canComplexColorApplyTheme(mFillColors);
boolean strokeCanApplyTheme = canComplexColorApplyTheme(mStrokeColors);
if (fillCanApplyTheme || strokeCanApplyTheme) {
return true;
}
return false;
}
@Override
public void applyTheme(Theme t) {
// Resolve the theme attributes directly referred by the VectorDrawable.
if (mThemeAttrs != null) {
final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath);
updateStateFromTypedArray(a);
a.recycle();
}
// Resolve the theme attributes in-directly referred by the VectorDrawable, for example,
// fillColor can refer to a color state list which itself needs to apply theme.
// And this is the reason we still want to keep partial update for the path's properties.
boolean fillCanApplyTheme = canComplexColorApplyTheme(mFillColors);
boolean strokeCanApplyTheme = canComplexColorApplyTheme(mStrokeColors);
if (fillCanApplyTheme) {
mFillColors = mFillColors.obtainForTheme(t);
if (mFillColors instanceof GradientColor) {
nUpdateFullPathFillGradient(mNativePtr,
((GradientColor) mFillColors).getShader().getNativeInstance());
} else if (mFillColors instanceof ColorStateList) {
nSetFillColor(mNativePtr, mFillColors.getDefaultColor());
}
}
if (strokeCanApplyTheme) {
mStrokeColors = mStrokeColors.obtainForTheme(t);
if (mStrokeColors instanceof GradientColor) {
nUpdateFullPathStrokeGradient(mNativePtr,
((GradientColor) mStrokeColors).getShader().getNativeInstance());
} else if (mStrokeColors instanceof ColorStateList) {
nSetStrokeColor(mNativePtr, mStrokeColors.getDefaultColor());
}
}
}
private boolean canComplexColorApplyTheme(ComplexColor complexColor) {
return complexColor != null && complexColor.canApplyTheme();
}
/* Setters and Getters, used by animator from AnimatedVectorDrawable. */
@SuppressWarnings("unused")
int getStrokeColor() {
return isTreeValid() ? nGetStrokeColor(mNativePtr) : 0;
}
@SuppressWarnings("unused")
void setStrokeColor(int strokeColor) {
mStrokeColors = null;
if (isTreeValid()) {
nSetStrokeColor(mNativePtr, strokeColor);
}
}
@SuppressWarnings("unused")
float getStrokeWidth() {
return isTreeValid() ? nGetStrokeWidth(mNativePtr) : 0;
}
@SuppressWarnings("unused")
void setStrokeWidth(float strokeWidth) {
if (isTreeValid()) {
nSetStrokeWidth(mNativePtr, strokeWidth);
}
}
@SuppressWarnings("unused")
float getStrokeAlpha() {
return isTreeValid() ? nGetStrokeAlpha(mNativePtr) : 0;
}
@SuppressWarnings("unused")
void setStrokeAlpha(float strokeAlpha) {
if (isTreeValid()) {
nSetStrokeAlpha(mNativePtr, strokeAlpha);
}
}
@SuppressWarnings("unused")
int getFillColor() {
return isTreeValid() ? nGetFillColor(mNativePtr) : 0;
}
@SuppressWarnings("unused")
void setFillColor(int fillColor) {
mFillColors = null;
if (isTreeValid()) {
nSetFillColor(mNativePtr, fillColor);
}
}
@SuppressWarnings("unused")
float getFillAlpha() {
return isTreeValid() ? nGetFillAlpha(mNativePtr) : 0;
}
@SuppressWarnings("unused")
void setFillAlpha(float fillAlpha) {
if (isTreeValid()) {
nSetFillAlpha(mNativePtr, fillAlpha);
}
}
@SuppressWarnings("unused")
float getTrimPathStart() {
return isTreeValid() ? nGetTrimPathStart(mNativePtr) : 0;
}
@SuppressWarnings("unused")
void setTrimPathStart(float trimPathStart) {
if (isTreeValid()) {
nSetTrimPathStart(mNativePtr, trimPathStart);
}
}
@SuppressWarnings("unused")
float getTrimPathEnd() {
return isTreeValid() ? nGetTrimPathEnd(mNativePtr) : 0;
}
@SuppressWarnings("unused")
void setTrimPathEnd(float trimPathEnd) {
if (isTreeValid()) {
nSetTrimPathEnd(mNativePtr, trimPathEnd);
}
}
@SuppressWarnings("unused")
float getTrimPathOffset() {
return isTreeValid() ? nGetTrimPathOffset(mNativePtr) : 0;
}
@SuppressWarnings("unused")
void setTrimPathOffset(float trimPathOffset) {
if (isTreeValid()) {
nSetTrimPathOffset(mNativePtr, trimPathOffset);
}
}
}
abstract static class VObject {
VirtualRefBasePtr mTreePtr = null;
boolean isTreeValid() {
return mTreePtr != null && mTreePtr.get() != 0;
}
void setTree(VirtualRefBasePtr ptr) {
mTreePtr = ptr;
}
abstract long getNativePtr();
abstract void inflate(Resources r, AttributeSet attrs, Theme theme);
abstract boolean canApplyTheme();
abstract void applyTheme(Theme t);
abstract boolean onStateChange(int[] state);
abstract boolean isStateful();
abstract boolean hasFocusStateSpecified();
abstract int getNativeSize();
abstract Property getProperty(String propertyName);
}
private static native int nDraw(long rendererPtr, long canvasWrapperPtr,
long colorFilterPtr, Rect bounds, boolean needsMirroring, boolean canReuseCache);
private static native boolean nGetFullPathProperties(long pathPtr, byte[] properties,
int length);
private static native void nSetName(long nodePtr, String name);
private static native boolean nGetGroupProperties(long groupPtr, float[] properties,
int length);
private static native void nSetPathString(long pathPtr, String pathString, int length);
// ------------- @FastNative ------------------
@FastNative
private static native long nCreateTree(long rootGroupPtr);
@FastNative
private static native long nCreateTreeFromCopy(long treeToCopy, long rootGroupPtr);
@FastNative
private static native void nSetRendererViewportSize(long rendererPtr, float viewportWidth,
float viewportHeight);
@FastNative
private static native boolean nSetRootAlpha(long rendererPtr, float alpha);
@FastNative
private static native float nGetRootAlpha(long rendererPtr);
@FastNative
private static native void nSetAntiAlias(long rendererPtr, boolean aa);
@FastNative
private static native void nSetAllowCaching(long rendererPtr, boolean allowCaching);
@FastNative
private static native long nCreateFullPath();
@FastNative
private static native long nCreateFullPath(long nativeFullPathPtr);
@FastNative
private static native void nUpdateFullPathProperties(long pathPtr, float strokeWidth,
int strokeColor, float strokeAlpha, int fillColor, float fillAlpha, float trimPathStart,
float trimPathEnd, float trimPathOffset, float strokeMiterLimit, int strokeLineCap,
int strokeLineJoin, int fillType);
@FastNative
private static native void nUpdateFullPathFillGradient(long pathPtr, long fillGradientPtr);
@FastNative
private static native void nUpdateFullPathStrokeGradient(long pathPtr, long strokeGradientPtr);
@FastNative
private static native long nCreateClipPath();
@FastNative
private static native long nCreateClipPath(long clipPathPtr);
@FastNative
private static native long nCreateGroup();
@FastNative
private static native long nCreateGroup(long groupPtr);
@FastNative
private static native void nUpdateGroupProperties(long groupPtr, float rotate, float pivotX,
float pivotY, float scaleX, float scaleY, float translateX, float translateY);
@FastNative
private static native void nAddChild(long groupPtr, long nodePtr);
/**
* The setters and getters below for paths and groups are here temporarily, and will be
* removed once the animation in AVD is replaced with RenderNodeAnimator, in which case the
* animation will modify these properties in native. By then no JNI hopping would be necessary
* for VD during animation, and these setters and getters will be obsolete.
*/
// Setters and getters during animation.
@FastNative
private static native float nGetRotation(long groupPtr);
@FastNative
private static native void nSetRotation(long groupPtr, float rotation);
@FastNative
private static native float nGetPivotX(long groupPtr);
@FastNative
private static native void nSetPivotX(long groupPtr, float pivotX);
@FastNative
private static native float nGetPivotY(long groupPtr);
@FastNative
private static native void nSetPivotY(long groupPtr, float pivotY);
@FastNative
private static native float nGetScaleX(long groupPtr);
@FastNative
private static native void nSetScaleX(long groupPtr, float scaleX);
@FastNative
private static native float nGetScaleY(long groupPtr);
@FastNative
private static native void nSetScaleY(long groupPtr, float scaleY);
@FastNative
private static native float nGetTranslateX(long groupPtr);
@FastNative
private static native void nSetTranslateX(long groupPtr, float translateX);
@FastNative
private static native float nGetTranslateY(long groupPtr);
@FastNative
private static native void nSetTranslateY(long groupPtr, float translateY);
// Setters and getters for VPath during animation.
@FastNative
private static native void nSetPathData(long pathPtr, long pathDataPtr);
@FastNative
private static native float nGetStrokeWidth(long pathPtr);
@FastNative
private static native void nSetStrokeWidth(long pathPtr, float width);
@FastNative
private static native int nGetStrokeColor(long pathPtr);
@FastNative
private static native void nSetStrokeColor(long pathPtr, int strokeColor);
@FastNative
private static native float nGetStrokeAlpha(long pathPtr);
@FastNative
private static native void nSetStrokeAlpha(long pathPtr, float alpha);
@FastNative
private static native int nGetFillColor(long pathPtr);
@FastNative
private static native void nSetFillColor(long pathPtr, int fillColor);
@FastNative
private static native float nGetFillAlpha(long pathPtr);
@FastNative
private static native void nSetFillAlpha(long pathPtr, float fillAlpha);
@FastNative
private static native float nGetTrimPathStart(long pathPtr);
@FastNative
private static native void nSetTrimPathStart(long pathPtr, float trimPathStart);
@FastNative
private static native float nGetTrimPathEnd(long pathPtr);
@FastNative
private static native void nSetTrimPathEnd(long pathPtr, float trimPathEnd);
@FastNative
private static native float nGetTrimPathOffset(long pathPtr);
@FastNative
private static native void nSetTrimPathOffset(long pathPtr, float trimPathOffset);
}