| /* |
| * 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.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.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PathMeasure; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.Region; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.Xml; |
| |
| import com.android.internal.R; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlPullParserFactory; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| |
| /** |
| * 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/> |
| * <dl> |
| * <dt><code><vector></code></dt> |
| * <dd>Used to defined a vector drawable</dd> |
| * <dt><code><size></code></dt> |
| * <dd>Used to defined the intrinsic Width Height size of the drawable using |
| * <code>android:width</code> and <code>android:height</code></dd> |
| * <dt><code><viewport></code></dt> |
| * <dd>Used to defined the size of the virtual canvas the paths are drawn on. |
| * The size is defined using the attributes <code>android:viewportHeight</code> |
| * <code>android:viewportWidth</code></dd> |
| * <dt><code><group></code></dt> |
| * <dd>Defines a group of paths or subgroups, plus transformation information.</dd> |
| * <dt><code><path></code></dt> |
| * <dd>Defines paths to be drawn. |
| * <dl> |
| * <dt><code>android:name</code> |
| * <dd>Defines the name of the path.</dd></dt> |
| * <dt><code>android:pathData</code> |
| * <dd>Defines path string. This is using exactly same format as "d" attribute |
| * in the SVG's path data</dd></dt> |
| * <dt><code>android:fill</code> |
| * <dd>Defines the color to fill the path (none if not present).</dd></dt> |
| * <dt><code>android:stroke</code> |
| * <dd>Defines the color to draw the path outline (none if not present).</dd> |
| * </dt> |
| * <dt><code>android:strokeWidth</code> |
| * <dd>The width a path stroke</dd></dt> |
| * <dt><code>android:strokeOpacity</code> |
| * <dd>The opacity of a path stroke</dd></dt> |
| * <dt><code>android:rotation</code> |
| * <dd>The amount to rotation the path stroke.</dd></dt> |
| * <dt><code>android:pivotX</code> |
| * <dd>The X coordinate of the center of rotation of a path</dd></dt> |
| * <dt><code>android:pivotY</code> |
| * <dd>The Y coordinate of the center of rotation of a path</dd></dt> |
| * <dt><code>android:fillOpacity</code> |
| * <dd>The opacity to fill the path with</dd></dt> |
| * <dt><code>android:trimPathStart</code> |
| * <dd>The fraction of the path to trim from the start from 0 to 1</dd></dt> |
| * <dt><code>android:trimPathEnd</code> |
| * <dd>The fraction of the path to trim from the end from 0 to 1</dd></dt> |
| * <dt><code>android:trimPathOffset</code> |
| * <dd>Shift trim region (allows showed region to include the start and end) |
| * from 0 to 1</dd></dt> |
| * <dt><code>android:clipToPath</code> |
| * <dd>Path will set the clip path</dd></dt> |
| * <dt><code>android:strokeLineCap</code> |
| * <dd>Sets the linecap for a stroked path: butt, round, square</dd></dt> |
| * <dt><code>android:strokeLineJoin</code> |
| * <dd>Sets the lineJoin for a stroked path: miter,round,bevel</dd></dt> |
| * <dt><code>android:strokeMiterLimit</code> |
| * <dd>Sets the Miter limit for a stroked path</dd></dt> |
| * </dl> |
| * </dd> |
| */ |
| public class VectorDrawable extends Drawable { |
| private static final String LOGTAG = VectorDrawable.class.getSimpleName(); |
| |
| private static final String SHAPE_SIZE = "size"; |
| private static final String SHAPE_VIEWPORT = "viewport"; |
| 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; |
| |
| private final VectorDrawableState mVectorState; |
| |
| private int mAlpha = 0xFF; |
| |
| public VectorDrawable() { |
| mVectorState = new VectorDrawableState(null); |
| } |
| |
| private VectorDrawable(VectorDrawableState state, Resources res, Theme theme) { |
| mVectorState = new VectorDrawableState(state); |
| |
| if (theme != null && canApplyTheme()) { |
| applyTheme(theme); |
| } |
| } |
| |
| @Override |
| public ConstantState getConstantState() { |
| return mVectorState; |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| final int saveCount = canvas.save(); |
| final Rect bounds = getBounds(); |
| canvas.translate(bounds.left, bounds.top); |
| mVectorState.mVPathRenderer.draw(canvas, bounds.width(), bounds.height()); |
| canvas.restoreToCount(saveCount); |
| } |
| |
| @Override |
| public void setAlpha(int alpha) { |
| // TODO correct handling of transparent |
| if (mAlpha != alpha) { |
| mAlpha = alpha; |
| invalidateSelf(); |
| } |
| } |
| |
| @Override |
| public void setColorFilter(ColorFilter colorFilter) { |
| mVectorState.mVPathRenderer.setColorFilter(colorFilter); |
| invalidateSelf(); |
| } |
| |
| @Override |
| public int getOpacity() { |
| return PixelFormat.TRANSLUCENT; |
| } |
| |
| /** |
| * Sets padding for this shape, defined by a Rect object. Define the padding |
| * in the Rect object as: left, top, right, bottom. |
| */ |
| public void setPadding(Rect padding) { |
| setPadding(padding.left, padding.top, padding.right, padding.bottom); |
| } |
| |
| /** |
| * Sets padding for the shape. |
| * |
| * @param left padding for the left side (in pixels) |
| * @param top padding for the top (in pixels) |
| * @param right padding for the right side (in pixels) |
| * @param bottom padding for the bottom (in pixels) |
| */ |
| public void setPadding(int left, int top, int right, int bottom) { |
| if ((left | top | right | bottom) == 0) { |
| mVectorState.mPadding = null; |
| } else { |
| if (mVectorState.mPadding == null) { |
| mVectorState.mPadding = new Rect(); |
| } |
| mVectorState.mPadding.set(left, top, right, bottom); |
| } |
| invalidateSelf(); |
| } |
| |
| @Override |
| public int getIntrinsicWidth() { |
| return (int) mVectorState.mVPathRenderer.mBaseWidth; |
| } |
| |
| @Override |
| public int getIntrinsicHeight() { |
| return (int) mVectorState.mVPathRenderer.mBaseHeight; |
| } |
| |
| @Override |
| public boolean getPadding(Rect padding) { |
| if (mVectorState.mPadding != null) { |
| padding.set(mVectorState.mPadding); |
| return true; |
| } else { |
| return super.getPadding(padding); |
| } |
| } |
| |
| @Override |
| public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs, Theme theme) |
| throws XmlPullParserException, IOException { |
| final VPathRenderer p = inflateInternal(res, parser, attrs, theme); |
| setPathRenderer(p); |
| } |
| |
| @Override |
| public boolean canApplyTheme() { |
| return super.canApplyTheme() || mVectorState != null && mVectorState.canApplyTheme(); |
| } |
| |
| @Override |
| public void applyTheme(Theme t) { |
| super.applyTheme(t); |
| |
| final VectorDrawableState state = mVectorState; |
| final VPathRenderer path = state.mVPathRenderer; |
| if (path != null && path.canApplyTheme()) { |
| path.applyTheme(t); |
| } |
| } |
| |
| /** @hide */ |
| public static VectorDrawable create(Resources resources, int rid) { |
| try { |
| final XmlPullParser xpp = resources.getXml(rid); |
| final AttributeSet attrs = Xml.asAttributeSet(xpp); |
| final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); |
| factory.setNamespaceAware(true); |
| |
| final VectorDrawable drawable = new VectorDrawable(); |
| drawable.inflate(resources, xpp, attrs); |
| |
| return drawable; |
| } catch (XmlPullParserException e) { |
| Log.e(LOGTAG, "parser error", e); |
| } catch (IOException e) { |
| Log.e(LOGTAG, "parser error", e); |
| } |
| return null; |
| } |
| |
| private VPathRenderer inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs, |
| Theme theme) throws XmlPullParserException, IOException { |
| final VPathRenderer pathRenderer = new VPathRenderer(); |
| |
| boolean noSizeTag = true; |
| boolean noViewportTag = true; |
| boolean noGroupTag = true; |
| boolean noPathTag = true; |
| |
| VGroup currentGroup = new VGroup(); |
| |
| int eventType = parser.getEventType(); |
| while (eventType != XmlPullParser.END_DOCUMENT) { |
| if (eventType == XmlPullParser.START_TAG) { |
| final String tagName = parser.getName(); |
| if (SHAPE_PATH.equals(tagName)) { |
| final VPath path = new VPath(); |
| path.inflate(res, attrs, theme); |
| currentGroup.add(path); |
| noPathTag = false; |
| } else if (SHAPE_SIZE.equals(tagName)) { |
| pathRenderer.parseSize(res, attrs); |
| noSizeTag = false; |
| } else if (SHAPE_VIEWPORT.equals(tagName)) { |
| pathRenderer.parseViewport(res, attrs); |
| noViewportTag = false; |
| } else if (SHAPE_GROUP.equals(tagName)) { |
| currentGroup = new VGroup(); |
| pathRenderer.mGroupList.add(currentGroup); |
| noGroupTag = false; |
| } |
| } |
| |
| eventType = parser.next(); |
| } |
| |
| if (noGroupTag && !noPathTag) { |
| pathRenderer.mGroupList.add(currentGroup); |
| } |
| |
| if (noSizeTag || noViewportTag || noPathTag) { |
| final StringBuffer tag = new StringBuffer(); |
| |
| if (noSizeTag) { |
| tag.append(SHAPE_SIZE); |
| } |
| |
| if (noViewportTag) { |
| if (tag.length() > 0) { |
| tag.append(" & "); |
| } |
| tag.append(SHAPE_SIZE); |
| } |
| |
| if (noPathTag) { |
| if (tag.length() > 0) { |
| tag.append(" or "); |
| } |
| tag.append(SHAPE_PATH); |
| } |
| |
| throw new XmlPullParserException("no " + tag + " defined"); |
| } |
| |
| // post parse cleanup |
| pathRenderer.parseFinish(); |
| return pathRenderer; |
| } |
| |
| private void setPathRenderer(VPathRenderer pathRenderer) { |
| mVectorState.mVPathRenderer = pathRenderer; |
| } |
| |
| private static class VectorDrawableState extends ConstantState { |
| int mChangingConfigurations; |
| VPathRenderer mVPathRenderer; |
| Rect mPadding; |
| |
| public VectorDrawableState(VectorDrawableState copy) { |
| if (copy != null) { |
| mChangingConfigurations = copy.mChangingConfigurations; |
| mVPathRenderer = new VPathRenderer(copy.mVPathRenderer); |
| mPadding = new Rect(copy.mPadding); |
| } |
| } |
| |
| @Override |
| public Drawable newDrawable() { |
| return new VectorDrawable(this, null, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(Resources res) { |
| return new VectorDrawable(this, res, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(Resources res, Theme theme) { |
| return new VectorDrawable(this, res, theme); |
| } |
| |
| @Override |
| public int getChangingConfigurations() { |
| return mChangingConfigurations; |
| } |
| } |
| |
| private static class VPathRenderer { |
| private final Path mPath = new Path(); |
| private final Path mRenderPath = new Path(); |
| private final Matrix mMatrix = new Matrix(); |
| |
| private VPath[] mCurrentPaths; |
| private Paint mStrokePaint; |
| private Paint mFillPaint; |
| private ColorFilter mColorFilter; |
| private PathMeasure mPathMeasure; |
| |
| final ArrayList<VGroup> mGroupList = new ArrayList<VGroup>(); |
| |
| float mBaseWidth = 0; |
| float mBaseHeight = 0; |
| float mViewportWidth = 0; |
| float mViewportHeight = 0; |
| |
| public VPathRenderer() { |
| } |
| |
| public VPathRenderer(VPathRenderer copy) { |
| mGroupList.addAll(copy.mGroupList); |
| if (copy.mCurrentPaths != null) { |
| mCurrentPaths = new VPath[copy.mCurrentPaths.length]; |
| for (int i = 0; i < mCurrentPaths.length; i++) { |
| mCurrentPaths[i] = new VPath(copy.mCurrentPaths[i]); |
| } |
| } |
| |
| mBaseWidth = copy.mBaseWidth; |
| mBaseHeight = copy.mBaseHeight; |
| mViewportWidth = copy.mViewportHeight; |
| mViewportHeight = copy.mViewportHeight; |
| } |
| |
| public boolean canApplyTheme() { |
| final ArrayList<VGroup> groups = mGroupList; |
| for (int i = groups.size() - 1; i >= 0; i--) { |
| final ArrayList<VPath> paths = groups.get(i).mVGList; |
| for (int j = paths.size() - 1; j >= 0; j--) { |
| final VPath path = paths.get(j); |
| if (path.canApplyTheme()) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| public void applyTheme(Theme t) { |
| final ArrayList<VGroup> groups = mGroupList; |
| for (int i = groups.size() - 1; i >= 0; i--) { |
| final ArrayList<VPath> paths = groups.get(i).mVGList; |
| for (int j = paths.size() - 1; j >= 0; j--) { |
| final VPath path = paths.get(j); |
| if (path.canApplyTheme()) { |
| path.applyTheme(t); |
| } |
| } |
| } |
| } |
| |
| public void setColorFilter(ColorFilter colorFilter) { |
| mColorFilter = colorFilter; |
| |
| if (mFillPaint != null) { |
| mFillPaint.setColorFilter(colorFilter); |
| } |
| |
| if (mStrokePaint != null) { |
| mStrokePaint.setColorFilter(colorFilter); |
| } |
| |
| } |
| |
| public void draw(Canvas canvas, int w, int h) { |
| if (mCurrentPaths == null) { |
| Log.e(LOGTAG,"mCurrentPaths == null"); |
| return; |
| } |
| |
| for (int i = 0; i < mCurrentPaths.length; i++) { |
| if (mCurrentPaths[i] != null) { |
| drawPath(mCurrentPaths[i], canvas, w, h); |
| } |
| } |
| } |
| |
| private void drawPath(VPath vPath, Canvas canvas, int w, int h) { |
| final float scale = Math.min(h / mViewportHeight, w / mViewportWidth); |
| |
| vPath.toPath(mPath); |
| final Path path = mPath; |
| |
| if (vPath.mTrimPathStart != 0.0f || vPath.mTrimPathEnd != 1.0f) { |
| float start = (vPath.mTrimPathStart + vPath.mTrimPathOffset) % 1.0f; |
| float end = (vPath.mTrimPathEnd + vPath.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.reset(); |
| mMatrix.reset(); |
| |
| mMatrix.postRotate(vPath.mRotate, vPath.mPivotX, vPath.mPivotY); |
| mMatrix.postScale(scale, scale, mViewportWidth / 2f, mViewportHeight / 2f); |
| mMatrix.postTranslate(w / 2f - mViewportWidth / 2f, h / 2f - mViewportHeight / 2f); |
| |
| mRenderPath.addPath(path, mMatrix); |
| |
| if (vPath.mClip) { |
| canvas.clipPath(mRenderPath, Region.Op.REPLACE); |
| } |
| |
| if (vPath.mFillColor != 0) { |
| if (mFillPaint == null) { |
| mFillPaint = new Paint(); |
| mFillPaint.setColorFilter(mColorFilter); |
| mFillPaint.setStyle(Paint.Style.FILL); |
| mFillPaint.setAntiAlias(true); |
| } |
| |
| mFillPaint.setColor(vPath.mFillColor); |
| canvas.drawPath(mRenderPath, mFillPaint); |
| } |
| |
| if (vPath.mStrokeColor != 0) { |
| if (mStrokePaint == null) { |
| mStrokePaint = new Paint(); |
| mStrokePaint.setColorFilter(mColorFilter); |
| mStrokePaint.setStyle(Paint.Style.STROKE); |
| mStrokePaint.setAntiAlias(true); |
| } |
| |
| final Paint strokePaint = mStrokePaint; |
| if (vPath.mStrokeLineJoin != null) { |
| strokePaint.setStrokeJoin(vPath.mStrokeLineJoin); |
| } |
| |
| if (vPath.mStrokeLineCap != null) { |
| strokePaint.setStrokeCap(vPath.mStrokeLineCap); |
| } |
| |
| strokePaint.setStrokeMiter(vPath.mStrokeMiterlimit * scale); |
| strokePaint.setColor(vPath.mStrokeColor); |
| strokePaint.setStrokeWidth(vPath.mStrokeWidth * scale); |
| canvas.drawPath(mRenderPath, strokePaint); |
| } |
| } |
| |
| /** |
| * Build the "current" path based on the current group |
| * TODO: improve memory use & performance or move to C++ |
| */ |
| public void parseFinish() { |
| final Collection<VPath> paths = mGroupList.get(0).getPaths(); |
| mCurrentPaths = paths.toArray(new VPath[paths.size()]); |
| for (int i = 0; i < mCurrentPaths.length; i++) { |
| mCurrentPaths[i] = new VPath(mCurrentPaths[i]); |
| } |
| } |
| |
| private void parseViewport(Resources r, AttributeSet attrs) |
| throws XmlPullParserException { |
| final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableViewport); |
| mViewportWidth = a.getFloat(R.styleable.VectorDrawableViewport_viewportWidth, mViewportWidth); |
| mViewportHeight = a.getFloat(R.styleable.VectorDrawableViewport_viewportHeight, mViewportHeight); |
| |
| if (mViewportWidth <= 0) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| "<viewport> tag requires viewportWidth > 0"); |
| } else if (mViewportHeight <= 0) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| "<viewport> tag requires viewportHeight > 0"); |
| } |
| |
| a.recycle(); |
| } |
| |
| private void parseSize(Resources r, AttributeSet attrs) |
| throws XmlPullParserException { |
| final TypedArray a = r.obtainAttributes(attrs, R.styleable.VectorDrawableSize); |
| mBaseWidth = a.getDimension(R.styleable.VectorDrawableSize_width, mBaseWidth); |
| mBaseHeight = a.getDimension(R.styleable.VectorDrawableSize_height, mBaseHeight); |
| |
| if (mBaseWidth <= 0) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| "<size> tag requires width > 0"); |
| } else if (mBaseHeight <= 0) { |
| throw new XmlPullParserException(a.getPositionDescription() + |
| "<size> tag requires height > 0"); |
| } |
| |
| a.recycle(); |
| } |
| |
| } |
| |
| private static class VGroup { |
| private final HashMap<String, VPath> mVGPathMap = new HashMap<String, VPath>(); |
| private final ArrayList<VPath> mVGList = new ArrayList<VPath>(); |
| |
| public void add(VPath path) { |
| String id = path.getID(); |
| mVGPathMap.put(id, path); |
| mVGList.add(path); |
| } |
| |
| /** |
| * Must return in order of adding |
| * @return ordered list of paths |
| */ |
| public Collection<VPath> getPaths() { |
| return mVGList; |
| } |
| |
| } |
| |
| private static class VPath { |
| private static final int MAX_STATES = 10; |
| |
| private int[] mThemeAttrs; |
| |
| int mStrokeColor = 0; |
| float mStrokeWidth = 0; |
| float mStrokeOpacity = Float.NaN; |
| |
| int mFillColor = 0; |
| int mFillRule; |
| float mFillOpacity = Float.NaN; |
| |
| float mRotate = 0; |
| float mPivotX = 0; |
| float mPivotY = 0; |
| |
| float mTrimPathStart = 0; |
| float mTrimPathEnd = 1; |
| float mTrimPathOffset = 0; |
| |
| boolean mClip = false; |
| Paint.Cap mStrokeLineCap = Paint.Cap.BUTT; |
| Paint.Join mStrokeLineJoin = Paint.Join.MITER; |
| float mStrokeMiterlimit = 4; |
| |
| private VNode[] mNode = null; |
| private String mId; |
| private int[] mCheckState = new int[MAX_STATES]; |
| private boolean[] mCheckValue = new boolean[MAX_STATES]; |
| private int mNumberOfStates = 0; |
| |
| public VPath() { |
| // Empty constructor. |
| } |
| |
| public VPath(VPath p) { |
| copyFrom(p); |
| } |
| |
| public void toPath(Path path) { |
| path.reset(); |
| if (mNode != null) { |
| VNode.createPath(mNode, path); |
| } |
| } |
| |
| public String getID() { |
| return mId; |
| } |
| |
| 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; |
| } |
| } |
| |
| public void inflate(Resources r, AttributeSet attrs, Theme theme) { |
| final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.VectorDrawablePath); |
| final int[] themeAttrs = a.extractThemeAttrs(); |
| mThemeAttrs = themeAttrs; |
| |
| // NOTE: The set of attributes loaded here MUST match the |
| // set of attributes loaded in applyTheme. |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_clipToPath] == 0) { |
| mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_name] == 0) { |
| mId = a.getString(R.styleable.VectorDrawablePath_name); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pathData] == 0) { |
| mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData)); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fill] == 0) { |
| mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_fillOpacity] == 0) { |
| mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_rotation] == 0) { |
| mRotate = a.getFloat(R.styleable.VectorDrawablePath_rotation, mRotate); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pivotX] == 0) { |
| mPivotX = a.getFloat(R.styleable.VectorDrawablePath_pivotX, mPivotX); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_pivotY] == 0) { |
| mPivotY = a.getFloat(R.styleable.VectorDrawablePath_pivotY, mPivotY); |
| } |
| |
| if (themeAttrs == null |
| || themeAttrs[R.styleable.VectorDrawablePath_strokeLineCap] == 0) { |
| mStrokeLineCap = getStrokeLineCap( |
| a.getInt(R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap); |
| } |
| |
| if (themeAttrs == null |
| || themeAttrs[R.styleable.VectorDrawablePath_strokeLineJoin] == 0) { |
| mStrokeLineJoin = getStrokeLineJoin( |
| a.getInt(R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin); |
| } |
| |
| if (themeAttrs == null |
| || themeAttrs[R.styleable.VectorDrawablePath_strokeMiterLimit] == 0) { |
| mStrokeMiterlimit = a.getFloat( |
| R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_stroke] == 0) { |
| mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_stroke, mStrokeColor); |
| } |
| |
| if (themeAttrs == null |
| || themeAttrs[R.styleable.VectorDrawablePath_strokeOpacity] == 0) { |
| mStrokeOpacity = a.getFloat( |
| R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_strokeWidth] == 0) { |
| mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, mStrokeWidth); |
| } |
| |
| if (themeAttrs == null || themeAttrs[R.styleable.VectorDrawablePath_trimPathEnd] == 0) { |
| mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, mTrimPathEnd); |
| } |
| |
| if (themeAttrs == null |
| || themeAttrs[R.styleable.VectorDrawablePath_trimPathOffset] == 0) { |
| mTrimPathOffset = a.getFloat( |
| R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset); |
| } |
| |
| if (themeAttrs == null |
| || themeAttrs[R.styleable.VectorDrawablePath_trimPathStart] == 0) { |
| mTrimPathStart = a.getFloat( |
| R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart); |
| } |
| |
| updateColorAlphas(); |
| |
| a.recycle(); |
| } |
| |
| public boolean canApplyTheme() { |
| return mThemeAttrs != null; |
| } |
| |
| public void applyTheme(Theme t) { |
| if (mThemeAttrs == null) { |
| return; |
| } |
| |
| final TypedArray a = t.resolveAttributes( |
| mThemeAttrs, R.styleable.VectorDrawablePath); |
| |
| mClip = a.getBoolean(R.styleable.VectorDrawablePath_clipToPath, mClip); |
| |
| if (a.hasValue(R.styleable.VectorDrawablePath_name)) { |
| mId = a.getString(R.styleable.VectorDrawablePath_name); |
| } |
| |
| if (a.hasValue(R.styleable.VectorDrawablePath_pathData)) { |
| mNode = parsePath(a.getString(R.styleable.VectorDrawablePath_pathData)); |
| } |
| |
| mFillColor = a.getColor(R.styleable.VectorDrawablePath_fill, mFillColor); |
| mFillOpacity = a.getFloat(R.styleable.VectorDrawablePath_fillOpacity, mFillOpacity); |
| |
| mRotate = a.getFloat(R.styleable.VectorDrawablePath_rotation, mRotate); |
| mPivotX = a.getFloat(R.styleable.VectorDrawablePath_pivotX, mPivotX); |
| mPivotY = a.getFloat(R.styleable.VectorDrawablePath_pivotY, mPivotY); |
| |
| 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_stroke, mStrokeColor); |
| mStrokeOpacity = a.getFloat( |
| R.styleable.VectorDrawablePath_strokeOpacity, mStrokeOpacity); |
| 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); |
| |
| updateColorAlphas(); |
| } |
| |
| private void updateColorAlphas() { |
| if (!Float.isNaN(mFillOpacity)) { |
| mFillColor &= 0x00FFFFFF; |
| mFillColor |= ((int) (0xFF * mFillOpacity)) << 24; |
| } |
| |
| if (!Float.isNaN(mStrokeOpacity)) { |
| mStrokeColor &= 0x00FFFFFF; |
| mStrokeColor |= ((int) (0xFF * mStrokeOpacity)) << 24; |
| } |
| } |
| |
| private static int nextStart(String s, int end) { |
| char c; |
| |
| while (end < s.length()) { |
| c = s.charAt(end); |
| if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { |
| return end; |
| } |
| end++; |
| } |
| return end; |
| } |
| |
| private void addNode(ArrayList<VectorDrawable.VNode> list, char cmd, float[] val) { |
| list.add(new VectorDrawable.VNode(cmd, val)); |
| } |
| |
| /** |
| * parse the floats in the string |
| * this is an optimized version of |
| * parseFloat(s.split(",|\\s")); |
| * |
| * @param s the string containing a command and list of floats |
| * @return array of floats |
| */ |
| private static float[] getFloats(String s) { |
| if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { |
| return new float[0]; |
| } |
| try { |
| float[] tmp = new float[s.length()]; |
| int count = 0; |
| int pos = 1, end; |
| while ((end = extract(s, pos)) >= 0) { |
| if (pos < end) { |
| tmp[count++] = Float.parseFloat(s.substring(pos, end)); |
| } |
| pos = end + 1; |
| } |
| // handle the final float if there is one |
| if (pos < s.length()) { |
| tmp[count++] = Float.parseFloat(s.substring(pos, s.length())); |
| } |
| return Arrays.copyOf(tmp, count); |
| } catch (NumberFormatException e){ |
| Log.e(LOGTAG,"error in parsing \""+s+"\""); |
| throw e; |
| } |
| } |
| |
| /** |
| * calculate the position of the next comma or space |
| * @param s the string to search |
| * @param start the position to start searching |
| * @return the position of the next comma or space or -1 if none found |
| */ |
| private static int extract(String s, int start) { |
| int space = s.indexOf(' ', start); |
| int comma = s.indexOf(',', start); |
| if (space == -1) { |
| return comma; |
| } |
| if (comma == -1) { |
| return space; |
| } |
| return (comma > space) ? space : comma; |
| } |
| |
| private VectorDrawable.VNode[] parsePath(String value) { |
| int start = 0; |
| int end = 1; |
| |
| ArrayList<VectorDrawable.VNode> list = new ArrayList<VectorDrawable.VNode>(); |
| while (end < value.length()) { |
| end = nextStart(value, end); |
| String s = value.substring(start, end); |
| float[] val = getFloats(s); |
| addNode(list, s.charAt(0), val); |
| |
| start = end; |
| end++; |
| } |
| if ((end - start) == 1 && start < value.length()) { |
| |
| addNode(list, value.charAt(start), new float[0]); |
| } |
| return list.toArray(new VectorDrawable.VNode[list.size()]); |
| } |
| |
| public void copyFrom(VPath p1) { |
| mNode = new VNode[p1.mNode.length]; |
| for (int i = 0; i < mNode.length; i++) { |
| mNode[i] = new VNode(p1.mNode[i]); |
| } |
| mId = p1.mId; |
| mStrokeColor = p1.mStrokeColor; |
| mFillColor = p1.mFillColor; |
| mStrokeWidth = p1.mStrokeWidth; |
| mRotate = p1.mRotate; |
| mPivotX = p1.mPivotX; |
| mPivotY = p1.mPivotY; |
| mTrimPathStart = p1.mTrimPathStart; |
| mTrimPathEnd = p1.mTrimPathEnd; |
| mTrimPathOffset = p1.mTrimPathOffset; |
| mStrokeLineCap = p1.mStrokeLineCap; |
| mStrokeLineJoin = p1.mStrokeLineJoin; |
| mStrokeMiterlimit = p1.mStrokeMiterlimit; |
| mNumberOfStates = p1.mNumberOfStates; |
| for (int i = 0; i < mNumberOfStates; i++) { |
| mCheckState[i] = p1.mCheckState[i]; |
| mCheckValue[i] = p1.mCheckValue[i]; |
| } |
| |
| mFillRule = p1.mFillRule; |
| } |
| } |
| |
| private static class VNode { |
| private char mType; |
| private float[] mParams; |
| |
| public VNode(char type, float[] params) { |
| mType = type; |
| mParams = params; |
| } |
| |
| public VNode(VNode n) { |
| mType = n.mType; |
| mParams = Arrays.copyOf(n.mParams, n.mParams.length); |
| } |
| |
| public static void createPath(VNode[] node, Path path) { |
| float[] current = new float[4]; |
| char previousCommand = 'm'; |
| for (int i = 0; i < node.length; i++) { |
| addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); |
| previousCommand = node[i].mType; |
| } |
| } |
| |
| private static void addCommand(Path path, float[] current, |
| char previousCmd, char cmd, float[] val) { |
| |
| int incr = 2; |
| float currentX = current[0]; |
| float currentY = current[1]; |
| float ctrlPointX = current[2]; |
| float ctrlPointY = current[3]; |
| float reflectiveCtrlPointX; |
| float reflectiveCtrlPointY; |
| |
| switch (cmd) { |
| case 'z': |
| case 'Z': |
| path.close(); |
| return; |
| case 'm': |
| case 'M': |
| case 'l': |
| case 'L': |
| case 't': |
| case 'T': |
| incr = 2; |
| break; |
| case 'h': |
| case 'H': |
| case 'v': |
| case 'V': |
| incr = 1; |
| break; |
| case 'c': |
| case 'C': |
| incr = 6; |
| break; |
| case 's': |
| case 'S': |
| case 'q': |
| case 'Q': |
| incr = 4; |
| break; |
| case 'a': |
| case 'A': |
| incr = 7; |
| break; |
| } |
| for (int k = 0; k < val.length; k += incr) { |
| switch (cmd) { |
| case 'm': // moveto - Start a new sub-path (relative) |
| path.rMoveTo(val[k + 0], val[k + 1]); |
| currentX += val[k + 0]; |
| currentY += val[k + 1]; |
| break; |
| case 'M': // moveto - Start a new sub-path |
| path.moveTo(val[k + 0], val[k + 1]); |
| currentX = val[k + 0]; |
| currentY = val[k + 1]; |
| break; |
| case 'l': // lineto - Draw a line from the current point (relative) |
| path.rLineTo(val[k + 0], val[k + 1]); |
| currentX += val[k + 0]; |
| currentY += val[k + 1]; |
| break; |
| case 'L': // lineto - Draw a line from the current point |
| path.lineTo(val[k + 0], val[k + 1]); |
| currentX = val[k + 0]; |
| currentY = val[k + 1]; |
| break; |
| case 'z': // closepath - Close the current subpath |
| case 'Z': // closepath - Close the current subpath |
| path.close(); |
| break; |
| case 'h': // horizontal lineto - Draws a horizontal line (relative) |
| path.rLineTo(val[k + 0], 0); |
| currentX += val[k + 0]; |
| break; |
| case 'H': // horizontal lineto - Draws a horizontal line |
| path.lineTo(val[k + 0], currentY); |
| currentX = val[k + 0]; |
| break; |
| case 'v': // vertical lineto - Draws a vertical line from the current point (r) |
| path.rLineTo(0, val[k + 0]); |
| currentY += val[k + 0]; |
| break; |
| case 'V': // vertical lineto - Draws a vertical line from the current point |
| path.lineTo(currentX, val[k + 0]); |
| currentY = val[k + 0]; |
| break; |
| case 'c': // curveto - Draws a cubic Bézier curve (relative) |
| path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], |
| val[k + 4], val[k + 5]); |
| |
| ctrlPointX = currentX + val[k + 2]; |
| ctrlPointY = currentY + val[k + 3]; |
| currentX += val[k + 4]; |
| currentY += val[k + 5]; |
| |
| break; |
| case 'C': // curveto - Draws a cubic Bézier curve |
| path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], |
| val[k + 4], val[k + 5]); |
| currentX = val[k + 4]; |
| currentY = val[k + 5]; |
| ctrlPointX = val[k + 2]; |
| ctrlPointY = val[k + 3]; |
| break; |
| case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) |
| reflectiveCtrlPointX = 0; |
| reflectiveCtrlPointY = 0; |
| if (previousCmd == 'c' || previousCmd == 's' |
| || previousCmd == 'C' || previousCmd == 'S') { |
| reflectiveCtrlPointX = currentX - ctrlPointX; |
| reflectiveCtrlPointY = currentY - ctrlPointY; |
| } |
| path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, |
| val[k + 0], val[k + 1], |
| val[k + 2], val[k + 3]); |
| |
| ctrlPointX = currentX + val[k + 0]; |
| ctrlPointY = currentY + val[k + 1]; |
| currentX += val[k + 2]; |
| currentY += val[k + 3]; |
| break; |
| case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) |
| reflectiveCtrlPointX = currentX; |
| reflectiveCtrlPointY = currentY; |
| if (previousCmd == 'c' || previousCmd == 's' |
| || previousCmd == 'C' || previousCmd == 'S') { |
| reflectiveCtrlPointX = 2 * currentX - ctrlPointX; |
| reflectiveCtrlPointY = 2 * currentY - ctrlPointY; |
| } |
| path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, |
| val[k + 0], val[k + 1], val[k + 2], val[k + 3]); |
| ctrlPointX = val[k + 0]; |
| ctrlPointY = val[k + 1]; |
| currentX = val[k + 2]; |
| currentY = val[k + 3]; |
| break; |
| case 'q': // Draws a quadratic Bézier (relative) |
| path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); |
| ctrlPointX = currentX + val[k + 0]; |
| ctrlPointY = currentY + val[k + 1]; |
| currentX += val[k + 2]; |
| currentY += val[k + 3]; |
| break; |
| case 'Q': // Draws a quadratic Bézier |
| path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); |
| ctrlPointX = val[k + 0]; |
| ctrlPointY = val[k + 1]; |
| currentX = val[k + 2]; |
| currentY = val[k + 3]; |
| break; |
| case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) |
| reflectiveCtrlPointX = 0; |
| reflectiveCtrlPointY = 0; |
| if (previousCmd == 'q' || previousCmd == 't' |
| || previousCmd == 'Q' || previousCmd == 'T') { |
| reflectiveCtrlPointX = currentX - ctrlPointX; |
| reflectiveCtrlPointY = currentY - ctrlPointY; |
| } |
| path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, |
| val[k + 0], val[k + 1]); |
| ctrlPointX = currentX + reflectiveCtrlPointX; |
| ctrlPointY = currentY + reflectiveCtrlPointY; |
| currentX += val[k + 0]; |
| currentY += val[k + 1]; |
| break; |
| case 'T': // Draws a quadratic Bézier curve (reflective control point) |
| reflectiveCtrlPointX = currentX; |
| reflectiveCtrlPointY = currentY; |
| if (previousCmd == 'q' || previousCmd == 't' |
| || previousCmd == 'Q' || previousCmd == 'T') { |
| reflectiveCtrlPointX = 2 * currentX - ctrlPointX; |
| reflectiveCtrlPointY = 2 * currentY - ctrlPointY; |
| } |
| path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, |
| val[k + 0], val[k + 1]); |
| ctrlPointX = reflectiveCtrlPointX; |
| ctrlPointY = reflectiveCtrlPointY; |
| currentX = val[k + 0]; |
| currentY = val[k + 1]; |
| break; |
| case 'a': // Draws an elliptical arc |
| // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) |
| drawArc(path, |
| currentX, |
| currentY, |
| val[k + 5] + currentX, |
| val[k + 6] + currentY, |
| val[k + 0], |
| val[k + 1], |
| val[k + 2], |
| val[k + 3] != 0, |
| val[k + 4] != 0); |
| currentX += val[k + 5]; |
| currentY += val[k + 6]; |
| ctrlPointX = currentX; |
| ctrlPointY = currentY; |
| break; |
| case 'A': // Draws an elliptical arc |
| drawArc(path, |
| currentX, |
| currentY, |
| val[k + 5], |
| val[k + 6], |
| val[k + 0], |
| val[k + 1], |
| val[k + 2], |
| val[k + 3] != 0, |
| val[k + 4] != 0); |
| currentX = val[k + 5]; |
| currentY = val[k + 6]; |
| ctrlPointX = currentX; |
| ctrlPointY = currentY; |
| break; |
| } |
| previousCmd = cmd; |
| } |
| current[0] = currentX; |
| current[1] = currentY; |
| current[2] = ctrlPointX; |
| current[3] = ctrlPointY; |
| } |
| |
| private static void drawArc(Path p, |
| float x0, |
| float y0, |
| float x1, |
| float y1, |
| float a, |
| float b, |
| float theta, |
| boolean isMoreThanHalf, |
| boolean isPositiveArc) { |
| |
| /* Convert rotation angle from degrees to radians */ |
| double thetaD = Math.toRadians(theta); |
| /* Pre-compute rotation matrix entries */ |
| double cosTheta = Math.cos(thetaD); |
| double sinTheta = Math.sin(thetaD); |
| /* Transform (x0, y0) and (x1, y1) into unit space */ |
| /* using (inverse) rotation, followed by (inverse) scale */ |
| double x0p = (x0 * cosTheta + y0 * sinTheta) / a; |
| double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; |
| double x1p = (x1 * cosTheta + y1 * sinTheta) / a; |
| double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; |
| |
| /* Compute differences and averages */ |
| double dx = x0p - x1p; |
| double dy = y0p - y1p; |
| double xm = (x0p + x1p) / 2; |
| double ym = (y0p + y1p) / 2; |
| /* Solve for intersecting unit circles */ |
| double dsq = dx * dx + dy * dy; |
| if (dsq == 0.0) { |
| Log.w(LOGTAG, " Points are coincident"); |
| return; /* Points are coincident */ |
| } |
| double disc = 1.0 / dsq - 1.0 / 4.0; |
| if (disc < 0.0) { |
| Log.w(LOGTAG, "Points are too far apart " + dsq); |
| float adjust = (float) (Math.sqrt(dsq) / 1.99999); |
| drawArc(p, x0, y0, x1, y1, a * adjust, |
| b * adjust, theta, isMoreThanHalf, isPositiveArc); |
| return; /* Points are too far apart */ |
| } |
| double s = Math.sqrt(disc); |
| double sdx = s * dx; |
| double sdy = s * dy; |
| double cx; |
| double cy; |
| if (isMoreThanHalf == isPositiveArc) { |
| cx = xm - sdy; |
| cy = ym + sdx; |
| } else { |
| cx = xm + sdy; |
| cy = ym - sdx; |
| } |
| |
| double eta0 = Math.atan2((y0p - cy), (x0p - cx)); |
| |
| double eta1 = Math.atan2((y1p - cy), (x1p - cx)); |
| |
| double sweep = (eta1 - eta0); |
| if (isPositiveArc != (sweep >= 0)) { |
| if (sweep > 0) { |
| sweep -= 2 * Math.PI; |
| } else { |
| sweep += 2 * Math.PI; |
| } |
| } |
| |
| cx *= a; |
| cy *= b; |
| double tcx = cx; |
| cx = cx * cosTheta - cy * sinTheta; |
| cy = tcx * sinTheta + cy * cosTheta; |
| |
| arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); |
| } |
| |
| /** |
| * Converts an arc to cubic Bezier segments and records them in p. |
| * |
| * @param p The target for the cubic Bezier segments |
| * @param cx The x coordinate center of the ellipse |
| * @param cy The y coordinate center of the ellipse |
| * @param a The radius of the ellipse in the horizontal direction |
| * @param b The radius of the ellipse in the vertical direction |
| * @param e1x E(eta1) x coordinate of the starting point of the arc |
| * @param e1y E(eta2) y coordinate of the starting point of the arc |
| * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane |
| * @param start The start angle of the arc on the ellipse |
| * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse |
| */ |
| private static void arcToBezier(Path p, |
| double cx, |
| double cy, |
| double a, |
| double b, |
| double e1x, |
| double e1y, |
| double theta, |
| double start, |
| double sweep) { |
| // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html |
| // and http://www.spaceroots.org/documents/ellipse/node22.html |
| |
| // Maximum of 45 degrees per cubic Bezier segment |
| int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); |
| |
| double eta1 = start; |
| double cosTheta = Math.cos(theta); |
| double sinTheta = Math.sin(theta); |
| double cosEta1 = Math.cos(eta1); |
| double sinEta1 = Math.sin(eta1); |
| double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); |
| double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); |
| |
| double anglePerSegment = sweep / numSegments; |
| for (int i = 0; i < numSegments; i++) { |
| double eta2 = eta1 + anglePerSegment; |
| double sinEta2 = Math.sin(eta2); |
| double cosEta2 = Math.cos(eta2); |
| double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); |
| double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); |
| double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; |
| double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; |
| double tanDiff2 = Math.tan((eta2 - eta1) / 2); |
| double alpha = |
| Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; |
| double q1x = e1x + alpha * ep1x; |
| double q1y = e1y + alpha * ep1y; |
| double q2x = e2x - alpha * ep2x; |
| double q2y = e2y - alpha * ep2y; |
| |
| p.cubicTo((float) q1x, |
| (float) q1y, |
| (float) q2x, |
| (float) q2y, |
| (float) e2x, |
| (float) e2y); |
| eta1 = eta2; |
| e1x = e2x; |
| e1y = e2y; |
| ep1x = ep2x; |
| ep1y = ep2y; |
| } |
| } |
| |
| } |
| } |