/* * 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.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.Canvas; import android.graphics.ColorFilter; import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.Shader; 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 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; import dalvik.system.VMRuntime; /** * 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.
*
Animatable : No.
*
android:width
*
Used to define the intrinsic width of the drawable. * This support all the dimension units, normally specified with dp.
*
Animatable : No.
*
android:height
*
Used to define the intrinsic height the drawable. * This support all the dimension units, normally specified with dp.
*
Animatable : No.
*
android:viewportWidth
*
Used to define the width of the viewport space. Viewport is basically * the virtual canvas where the paths are drawn on.
*
Animatable : No.
*
android:viewportHeight
*
Used to define the height of the viewport space. Viewport is basically * the virtual canvas where the paths are drawn on.
*
Animatable : No.
*
android:tint
*
The color to apply to the drawable as a tint. By default, no tint is applied.
*
Animatable : No.
*
android:tintMode
*
The Porter-Duff blending mode for the tint color. The default value is src_in.
*
Animatable : No.
*
android:autoMirrored
*
Indicates if the drawable needs to be mirrored when its layout direction is * RTL (right-to-left).
*
Animatable : No.
*
android:alpha
*
The opacity of this drawable.
*
Animatable : Yes.
*
*
* *
*
<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.
*
Animatable : No.
*
android:rotation
*
The degrees of rotation of the group.
*
Animatable : Yes.
*
android:pivotX
*
The X coordinate of the pivot for the scale and rotation of the group. * This is defined in the viewport space.
*
Animatable : Yes.
*
android:pivotY
*
The Y coordinate of the pivot for the scale and rotation of the group. * This is defined in the viewport space.
*
Animatable : Yes.
*
android:scaleX
*
The amount of scale on the X Coordinate.
*
Animatable : Yes.
*
android:scaleY
*
The amount of scale on the Y coordinate.
*
Animatable : Yes.
*
android:translateX
*
The amount of translation on the X coordinate. * This is defined in the viewport space.
*
Animatable : Yes.
*
android:translateY
*
The amount of translation on the Y coordinate. * This is defined in the viewport space.
*
Animatable : Yes.
*
*
* *
*
<path>
*
Defines paths to be drawn. *
*
android:name
*
Defines the name of the path.
*
Animatable : No.
*
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.
*
Animatable : Yes.
*
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.
*
Animatable : Yes.
*
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.
*
Animatable : Yes.
*
android:strokeWidth
*
The width a path stroke.
*
Animatable : Yes.
*
android:strokeAlpha
*
The opacity of a path stroke.
*
Animatable : Yes.
*
android:fillAlpha
*
The opacity to fill the path with.
*
Animatable : Yes.
*
android:trimPathStart
*
The fraction of the path to trim from the start, in the range from 0 to 1.
*
Animatable : Yes.
*
android:trimPathEnd
*
The fraction of the path to trim from the end, in the range from 0 to 1.
*
Animatable : Yes.
*
android:trimPathOffset
*
Shift trim region (allows showed region to include the start and end), in the range * from 0 to 1.
*
Animatable : Yes.
*
android:strokeLineCap
*
Sets the linecap for a stroked path: butt, round, square.
*
Animatable : No.
*
android:strokeLineJoin
*
Sets the lineJoin for a stroked path: miter,round,bevel.
*
Animatable : No.
*
android:strokeMiterLimit
*
Sets the Miter limit for a stroked path.
*
Animatable : No.
*
android:fillType
*
Sets the fillType for a path. The types can be either "evenOdd" or "nonZero". They behave the * same as SVG's "fill-rule" properties. For more details, see * FillRuleProperty
*
Animatable : No.
*
* *
* *
*
<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>
     * 
    *
  • *
  • And here is an example of linear gradient color, which is supported in SDK 24+. * See more details in {@link android.R.styleable#GradientColor} and * {@link android.R.styleable#GradientColorItem}. *
     * <gradient xmlns:android="http://schemas.android.com/apk/res/android"
     *     android:angle="90"
     *     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>
     * 
    *
  • * */ 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; private PorterDuffColorFilter mTintFilter; 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(new VectorDrawableState(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(@NonNull VectorDrawableState state, @Nullable Resources res) { mVectorState = 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; } mTintFilter = updateTintFilter(mTintFilter, mVectorState.mTint, mVectorState.mTintMode); } @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; } 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 ? mTintFilter : mColorFilter); final long colorFilterNativeInstance = colorFilter == null ? 0 : colorFilter.native_instance; 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; mTintFilter = updateTintFilter(mTintFilter, tint, state.mTintMode); invalidateSelf(); } } @Override public void setTintMode(Mode tintMode) { final VectorDrawableState state = mVectorState; if (state.mTintMode != tintMode) { state.mTintMode = tintMode; mTintFilter = updateTintFilter(mTintFilter, state.mTint, tintMode); invalidateSelf(); } } @Override public boolean isStateful() { return super.isStateful() || (mVectorState != null && mVectorState.isStateful()); } @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.mTintMode != null) { mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode); changed = true; } return changed; } @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; } /** @hide */ @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( (int) mVectorState.mBaseWidth, sourceDensity, targetDensity, true); mDpiScaledHeight = Drawable.scaleFromDensity( (int) 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 = (int) mVectorState.mBaseWidth; mDpiScaledHeight = (int) 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 { 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); } 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.mTintMode = Drawable.parseTintMode(tintMode, Mode.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.getDimension( R.styleable.VectorDrawable_width, state.mBaseWidth); state.mBaseHeight = a.getDimension( 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(); } 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(); } 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; Mode mTintMode = DEFAULT_TINT_MODE; boolean mAutoMirrored; float mBaseWidth = 0; float 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; Mode mCachedTintMode; 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; mTintMode = copy.mTintMode; 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 && mCachedTintMode == mTintMode && 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; mCachedTintMode = mTintMode; 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()); } 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); mBaseHeight = Drawable.scaleFromDensity(mBaseHeight, sourceDensity, targetDensity); 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 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") public void setRotation(float rotation) { if (isTreeValid()) { nSetRotation(mNativePtr, rotation); } } @SuppressWarnings("unused") public float getPivotX() { return isTreeValid() ? nGetPivotX(mNativePtr) : 0; } @SuppressWarnings("unused") public void setPivotX(float pivotX) { if (isTreeValid()) { nSetPivotX(mNativePtr, pivotX); } } @SuppressWarnings("unused") public float getPivotY() { return isTreeValid() ? nGetPivotY(mNativePtr) : 0; } @SuppressWarnings("unused") 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") public void setTranslateX(float translateX) { if (isTreeValid()) { nSetTranslateX(mNativePtr, translateX); } } @SuppressWarnings("unused") public float getTranslateY() { return isTreeValid() ? nGetTranslateY(mNativePtr) : 0; } @SuppressWarnings("unused") 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 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 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()) { 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()) { 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 int getNativeSize(); abstract Property getProperty(String propertyName); } private static native long nCreateTree(long rootGroupPtr); private static native long nCreateTreeFromCopy(long treeToCopy, long rootGroupPtr); private static native void nSetRendererViewportSize(long rendererPtr, float viewportWidth, float viewportHeight); private static native boolean nSetRootAlpha(long rendererPtr, float alpha); private static native float nGetRootAlpha(long rendererPtr); private static native void nSetAllowCaching(long rendererPtr, boolean allowCaching); private static native int nDraw(long rendererPtr, long canvasWrapperPtr, long colorFilterPtr, Rect bounds, boolean needsMirroring, boolean canReuseCache); private static native long nCreateFullPath(); private static native long nCreateFullPath(long nativeFullPathPtr); private static native boolean nGetFullPathProperties(long pathPtr, byte[] properties, int length); 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); private static native void nUpdateFullPathFillGradient(long pathPtr, long fillGradientPtr); private static native void nUpdateFullPathStrokeGradient(long pathPtr, long strokeGradientPtr); private static native long nCreateClipPath(); private static native long nCreateClipPath(long clipPathPtr); private static native long nCreateGroup(); private static native long nCreateGroup(long groupPtr); private static native void nSetName(long nodePtr, String name); private static native boolean nGetGroupProperties(long groupPtr, float[] properties, int length); private static native void nUpdateGroupProperties(long groupPtr, float rotate, float pivotX, float pivotY, float scaleX, float scaleY, float translateX, float translateY); private static native void nAddChild(long groupPtr, long nodePtr); private static native void nSetPathString(long pathPtr, String pathString, int length); /** * 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. private static native float nGetRotation(long groupPtr); private static native void nSetRotation(long groupPtr, float rotation); private static native float nGetPivotX(long groupPtr); private static native void nSetPivotX(long groupPtr, float pivotX); private static native float nGetPivotY(long groupPtr); private static native void nSetPivotY(long groupPtr, float pivotY); private static native float nGetScaleX(long groupPtr); private static native void nSetScaleX(long groupPtr, float scaleX); private static native float nGetScaleY(long groupPtr); private static native void nSetScaleY(long groupPtr, float scaleY); private static native float nGetTranslateX(long groupPtr); private static native void nSetTranslateX(long groupPtr, float translateX); private static native float nGetTranslateY(long groupPtr); private static native void nSetTranslateY(long groupPtr, float translateY); // Setters and getters for VPath during animation. private static native void nSetPathData(long pathPtr, long pathDataPtr); private static native float nGetStrokeWidth(long pathPtr); private static native void nSetStrokeWidth(long pathPtr, float width); private static native int nGetStrokeColor(long pathPtr); private static native void nSetStrokeColor(long pathPtr, int strokeColor); private static native float nGetStrokeAlpha(long pathPtr); private static native void nSetStrokeAlpha(long pathPtr, float alpha); private static native int nGetFillColor(long pathPtr); private static native void nSetFillColor(long pathPtr, int fillColor); private static native float nGetFillAlpha(long pathPtr); private static native void nSetFillAlpha(long pathPtr, float fillAlpha); private static native float nGetTrimPathStart(long pathPtr); private static native void nSetTrimPathStart(long pathPtr, float trimPathStart); private static native float nGetTrimPathEnd(long pathPtr); private static native void nSetTrimPathEnd(long pathPtr, float trimPathEnd); private static native float nGetTrimPathOffset(long pathPtr); private static native void nSetTrimPathOffset(long pathPtr, float trimPathOffset); }