| /* |
| * 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.res.ColorStateList; |
| import android.content.res.Resources; |
| import android.content.res.Resources.Theme; |
| import android.content.res.TypedArray; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.ColorFilter; |
| import android.graphics.Insets; |
| import android.graphics.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PathMeasure; |
| import android.graphics.PixelFormat; |
| import android.graphics.PorterDuffColorFilter; |
| import android.graphics.Rect; |
| import android.graphics.PorterDuff.Mode; |
| import android.util.ArrayMap; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.util.LayoutDirection; |
| import android.util.Log; |
| import android.util.MathUtils; |
| import android.util.PathParser; |
| import android.util.Xml; |
| |
| import com.android.internal.R; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Stack; |
| |
| /** |
| * This lets you create a drawable based on an XML vector graphic. It can be |
| * defined in an XML file with the <code><vector></code> element. |
| * <p/> |
| * The vector drawable has the following elements: |
| * <p/> |
| * <dt><code><vector></code></dt> |
| * <dl> |
| * <dd>Used to define a vector drawable |
| * <dl> |
| * <dt><code>android:name</code></dt> |
| * <dd>Defines the name of this vector drawable.</dd> |
| * <dt><code>android:width</code></dt> |
| * <dd>Used to define the intrinsic width of the drawable. |
| * This support all the dimension units, normally specified with dp.</dd> |
| * <dt><code>android:height</code></dt> |
| * <dd>Used to define the intrinsic height the drawable. |
| * This support all the dimension units, normally specified with dp.</dd> |
| * <dt><code>android:viewportWidth</code></dt> |
| * <dd>Used to define the width of the viewport space. Viewport is basically |
| * the virtual canvas where the paths are drawn on.</dd> |
| * <dt><code>android:viewportHeight</code></dt> |
| * <dd>Used to define the height of the viewport space. Viewport is basically |
| * the virtual canvas where the paths are drawn on.</dd> |
| * <dt><code>android:tint</code></dt> |
| * <dd>The color to apply to the drawable as a tint. By default, no tint is applied.</dd> |
| * <dt><code>android:tintMode</code></dt> |
| * <dd>The Porter-Duff blending mode for the tint color. The default value is src_in.</dd> |
| * <dt><code>android:autoMirrored</code></dt> |
| * <dd>Indicates if the drawable needs to be mirrored when its layout direction is |
| * RTL (right-to-left).</dd> |
| * <dt><code>android:alpha</code></dt> |
| * <dd>The opacity of this drawable.</dd> |
| * </dl></dd> |
| * </dl> |
| * |
| * <dl> |
| * <dt><code><group></code></dt> |
| * <dd>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. |
| * <dl> |
| * <dt><code>android:name</code></dt> |
| * <dd>Defines the name of the group.</dd> |
| * <dt><code>android:rotation</code></dt> |
| * <dd>The degrees of rotation of the group.</dd> |
| * <dt><code>android:pivotX</code></dt> |
| * <dd>The X coordinate of the pivot for the scale and rotation of the group. |
| * This is defined in the viewport space.</dd> |
| * <dt><code>android:pivotY</code></dt> |
| * <dd>The Y coordinate of the pivot for the scale and rotation of the group. |
| * This is defined in the viewport space.</dd> |
| * <dt><code>android:scaleX</code></dt> |
| * <dd>The amount of scale on the X Coordinate.</dd> |
| * <dt><code>android:scaleY</code></dt> |
| * <dd>The amount of scale on the Y coordinate.</dd> |
| * <dt><code>android:translateX</code></dt> |
| * <dd>The amount of translation on the X coordinate. |
| * This is defined in the viewport space.</dd> |
| * <dt><code>android:translateY</code></dt> |
| * <dd>The amount of translation on the Y coordinate. |
| * This is defined in the viewport space.</dd> |
| * </dl></dd> |
| * </dl> |
| * |
| * <dl> |
| * <dt><code><path></code></dt> |
| * <dd>Defines paths to be drawn. |
| * <dl> |
| * <dt><code>android:name</code></dt> |
| * <dd>Defines the name of the path.</dd> |
| * <dt><code>android:pathData</code></dt> |
| * <dd>Defines path data using exactly same format as "d" attribute |
| * in the SVG's path data. This is defined in the viewport space.</dd> |
| * <dt><code>android:fillColor</code></dt> |
| * <dd>Defines the color to fill the path (none if not present).</dd> |
| * <dt><code>android:strokeColor</code></dt> |
| * <dd>Defines the color to draw the path outline (none if not present).</dd> |
| * <dt><code>android:strokeWidth</code></dt> |
| * <dd>The width a path stroke.</dd> |
| * <dt><code>android:strokeAlpha</code></dt> |
| * <dd>The opacity of a path stroke.</dd> |
| * <dt><code>android:fillAlpha</code></dt> |
| * <dd>The opacity to fill the path with.</dd> |
| * <dt><code>android:trimPathStart</code></dt> |
| * <dd>The fraction of the path to trim from the start, in the range from 0 to 1.</dd> |
| * <dt><code>android:trimPathEnd</code></dt> |
| * <dd>The fraction of the path to trim from the end, in the range from 0 to 1.</dd> |
| * <dt><code>android:trimPathOffset</code></dt> |
| * <dd>Shift trim region (allows showed region to include the start and end), in the range |
| * from 0 to 1.</dd> |
| * <dt><code>android:strokeLineCap</code></dt> |
| * <dd>Sets the linecap for a stroked path: butt, round, square.</dd> |
| * <dt><code>android:strokeLineJoin</code></dt> |
| * <dd>Sets the lineJoin for a stroked path: miter,round,bevel.</dd> |
| * <dt><code>android:strokeMiterLimit</code></dt> |
| * <dd>Sets the Miter limit for a stroked path.</dd> |
| * </dl></dd> |
| * </dl> |
| * |
| * <dl> |
| * <dt><code><clip-path></code></dt> |
| * <dd>Defines path to be the current clip. Note that the clip path only apply to |
| * the current group and its children. |
| * <dl> |
| * <dt><code>android:name</code></dt> |
| * <dd>Defines the name of the clip path.</dd> |
| * <dt><code>android:pathData</code></dt> |
| * <dd>Defines clip path using the same format as "d" attribute |
| * in the SVG's path data.</dd> |
| * </dl></dd> |
| * </dl> |
| * <li>Here is a simple VectorDrawable in this vectordrawable.xml file. |
| * <pre> |
| * <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> |
| * </pre></li> |
| */ |
| |
| 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 static final int LINECAP_BUTT = 0; |
| private static final int LINECAP_ROUND = 1; |
| private static final int LINECAP_SQUARE = 2; |
| |
| private static final int LINEJOIN_MITER = 0; |
| private static final int LINEJOIN_ROUND = 1; |
| private static final int LINEJOIN_BEVEL = 2; |
| |
| // Cap the bitmap size, such that it won't hurt the performance too much |
| // and it won't crash due to a very large scale. |
| // The drawable will look blurry above this size. |
| private static final int MAX_CACHED_BITMAP_SIZE = 2048; |
| |
| private static final boolean DBG_VECTOR_DRAWABLE = false; |
| |
| private VectorDrawableState mVectorState; |
| |
| private PorterDuffColorFilter mTintFilter; |
| private ColorFilter mColorFilter; |
| |
| private boolean mMutated; |
| |
| // AnimatedVectorDrawable needs to turn off the cache all the time, otherwise, |
| // caching the bitmap by default is allowed. |
| private boolean mAllowCaching = true; |
| |
| // 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 mDpiScaleInsets = Insets.NONE; |
| |
| // Temp variable, only for saving "new" operation at the draw() time. |
| private final float[] mTmpFloats = new float[9]; |
| private final Matrix mTmpMatrix = new Matrix(); |
| private final Rect mTmpBounds = new Rect(); |
| |
| public VectorDrawable() { |
| this(null, null); |
| } |
| |
| private VectorDrawable(@NonNull VectorDrawableState state, @Nullable Resources res) { |
| if (state == null) { |
| mVectorState = new VectorDrawableState(); |
| } else { |
| mVectorState = state; |
| mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode); |
| } |
| updateDimensionInfo(res, false); |
| } |
| |
| @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.mVPathRenderer.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); |
| |
| // The imageView can scale the canvas in different ways, in order to |
| // avoid blurry scaling, we have to draw into a bitmap with exact pixel |
| // size first. This bitmap size is determined by the bounds and the |
| // canvas scale. |
| canvas.getMatrix(mTmpMatrix); |
| mTmpMatrix.getValues(mTmpFloats); |
| float canvasScaleX = Math.abs(mTmpFloats[Matrix.MSCALE_X]); |
| float canvasScaleY = Math.abs(mTmpFloats[Matrix.MSCALE_Y]); |
| int scaledWidth = (int) (mTmpBounds.width() * canvasScaleX); |
| int scaledHeight = (int) (mTmpBounds.height() * canvasScaleY); |
| scaledWidth = Math.min(MAX_CACHED_BITMAP_SIZE, scaledWidth); |
| scaledHeight = Math.min(MAX_CACHED_BITMAP_SIZE, scaledHeight); |
| |
| if (scaledWidth <= 0 || scaledHeight <= 0) { |
| return; |
| } |
| |
| final int saveCount = canvas.save(); |
| canvas.translate(mTmpBounds.left, mTmpBounds.top); |
| |
| // Handle RTL mirroring. |
| final boolean needMirroring = needMirroring(); |
| if (needMirroring) { |
| canvas.translate(mTmpBounds.width(), 0); |
| canvas.scale(-1.0f, 1.0f); |
| } |
| |
| // At this point, canvas has been translated to the right position. |
| // And we use this bound for the destination rect for the drawBitmap, so |
| // we offset to (0, 0); |
| mTmpBounds.offsetTo(0, 0); |
| |
| mVectorState.createCachedBitmapIfNeeded(scaledWidth, scaledHeight); |
| if (!mAllowCaching) { |
| mVectorState.updateCachedBitmap(scaledWidth, scaledHeight); |
| } else { |
| if (!mVectorState.canReuseCache()) { |
| mVectorState.updateCachedBitmap(scaledWidth, scaledHeight); |
| mVectorState.updateCacheStates(); |
| } |
| } |
| mVectorState.drawCachedBitmapWithRootAlpha(canvas, colorFilter, mTmpBounds); |
| canvas.restoreToCount(saveCount); |
| } |
| |
| @Override |
| public int getAlpha() { |
| return mVectorState.mVPathRenderer.getRootAlpha(); |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| if (mVectorState.mVPathRenderer.getRootAlpha() != alpha) { |
| mVectorState.mVPathRenderer.setRootAlpha(alpha); |
| 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.mTint != null |
| && mVectorState.mTint.isStateful()); |
| } |
| |
| @Override |
| protected boolean onStateChange(int[] stateSet) { |
| final VectorDrawableState state = mVectorState; |
| if (state.mTint != null && state.mTintMode != null) { |
| mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode); |
| invalidateSelf(); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return mDpiScaledWidth; |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return mDpiScaledHeight; |
| } |
| |
| /** @hide */ |
| @Override |
| public Insets getOpticalInsets() { |
| return mDpiScaleInsets; |
| } |
| |
| /* |
| * Update the VectorDrawable dimension since the res can be in different Dpi now. |
| * Basically, when a new instance is created or getDimension() is called, we should update |
| * the current VectorDrawable's dimension information. |
| * Only after updateStateFromTypedArray() is called, we should called this and update the |
| * constant state's dpi info, i.e. updateConstantStateDensity == true. |
| */ |
| void updateDimensionInfo(@Nullable Resources res, boolean updateConstantStateDensity) { |
| if (res != null) { |
| final int densityDpi = res.getDisplayMetrics().densityDpi; |
| final int targetDensity = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; |
| |
| if (updateConstantStateDensity) { |
| mVectorState.mVPathRenderer.mTargetDensity = targetDensity; |
| } else { |
| final int constantStateDensity = mVectorState.mVPathRenderer.mTargetDensity; |
| if (targetDensity != constantStateDensity && constantStateDensity != 0) { |
| mDpiScaledWidth = Bitmap.scaleFromDensity( |
| (int) mVectorState.mVPathRenderer.mBaseWidth, constantStateDensity, |
| targetDensity); |
| mDpiScaledHeight = Bitmap.scaleFromDensity( |
| (int) mVectorState.mVPathRenderer.mBaseHeight,constantStateDensity, |
| targetDensity); |
| final int left = Bitmap.scaleFromDensity( |
| mVectorState.mVPathRenderer.mOpticalInsets.left, constantStateDensity, |
| targetDensity); |
| final int right = Bitmap.scaleFromDensity( |
| mVectorState.mVPathRenderer.mOpticalInsets.right, constantStateDensity, |
| targetDensity); |
| final int top = Bitmap.scaleFromDensity( |
| mVectorState.mVPathRenderer.mOpticalInsets.top, constantStateDensity, |
| targetDensity); |
| final int bottom = Bitmap.scaleFromDensity( |
| mVectorState.mVPathRenderer.mOpticalInsets.bottom, constantStateDensity, |
| targetDensity); |
| mDpiScaleInsets = Insets.of(left, top, right, bottom); |
| return; |
| } |
| } |
| } |
| // For all the other cases, like either res is null, constant state is not initialized or |
| // target density is the same as the constant state, we will just use the constant state |
| // dimensions. |
| mDpiScaledWidth = (int) mVectorState.mVPathRenderer.mBaseWidth; |
| mDpiScaledHeight = (int) mVectorState.mVPathRenderer.mBaseHeight; |
| mDpiScaleInsets = mVectorState.mVPathRenderer.mOpticalInsets; |
| } |
| |
| @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; |
| } |
| |
| if (state.mThemeAttrs != null) { |
| final TypedArray a = t.resolveAttributes( |
| state.mThemeAttrs, R.styleable.VectorDrawable); |
| try { |
| state.mCacheDirty = true; |
| updateStateFromTypedArray(a); |
| updateDimensionInfo(t.getResources(), true /* update constant state */); |
| } catch (XmlPullParserException e) { |
| throw new RuntimeException(e); |
| } finally { |
| a.recycle(); |
| } |
| } |
| |
| // Apply theme to contained color state list. |
| if (state.mTint != null && state.mTint.canApplyTheme()) { |
| state.mTint = state.mTint.obtainForTheme(t); |
| } |
| |
| final VPathRenderer path = state.mVPathRenderer; |
| if (path != null && path.canApplyTheme()) { |
| path.applyTheme(t); |
| } |
| |
| // Update local state. |
| mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode); |
| } |
| |
| /** |
| * 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.mVPathRenderer == null || |
| mVectorState.mVPathRenderer.mBaseWidth == 0 || |
| mVectorState.mVPathRenderer.mBaseHeight == 0 || |
| mVectorState.mVPathRenderer.mViewportHeight == 0 || |
| mVectorState.mVPathRenderer.mViewportWidth == 0) { |
| return 1; // fall back to 1:1 pixel mapping. |
| } |
| float intrinsicWidth = mVectorState.mVPathRenderer.mBaseWidth; |
| float intrinsicHeight = mVectorState.mVPathRenderer.mBaseHeight; |
| float viewportWidth = mVectorState.mVPathRenderer.mViewportWidth; |
| float viewportHeight = mVectorState.mVPathRenderer.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; |
| } |
| |
| private static int applyAlpha(int color, float alpha) { |
| int alphaBytes = Color.alpha(color); |
| color &= 0x00FFFFFF; |
| color |= ((int) (alphaBytes * alpha)) << 24; |
| return color; |
| } |
| |
| @Override |
| public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) |
| throws XmlPullParserException, IOException { |
| final VectorDrawableState state = mVectorState; |
| final VPathRenderer pathRenderer = new VPathRenderer(); |
| state.mVPathRenderer = pathRenderer; |
| |
| final TypedArray a = obtainAttributes(res, theme, attrs, R.styleable.VectorDrawable); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| |
| state.mCacheDirty = true; |
| inflateInternal(res, parser, attrs, theme); |
| |
| mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode); |
| updateDimensionInfo(res, true /* update constant state */); |
| } |
| |
| private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { |
| final VectorDrawableState state = mVectorState; |
| final VPathRenderer pathRenderer = state.mVPathRenderer; |
| |
| // 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); |
| |
| pathRenderer.mViewportWidth = a.getFloat( |
| R.styleable.VectorDrawable_viewportWidth, pathRenderer.mViewportWidth); |
| pathRenderer.mViewportHeight = a.getFloat( |
| R.styleable.VectorDrawable_viewportHeight, pathRenderer.mViewportHeight); |
| |
| if (pathRenderer.mViewportWidth <= 0) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| "<vector> tag requires viewportWidth > 0"); |
| } else if (pathRenderer.mViewportHeight <= 0) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| "<vector> tag requires viewportHeight > 0"); |
| } |
| |
| pathRenderer.mBaseWidth = a.getDimension( |
| R.styleable.VectorDrawable_width, pathRenderer.mBaseWidth); |
| pathRenderer.mBaseHeight = a.getDimension( |
| R.styleable.VectorDrawable_height, pathRenderer.mBaseHeight); |
| |
| if (pathRenderer.mBaseWidth <= 0) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| "<vector> tag requires width > 0"); |
| } else if (pathRenderer.mBaseHeight <= 0) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| "<vector> tag requires height > 0"); |
| } |
| |
| final int insetLeft = a.getDimensionPixelSize( |
| R.styleable.VectorDrawable_opticalInsetLeft, pathRenderer.mOpticalInsets.left); |
| final int insetTop = a.getDimensionPixelSize( |
| R.styleable.VectorDrawable_opticalInsetTop, pathRenderer.mOpticalInsets.top); |
| final int insetRight = a.getDimensionPixelSize( |
| R.styleable.VectorDrawable_opticalInsetRight, pathRenderer.mOpticalInsets.right); |
| final int insetBottom = a.getDimensionPixelSize( |
| R.styleable.VectorDrawable_opticalInsetBottom, pathRenderer.mOpticalInsets.bottom); |
| pathRenderer.mOpticalInsets = Insets.of(insetLeft, insetTop, insetRight, insetBottom); |
| |
| final float alphaInFloat = a.getFloat(R.styleable.VectorDrawable_alpha, |
| pathRenderer.getAlpha()); |
| pathRenderer.setAlpha(alphaInFloat); |
| |
| final String name = a.getString(R.styleable.VectorDrawable_name); |
| if (name != null) { |
| pathRenderer.mRootName = name; |
| pathRenderer.mVGTargetsMap.put(name, pathRenderer); |
| } |
| } |
| |
| private void inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs, |
| Theme theme) throws XmlPullParserException, IOException { |
| final VectorDrawableState state = mVectorState; |
| final VPathRenderer pathRenderer = state.mVPathRenderer; |
| 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<VGroup> groupStack = new Stack<VGroup>(); |
| groupStack.push(pathRenderer.mRootGroup); |
| |
| int eventType = parser.getEventType(); |
| while (eventType != XmlPullParser.END_DOCUMENT) { |
| 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.mChildren.add(path); |
| if (path.getPathName() != null) { |
| pathRenderer.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.mChildren.add(path); |
| if (path.getPathName() != null) { |
| pathRenderer.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.mChildren.add(newChildGroup); |
| groupStack.push(newChildGroup); |
| if (newChildGroup.getGroupName() != null) { |
| pathRenderer.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(); |
| } |
| |
| // Print the tree out for debug. |
| if (DBG_VECTOR_DRAWABLE) { |
| printGroupTree(pathRenderer.mRootGroup, 0); |
| } |
| |
| if (noPathTag) { |
| final StringBuffer tag = new StringBuffer(); |
| |
| if (tag.length() > 0) { |
| tag.append(" or "); |
| } |
| tag.append(SHAPE_PATH); |
| |
| throw new XmlPullParserException("no " + tag + " defined"); |
| } |
| } |
| |
| private void printGroupTree(VGroup currentGroup, int level) { |
| String indent = ""; |
| for (int i = 0; i < level; i++) { |
| indent += " "; |
| } |
| // Print the current node |
| Log.v(LOGTAG, indent + "current group is :" + currentGroup.getGroupName() |
| + " rotation is " + currentGroup.mRotate); |
| Log.v(LOGTAG, indent + "matrix is :" + currentGroup.getLocalMatrix().toString()); |
| // Then print all the children groups |
| for (int i = 0; i < currentGroup.mChildren.size(); i++) { |
| Object child = currentGroup.mChildren.get(i); |
| if (child instanceof VGroup) { |
| printGroupTree((VGroup) child, level + 1); |
| } |
| } |
| } |
| |
| @Override |
| public int getChangingConfigurations() { |
| return super.getChangingConfigurations() | mVectorState.getChangingConfigurations(); |
| } |
| |
| void setAllowCaching(boolean allowCaching) { |
| mAllowCaching = 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; |
| } |
| |
| private static class VectorDrawableState extends ConstantState { |
| int[] mThemeAttrs; |
| int mChangingConfigurations; |
| VPathRenderer mVPathRenderer; |
| ColorStateList mTint = null; |
| Mode mTintMode = DEFAULT_TINT_MODE; |
| boolean mAutoMirrored; |
| |
| Bitmap mCachedBitmap; |
| int[] mCachedThemeAttrs; |
| ColorStateList mCachedTint; |
| Mode mCachedTintMode; |
| int mCachedRootAlpha; |
| boolean mCachedAutoMirrored; |
| boolean mCacheDirty; |
| /** Temporary paint object used to draw cached bitmaps. */ |
| Paint mTempPaint; |
| |
| // Deep copy for mutate() or implicitly mutate. |
| public VectorDrawableState(VectorDrawableState copy) { |
| if (copy != null) { |
| mThemeAttrs = copy.mThemeAttrs; |
| mChangingConfigurations = copy.mChangingConfigurations; |
| mVPathRenderer = new VPathRenderer(copy.mVPathRenderer); |
| if (copy.mVPathRenderer.mFillPaint != null) { |
| mVPathRenderer.mFillPaint = new Paint(copy.mVPathRenderer.mFillPaint); |
| } |
| if (copy.mVPathRenderer.mStrokePaint != null) { |
| mVPathRenderer.mStrokePaint = new Paint(copy.mVPathRenderer.mStrokePaint); |
| } |
| mTint = copy.mTint; |
| mTintMode = copy.mTintMode; |
| mAutoMirrored = copy.mAutoMirrored; |
| } |
| } |
| |
| public void drawCachedBitmapWithRootAlpha(Canvas canvas, ColorFilter filter, |
| Rect originalBounds) { |
| // The bitmap's size is the same as the bounds. |
| final Paint p = getPaint(filter); |
| canvas.drawBitmap(mCachedBitmap, null, originalBounds, p); |
| } |
| |
| public boolean hasTranslucentRoot() { |
| return mVPathRenderer.getRootAlpha() < 255; |
| } |
| |
| /** |
| * @return null when there is no need for alpha paint. |
| */ |
| public Paint getPaint(ColorFilter filter) { |
| if (!hasTranslucentRoot() && filter == null) { |
| return null; |
| } |
| |
| if (mTempPaint == null) { |
| mTempPaint = new Paint(); |
| mTempPaint.setFilterBitmap(true); |
| } |
| mTempPaint.setAlpha(mVPathRenderer.getRootAlpha()); |
| mTempPaint.setColorFilter(filter); |
| return mTempPaint; |
| } |
| |
| public void updateCachedBitmap(int width, int height) { |
| mCachedBitmap.eraseColor(Color.TRANSPARENT); |
| Canvas tmpCanvas = new Canvas(mCachedBitmap); |
| mVPathRenderer.draw(tmpCanvas, width, height, null); |
| } |
| |
| public void createCachedBitmapIfNeeded(int width, int height) { |
| if (mCachedBitmap == null || !canReuseBitmap(width, height)) { |
| mCachedBitmap = Bitmap.createBitmap(width, height, |
| Bitmap.Config.ARGB_8888); |
| mCacheDirty = true; |
| } |
| |
| } |
| |
| public boolean canReuseBitmap(int width, int height) { |
| if (width == mCachedBitmap.getWidth() |
| && height == mCachedBitmap.getHeight()) { |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean canReuseCache() { |
| if (!mCacheDirty |
| && mCachedThemeAttrs == mThemeAttrs |
| && mCachedTint == mTint |
| && mCachedTintMode == mTintMode |
| && mCachedAutoMirrored == mAutoMirrored |
| && mCachedRootAlpha == mVPathRenderer.getRootAlpha()) { |
| return true; |
| } |
| 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; |
| mCachedRootAlpha = mVPathRenderer.getRootAlpha(); |
| mCachedAutoMirrored = mAutoMirrored; |
| mCacheDirty = false; |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return mThemeAttrs != null |
| || (mVPathRenderer != null && mVPathRenderer.canApplyTheme()) |
| || (mTint != null && mTint.canApplyTheme()) |
| || super.canApplyTheme(); |
| } |
| |
| public VectorDrawableState() { |
| mVPathRenderer = new VPathRenderer(); |
| } |
| |
| @Override |
| public Drawable newDrawable() { |
| return new VectorDrawable(this, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(Resources res) { |
| return new VectorDrawable(this, res); |
| } |
| |
| @Override |
| public int getChangingConfigurations() { |
| return mChangingConfigurations |
| | (mTint != null ? mTint.getChangingConfigurations() : 0); |
| } |
| } |
| |
| private static class VPathRenderer { |
| /* Right now the internal data structure is organized as a tree. |
| * Each node can be a group node, or a path. |
| * A group node can have groups or paths as children, but a path node has |
| * no children. |
| * One example can be: |
| * Root Group |
| * / | \ |
| * Group Path Group |
| * / \ | |
| * Path Path Path |
| * |
| */ |
| // Variables that only used temporarily inside the draw() call, so there |
| // is no need for deep copying. |
| private final Path mPath; |
| private final Path mRenderPath; |
| private final Matrix mFinalPathMatrix = new Matrix(); |
| |
| private Paint mStrokePaint; |
| private Paint mFillPaint; |
| private PathMeasure mPathMeasure; |
| |
| ///////////////////////////////////////////////////// |
| // Variables below need to be copied (deep copy if applicable) for mutation. |
| private int mChangingConfigurations; |
| private final VGroup mRootGroup; |
| float mBaseWidth = 0; |
| float mBaseHeight = 0; |
| float mViewportWidth = 0; |
| float mViewportHeight = 0; |
| Insets mOpticalInsets = Insets.NONE; |
| int mRootAlpha = 0xFF; |
| String mRootName = null; |
| |
| int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; |
| |
| final ArrayMap<String, Object> mVGTargetsMap = new ArrayMap<String, Object>(); |
| |
| public VPathRenderer() { |
| mRootGroup = new VGroup(); |
| mPath = new Path(); |
| mRenderPath = new Path(); |
| } |
| |
| public void setRootAlpha(int alpha) { |
| mRootAlpha = alpha; |
| } |
| |
| public int getRootAlpha() { |
| return mRootAlpha; |
| } |
| |
| // setAlpha() and getAlpha() are used mostly for animation purpose, since |
| // Animator like to use alpha from 0 to 1. |
| public void setAlpha(float alpha) { |
| setRootAlpha((int) (alpha * 255)); |
| } |
| |
| @SuppressWarnings("unused") |
| public float getAlpha() { |
| return getRootAlpha() / 255.0f; |
| } |
| |
| public VPathRenderer(VPathRenderer copy) { |
| mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap); |
| mPath = new Path(copy.mPath); |
| mRenderPath = new Path(copy.mRenderPath); |
| mBaseWidth = copy.mBaseWidth; |
| mBaseHeight = copy.mBaseHeight; |
| mViewportWidth = copy.mViewportWidth; |
| mViewportHeight = copy.mViewportHeight; |
| mOpticalInsets = copy.mOpticalInsets; |
| mChangingConfigurations = copy.mChangingConfigurations; |
| mRootAlpha = copy.mRootAlpha; |
| mRootName = copy.mRootName; |
| mTargetDensity = copy.mTargetDensity; |
| if (copy.mRootName != null) { |
| mVGTargetsMap.put(copy.mRootName, this); |
| } |
| } |
| |
| public boolean canApplyTheme() { |
| // If one of the paths can apply theme, then return true; |
| return recursiveCanApplyTheme(mRootGroup); |
| } |
| |
| private boolean recursiveCanApplyTheme(VGroup currentGroup) { |
| // We can do a tree traverse here, if there is one path return true, |
| // then we return true for the whole tree. |
| final ArrayList<Object> children = currentGroup.mChildren; |
| |
| for (int i = 0; i < children.size(); i++) { |
| Object child = children.get(i); |
| if (child instanceof VGroup) { |
| VGroup childGroup = (VGroup) child; |
| if (childGroup.canApplyTheme() |
| || recursiveCanApplyTheme(childGroup)) { |
| return true; |
| } |
| } else if (child instanceof VPath) { |
| VPath childPath = (VPath) child; |
| if (childPath.canApplyTheme()) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| public void applyTheme(Theme t) { |
| // Apply theme to every path of the tree. |
| recursiveApplyTheme(mRootGroup, t); |
| } |
| |
| private void recursiveApplyTheme(VGroup currentGroup, Theme t) { |
| // We can do a tree traverse here, apply theme to all paths which |
| // can apply theme. |
| final ArrayList<Object> children = currentGroup.mChildren; |
| for (int i = 0; i < children.size(); i++) { |
| Object child = children.get(i); |
| if (child instanceof VGroup) { |
| VGroup childGroup = (VGroup) child; |
| if (childGroup.canApplyTheme()) { |
| childGroup.applyTheme(t); |
| } |
| recursiveApplyTheme(childGroup, t); |
| } else if (child instanceof VPath) { |
| VPath childPath = (VPath) child; |
| if (childPath.canApplyTheme()) { |
| childPath.applyTheme(t); |
| } |
| } |
| } |
| } |
| |
| private void drawGroupTree(VGroup currentGroup, Matrix currentMatrix, |
| Canvas canvas, int w, int h, ColorFilter filter) { |
| // Calculate current group's matrix by preConcat the parent's and |
| // and the current one on the top of the stack. |
| // Basically the Mfinal = Mviewport * M0 * M1 * M2; |
| // Mi the local matrix at level i of the group tree. |
| currentGroup.mStackedMatrix.set(currentMatrix); |
| currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix); |
| |
| // Save the current clip information, which is local to this group. |
| canvas.save(); |
| // Draw the group tree in the same order as the XML file. |
| for (int i = 0; i < currentGroup.mChildren.size(); i++) { |
| Object child = currentGroup.mChildren.get(i); |
| if (child instanceof VGroup) { |
| VGroup childGroup = (VGroup) child; |
| drawGroupTree(childGroup, currentGroup.mStackedMatrix, |
| canvas, w, h, filter); |
| } else if (child instanceof VPath) { |
| VPath childPath = (VPath) child; |
| drawPath(currentGroup, childPath, canvas, w, h, filter); |
| } |
| } |
| canvas.restore(); |
| } |
| |
| public void draw(Canvas canvas, int w, int h, ColorFilter filter) { |
| // Travese the tree in pre-order to draw. |
| drawGroupTree(mRootGroup, Matrix.IDENTITY_MATRIX, canvas, w, h, filter); |
| } |
| |
| private void drawPath(VGroup vGroup, VPath vPath, Canvas canvas, int w, int h, |
| ColorFilter filter) { |
| final float scaleX = w / mViewportWidth; |
| final float scaleY = h / mViewportHeight; |
| final float minScale = Math.min(scaleX, scaleY); |
| final Matrix groupStackedMatrix = vGroup.mStackedMatrix; |
| |
| mFinalPathMatrix.set(groupStackedMatrix); |
| mFinalPathMatrix.postScale(scaleX, scaleY); |
| |
| final float matrixScale = getMatrixScale(groupStackedMatrix); |
| if (matrixScale == 0) { |
| // When either x or y is scaled to 0, we don't need to draw anything. |
| return; |
| } |
| vPath.toPath(mPath); |
| final Path path = mPath; |
| |
| mRenderPath.reset(); |
| |
| if (vPath.isClipPath()) { |
| mRenderPath.addPath(path, mFinalPathMatrix); |
| canvas.clipPath(mRenderPath); |
| } else { |
| VFullPath fullPath = (VFullPath) vPath; |
| if (fullPath.mTrimPathStart != 0.0f || fullPath.mTrimPathEnd != 1.0f) { |
| float start = (fullPath.mTrimPathStart + fullPath.mTrimPathOffset) % 1.0f; |
| float end = (fullPath.mTrimPathEnd + fullPath.mTrimPathOffset) % 1.0f; |
| |
| if (mPathMeasure == null) { |
| mPathMeasure = new PathMeasure(); |
| } |
| mPathMeasure.setPath(mPath, false); |
| |
| float len = mPathMeasure.getLength(); |
| start = start * len; |
| end = end * len; |
| path.reset(); |
| if (start > end) { |
| mPathMeasure.getSegment(start, len, path, true); |
| mPathMeasure.getSegment(0f, end, path, true); |
| } else { |
| mPathMeasure.getSegment(start, end, path, true); |
| } |
| path.rLineTo(0, 0); // fix bug in measure |
| } |
| mRenderPath.addPath(path, mFinalPathMatrix); |
| |
| if (fullPath.mFillColor != Color.TRANSPARENT) { |
| if (mFillPaint == null) { |
| mFillPaint = new Paint(); |
| mFillPaint.setStyle(Paint.Style.FILL); |
| mFillPaint.setAntiAlias(true); |
| } |
| |
| final Paint fillPaint = mFillPaint; |
| fillPaint.setColor(applyAlpha(fullPath.mFillColor, fullPath.mFillAlpha)); |
| fillPaint.setColorFilter(filter); |
| canvas.drawPath(mRenderPath, fillPaint); |
| } |
| |
| if (fullPath.mStrokeColor != Color.TRANSPARENT) { |
| if (mStrokePaint == null) { |
| mStrokePaint = new Paint(); |
| mStrokePaint.setStyle(Paint.Style.STROKE); |
| mStrokePaint.setAntiAlias(true); |
| } |
| |
| final Paint strokePaint = mStrokePaint; |
| if (fullPath.mStrokeLineJoin != null) { |
| strokePaint.setStrokeJoin(fullPath.mStrokeLineJoin); |
| } |
| |
| if (fullPath.mStrokeLineCap != null) { |
| strokePaint.setStrokeCap(fullPath.mStrokeLineCap); |
| } |
| |
| strokePaint.setStrokeMiter(fullPath.mStrokeMiterlimit); |
| strokePaint.setColor(applyAlpha(fullPath.mStrokeColor, fullPath.mStrokeAlpha)); |
| strokePaint.setColorFilter(filter); |
| final float finalStrokeScale = minScale * matrixScale; |
| strokePaint.setStrokeWidth(fullPath.mStrokeWidth * finalStrokeScale); |
| canvas.drawPath(mRenderPath, strokePaint); |
| } |
| } |
| } |
| |
| private float getMatrixScale(Matrix groupStackedMatrix) { |
| // Given unit vectors A = (0, 1) and B = (1, 0). |
| // After matrix mapping, we got A' and B'. Let theta = the angel b/t A' and B'. |
| // Therefore, the final scale we want is min(|A'| * sin(theta), |B'| * sin(theta)), |
| // which is (|A'| * |B'| * sin(theta)) / max (|A'|, |B'|); |
| // If max (|A'|, |B'|) = 0, that means either x or y has a scale of 0. |
| // |
| // For non-skew case, which is most of the cases, matrix scale is computing exactly the |
| // scale on x and y axis, and take the minimal of these two. |
| // For skew case, an unit square will mapped to a parallelogram. And this function will |
| // return the minimal height of the 2 bases. |
| float[] unitVectors = new float[] {0, 1, 1, 0}; |
| groupStackedMatrix.mapVectors(unitVectors); |
| float scaleX = MathUtils.mag(unitVectors[0], unitVectors[1]); |
| float scaleY = MathUtils.mag(unitVectors[2], unitVectors[3]); |
| float crossProduct = MathUtils.cross(unitVectors[0], unitVectors[1], |
| unitVectors[2], unitVectors[3]); |
| float maxScale = MathUtils.max(scaleX, scaleY); |
| |
| float matrixScale = 0; |
| if (maxScale > 0) { |
| matrixScale = MathUtils.abs(crossProduct) / maxScale; |
| } |
| if (DBG_VECTOR_DRAWABLE) { |
| Log.d(LOGTAG, "Scale x " + scaleX + " y " + scaleY + " final " + matrixScale); |
| } |
| return matrixScale; |
| } |
| } |
| |
| private static class VGroup { |
| // mStackedMatrix is only used temporarily when drawing, it combines all |
| // the parents' local matrices with the current one. |
| private final Matrix mStackedMatrix = new Matrix(); |
| |
| ///////////////////////////////////////////////////// |
| // Variables below need to be copied (deep copy if applicable) for mutation. |
| final ArrayList<Object> mChildren = new ArrayList<Object>(); |
| |
| private float mRotate = 0; |
| private float mPivotX = 0; |
| private float mPivotY = 0; |
| private float mScaleX = 1; |
| private float mScaleY = 1; |
| private float mTranslateX = 0; |
| private float mTranslateY = 0; |
| |
| // mLocalMatrix is updated based on the update of transformation information, |
| // either parsed from the XML or by animation. |
| private final Matrix mLocalMatrix = new Matrix(); |
| private int mChangingConfigurations; |
| private int[] mThemeAttrs; |
| private String mGroupName = null; |
| |
| public VGroup(VGroup copy, ArrayMap<String, Object> targetsMap) { |
| mRotate = copy.mRotate; |
| mPivotX = copy.mPivotX; |
| mPivotY = copy.mPivotY; |
| mScaleX = copy.mScaleX; |
| mScaleY = copy.mScaleY; |
| mTranslateX = copy.mTranslateX; |
| mTranslateY = copy.mTranslateY; |
| mThemeAttrs = copy.mThemeAttrs; |
| mGroupName = copy.mGroupName; |
| mChangingConfigurations = copy.mChangingConfigurations; |
| if (mGroupName != null) { |
| targetsMap.put(mGroupName, this); |
| } |
| |
| mLocalMatrix.set(copy.mLocalMatrix); |
| |
| final ArrayList<Object> children = copy.mChildren; |
| for (int i = 0; i < children.size(); i++) { |
| Object copyChild = children.get(i); |
| if (copyChild instanceof VGroup) { |
| VGroup copyGroup = (VGroup) copyChild; |
| mChildren.add(new VGroup(copyGroup, targetsMap)); |
| } else { |
| VPath newPath = null; |
| 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!"); |
| } |
| mChildren.add(newPath); |
| if (newPath.mPathName != null) { |
| targetsMap.put(newPath.mPathName, newPath); |
| } |
| } |
| } |
| } |
| |
| public VGroup() { |
| } |
| |
| public String getGroupName() { |
| return mGroupName; |
| } |
| |
| public Matrix getLocalMatrix() { |
| return mLocalMatrix; |
| } |
| |
| public void inflate(Resources res, AttributeSet attrs, Theme theme) { |
| final TypedArray a = obtainAttributes(res, theme, attrs, |
| R.styleable.VectorDrawableGroup); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| } |
| |
| private void updateStateFromTypedArray(TypedArray a) { |
| // Account for any configuration changes. |
| mChangingConfigurations |= a.getChangingConfigurations(); |
| |
| // Extract the theme attributes, if any. |
| mThemeAttrs = a.extractThemeAttrs(); |
| |
| mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate); |
| mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX); |
| mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY); |
| mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX); |
| mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY); |
| mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX); |
| mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY); |
| |
| final String groupName = a.getString(R.styleable.VectorDrawableGroup_name); |
| if (groupName != null) { |
| mGroupName = groupName; |
| } |
| |
| updateLocalMatrix(); |
| } |
| |
| public boolean canApplyTheme() { |
| return mThemeAttrs != null; |
| } |
| |
| public void applyTheme(Theme t) { |
| if (mThemeAttrs == null) { |
| return; |
| } |
| |
| final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawableGroup); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| } |
| |
| private void updateLocalMatrix() { |
| // The order we apply is the same as the |
| // RenderNode.cpp::applyViewPropertyTransforms(). |
| mLocalMatrix.reset(); |
| mLocalMatrix.postTranslate(-mPivotX, -mPivotY); |
| mLocalMatrix.postScale(mScaleX, mScaleY); |
| mLocalMatrix.postRotate(mRotate, 0, 0); |
| mLocalMatrix.postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY); |
| } |
| |
| /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ |
| @SuppressWarnings("unused") |
| public float getRotation() { |
| return mRotate; |
| } |
| |
| @SuppressWarnings("unused") |
| public void setRotation(float rotation) { |
| if (rotation != mRotate) { |
| mRotate = rotation; |
| updateLocalMatrix(); |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| public float getPivotX() { |
| return mPivotX; |
| } |
| |
| @SuppressWarnings("unused") |
| public void setPivotX(float pivotX) { |
| if (pivotX != mPivotX) { |
| mPivotX = pivotX; |
| updateLocalMatrix(); |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| public float getPivotY() { |
| return mPivotY; |
| } |
| |
| @SuppressWarnings("unused") |
| public void setPivotY(float pivotY) { |
| if (pivotY != mPivotY) { |
| mPivotY = pivotY; |
| updateLocalMatrix(); |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| public float getScaleX() { |
| return mScaleX; |
| } |
| |
| @SuppressWarnings("unused") |
| public void setScaleX(float scaleX) { |
| if (scaleX != mScaleX) { |
| mScaleX = scaleX; |
| updateLocalMatrix(); |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| public float getScaleY() { |
| return mScaleY; |
| } |
| |
| @SuppressWarnings("unused") |
| public void setScaleY(float scaleY) { |
| if (scaleY != mScaleY) { |
| mScaleY = scaleY; |
| updateLocalMatrix(); |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| public float getTranslateX() { |
| return mTranslateX; |
| } |
| |
| @SuppressWarnings("unused") |
| public void setTranslateX(float translateX) { |
| if (translateX != mTranslateX) { |
| mTranslateX = translateX; |
| updateLocalMatrix(); |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| public float getTranslateY() { |
| return mTranslateY; |
| } |
| |
| @SuppressWarnings("unused") |
| public void setTranslateY(float translateY) { |
| if (translateY != mTranslateY) { |
| mTranslateY = translateY; |
| updateLocalMatrix(); |
| } |
| } |
| } |
| |
| /** |
| * Common Path information for clip path and normal path. |
| */ |
| private static class VPath { |
| protected PathParser.PathDataNode[] mNodes = null; |
| String mPathName; |
| int mChangingConfigurations; |
| |
| public VPath() { |
| // Empty constructor. |
| } |
| |
| public VPath(VPath copy) { |
| mPathName = copy.mPathName; |
| mChangingConfigurations = copy.mChangingConfigurations; |
| mNodes = PathParser.deepCopyNodes(copy.mNodes); |
| } |
| |
| public void toPath(Path path) { |
| path.reset(); |
| if (mNodes != null) { |
| PathParser.PathDataNode.nodesToPath(mNodes, path); |
| } |
| } |
| |
| public String getPathName() { |
| return mPathName; |
| } |
| |
| public boolean canApplyTheme() { |
| return false; |
| } |
| |
| public void applyTheme(Theme t) { |
| } |
| |
| public boolean isClipPath() { |
| return false; |
| } |
| |
| /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ |
| @SuppressWarnings("unused") |
| public PathParser.PathDataNode[] getPathData() { |
| return mNodes; |
| } |
| |
| @SuppressWarnings("unused") |
| public void setPathData(PathParser.PathDataNode[] nodes) { |
| if (!PathParser.canMorph(mNodes, nodes)) { |
| // This should not happen in the middle of animation. |
| mNodes = PathParser.deepCopyNodes(nodes); |
| } else { |
| PathParser.updateNodes(mNodes, nodes); |
| } |
| } |
| } |
| |
| /** |
| * Clip path, which only has name and pathData. |
| */ |
| private static class VClipPath extends VPath { |
| public VClipPath() { |
| // Empty constructor. |
| } |
| |
| public VClipPath(VClipPath copy) { |
| super(copy); |
| } |
| |
| public void inflate(Resources r, AttributeSet attrs, Theme theme) { |
| final TypedArray a = obtainAttributes(r, theme, attrs, |
| R.styleable.VectorDrawableClipPath); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| } |
| |
| 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; |
| } |
| |
| final String pathData = a.getString(R.styleable.VectorDrawableClipPath_pathData); |
| if (pathData != null) { |
| mNodes = PathParser.createNodesFromPathData(pathData); |
| } |
| } |
| |
| @Override |
| public boolean isClipPath() { |
| return true; |
| } |
| } |
| |
| /** |
| * Normal path, which contains all the fill / paint information. |
| */ |
| private static class VFullPath extends VPath { |
| ///////////////////////////////////////////////////// |
| // Variables below need to be copied (deep copy if applicable) for mutation. |
| private int[] mThemeAttrs; |
| |
| int mStrokeColor = Color.TRANSPARENT; |
| float mStrokeWidth = 0; |
| |
| int mFillColor = Color.TRANSPARENT; |
| float mStrokeAlpha = 1.0f; |
| int mFillRule; |
| float mFillAlpha = 1.0f; |
| float mTrimPathStart = 0; |
| float mTrimPathEnd = 1; |
| float mTrimPathOffset = 0; |
| |
| Paint.Cap mStrokeLineCap = Paint.Cap.BUTT; |
| Paint.Join mStrokeLineJoin = Paint.Join.MITER; |
| float mStrokeMiterlimit = 4; |
| |
| public VFullPath() { |
| // Empty constructor. |
| } |
| |
| public VFullPath(VFullPath copy) { |
| super(copy); |
| mThemeAttrs = copy.mThemeAttrs; |
| |
| mStrokeColor = copy.mStrokeColor; |
| mStrokeWidth = copy.mStrokeWidth; |
| mStrokeAlpha = copy.mStrokeAlpha; |
| mFillColor = copy.mFillColor; |
| mFillRule = copy.mFillRule; |
| mFillAlpha = copy.mFillAlpha; |
| mTrimPathStart = copy.mTrimPathStart; |
| mTrimPathEnd = copy.mTrimPathEnd; |
| mTrimPathOffset = copy.mTrimPathOffset; |
| |
| mStrokeLineCap = copy.mStrokeLineCap; |
| mStrokeLineJoin = copy.mStrokeLineJoin; |
| mStrokeMiterlimit = copy.mStrokeMiterlimit; |
| } |
| |
| private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) { |
| switch (id) { |
| case LINECAP_BUTT: |
| return Paint.Cap.BUTT; |
| case LINECAP_ROUND: |
| return Paint.Cap.ROUND; |
| case LINECAP_SQUARE: |
| return Paint.Cap.SQUARE; |
| default: |
| return defValue; |
| } |
| } |
| |
| private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) { |
| switch (id) { |
| case LINEJOIN_MITER: |
| return Paint.Join.MITER; |
| case LINEJOIN_ROUND: |
| return Paint.Join.ROUND; |
| case LINEJOIN_BEVEL: |
| return Paint.Join.BEVEL; |
| default: |
| return defValue; |
| } |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return mThemeAttrs != null; |
| } |
| |
| 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) { |
| // 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; |
| } |
| |
| final String pathData = a.getString(R.styleable.VectorDrawablePath_pathData); |
| if (pathData != null) { |
| mNodes = PathParser.createNodesFromPathData(pathData); |
| } |
| |
| mFillColor = a.getColor(R.styleable.VectorDrawablePath_fillColor, |
| mFillColor); |
| mFillAlpha = a.getFloat(R.styleable.VectorDrawablePath_fillAlpha, |
| mFillAlpha); |
| mStrokeLineCap = getStrokeLineCap(a.getInt( |
| R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap); |
| mStrokeLineJoin = getStrokeLineJoin(a.getInt( |
| R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin); |
| mStrokeMiterlimit = a.getFloat( |
| R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit); |
| mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_strokeColor, |
| mStrokeColor); |
| mStrokeAlpha = a.getFloat(R.styleable.VectorDrawablePath_strokeAlpha, |
| mStrokeAlpha); |
| mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, |
| mStrokeWidth); |
| mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, |
| mTrimPathEnd); |
| mTrimPathOffset = a.getFloat( |
| R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset); |
| mTrimPathStart = a.getFloat( |
| R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart); |
| } |
| |
| @Override |
| public void applyTheme(Theme t) { |
| if (mThemeAttrs == null) { |
| return; |
| } |
| |
| final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath); |
| updateStateFromTypedArray(a); |
| a.recycle(); |
| } |
| |
| /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ |
| @SuppressWarnings("unused") |
| int getStrokeColor() { |
| return mStrokeColor; |
| } |
| |
| @SuppressWarnings("unused") |
| void setStrokeColor(int strokeColor) { |
| mStrokeColor = strokeColor; |
| } |
| |
| @SuppressWarnings("unused") |
| float getStrokeWidth() { |
| return mStrokeWidth; |
| } |
| |
| @SuppressWarnings("unused") |
| void setStrokeWidth(float strokeWidth) { |
| mStrokeWidth = strokeWidth; |
| } |
| |
| @SuppressWarnings("unused") |
| float getStrokeAlpha() { |
| return mStrokeAlpha; |
| } |
| |
| @SuppressWarnings("unused") |
| void setStrokeAlpha(float strokeAlpha) { |
| mStrokeAlpha = strokeAlpha; |
| } |
| |
| @SuppressWarnings("unused") |
| int getFillColor() { |
| return mFillColor; |
| } |
| |
| @SuppressWarnings("unused") |
| void setFillColor(int fillColor) { |
| mFillColor = fillColor; |
| } |
| |
| @SuppressWarnings("unused") |
| float getFillAlpha() { |
| return mFillAlpha; |
| } |
| |
| @SuppressWarnings("unused") |
| void setFillAlpha(float fillAlpha) { |
| mFillAlpha = fillAlpha; |
| } |
| |
| @SuppressWarnings("unused") |
| float getTrimPathStart() { |
| return mTrimPathStart; |
| } |
| |
| @SuppressWarnings("unused") |
| void setTrimPathStart(float trimPathStart) { |
| mTrimPathStart = trimPathStart; |
| } |
| |
| @SuppressWarnings("unused") |
| float getTrimPathEnd() { |
| return mTrimPathEnd; |
| } |
| |
| @SuppressWarnings("unused") |
| void setTrimPathEnd(float trimPathEnd) { |
| mTrimPathEnd = trimPathEnd; |
| } |
| |
| @SuppressWarnings("unused") |
| float getTrimPathOffset() { |
| return mTrimPathOffset; |
| } |
| |
| @SuppressWarnings("unused") |
| void setTrimPathOffset(float trimPathOffset) { |
| mTrimPathOffset = trimPathOffset; |
| } |
| } |
| } |