/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.react.views.art; import javax.annotation.Nullable; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; import android.graphics.DashPathEffect; import com.facebook.common.logging.FLog; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.common.ReactConstants; import com.facebook.react.uimanager.annotations.ReactProp; /** * Shadow node for virtual ARTShape view */ public class ARTShapeShadowNode extends ARTVirtualNode { private static final int CAP_BUTT = 0; private static final int CAP_ROUND = 1; private static final int CAP_SQUARE = 2; private static final int JOIN_BEVEL = 2; private static final int JOIN_MITER = 0; private static final int JOIN_ROUND = 1; private static final int PATH_TYPE_ARC = 4; private static final int PATH_TYPE_CLOSE = 1; private static final int PATH_TYPE_CURVETO = 3; private static final int PATH_TYPE_LINETO = 2; private static final int PATH_TYPE_MOVETO = 0; protected @Nullable Path mPath; private @Nullable float[] mStrokeColor; private @Nullable float[] mFillColor; private @Nullable float[] mStrokeDash; private float mStrokeWidth = 1; private int mStrokeCap = CAP_ROUND; private int mStrokeJoin = JOIN_ROUND; @ReactProp(name = "d") public void setShapePath(@Nullable ReadableArray shapePath) { float[] pathData = PropHelper.toFloatArray(shapePath); mPath = createPath(pathData); markUpdated(); } @ReactProp(name = "stroke") public void setStroke(@Nullable ReadableArray strokeColors) { mStrokeColor = PropHelper.toFloatArray(strokeColors); markUpdated(); } @ReactProp(name = "strokeDash") public void setStrokeDash(@Nullable ReadableArray strokeDash) { mStrokeDash = PropHelper.toFloatArray(strokeDash); markUpdated(); } @ReactProp(name = "fill") public void setFill(@Nullable ReadableArray fillColors) { mFillColor = PropHelper.toFloatArray(fillColors); markUpdated(); } @ReactProp(name = "strokeWidth", defaultFloat = 1f) public void setStrokeWidth(float strokeWidth) { mStrokeWidth = strokeWidth; markUpdated(); } @ReactProp(name = "strokeCap", defaultInt = CAP_ROUND) public void setStrokeCap(int strokeCap) { mStrokeCap = strokeCap; markUpdated(); } @ReactProp(name = "strokeJoin", defaultInt = JOIN_ROUND) public void setStrokeJoin(int strokeJoin) { mStrokeJoin = strokeJoin; markUpdated(); } @Override public void draw(Canvas canvas, Paint paint, float opacity) { opacity *= mOpacity; if (opacity > MIN_OPACITY_FOR_DRAW) { saveAndSetupCanvas(canvas); if (mPath == null) { throw new JSApplicationIllegalArgumentException( "Shapes should have a valid path (d) prop"); } if (setupFillPaint(paint, opacity)) { canvas.drawPath(mPath, paint); } if (setupStrokePaint(paint, opacity)) { canvas.drawPath(mPath, paint); } restoreCanvas(canvas); } markUpdateSeen(); } /** * Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true} * if the stroke should be drawn, {@code false} if not. */ protected boolean setupStrokePaint(Paint paint, float opacity) { if (mStrokeWidth == 0 || mStrokeColor == null || mStrokeColor.length == 0) { return false; } paint.reset(); paint.setFlags(Paint.ANTI_ALIAS_FLAG); paint.setStyle(Paint.Style.STROKE); switch (mStrokeCap) { case CAP_BUTT: paint.setStrokeCap(Paint.Cap.BUTT); break; case CAP_SQUARE: paint.setStrokeCap(Paint.Cap.SQUARE); break; case CAP_ROUND: paint.setStrokeCap(Paint.Cap.ROUND); break; default: throw new JSApplicationIllegalArgumentException( "strokeCap " + mStrokeCap + " unrecognized"); } switch (mStrokeJoin) { case JOIN_MITER: paint.setStrokeJoin(Paint.Join.MITER); break; case JOIN_BEVEL: paint.setStrokeJoin(Paint.Join.BEVEL); break; case JOIN_ROUND: paint.setStrokeJoin(Paint.Join.ROUND); break; default: throw new JSApplicationIllegalArgumentException( "strokeJoin " + mStrokeJoin + " unrecognized"); } paint.setStrokeWidth(mStrokeWidth * mScale); paint.setARGB( (int) (mStrokeColor.length > 3 ? mStrokeColor[3] * opacity * 255 : opacity * 255), (int) (mStrokeColor[0] * 255), (int) (mStrokeColor[1] * 255), (int) (mStrokeColor[2] * 255)); if (mStrokeDash != null && mStrokeDash.length > 0) { paint.setPathEffect(new DashPathEffect(mStrokeDash, 0)); } return true; } /** * Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true} * if the fill should be drawn, {@code false} if not. */ protected boolean setupFillPaint(Paint paint, float opacity) { if (mFillColor != null && mFillColor.length > 0) { paint.reset(); paint.setFlags(Paint.ANTI_ALIAS_FLAG); paint.setStyle(Paint.Style.FILL); int colorType = (int) mFillColor[0]; switch (colorType) { case 0: paint.setARGB( (int) (mFillColor.length > 4 ? mFillColor[4] * opacity * 255 : opacity * 255), (int) (mFillColor[1] * 255), (int) (mFillColor[2] * 255), (int) (mFillColor[3] * 255)); break; default: // TODO(6352048): Support gradients etc. FLog.w(ReactConstants.TAG, "ART: Color type " + colorType + " not supported!"); } return true; } return false; } /** * Creates a {@link Path} from an array of instructions constructed by JS * (see ARTSerializablePath.js). Each instruction starts with a type (see PATH_TYPE_*) followed * by arguments for that instruction. For example, to create a line the instruction will be * 2 (PATH_LINE_TO), x, y. This will draw a line from the last draw point (or 0,0) to x,y. * * @param data the array of instructions * @return the {@link Path} that can be drawn to a canvas */ private Path createPath(float[] data) { Path path = new Path(); path.moveTo(0, 0); int i = 0; while (i < data.length) { int type = (int) data[i++]; switch (type) { case PATH_TYPE_MOVETO: path.moveTo(data[i++] * mScale, data[i++] * mScale); break; case PATH_TYPE_CLOSE: path.close(); break; case PATH_TYPE_LINETO: path.lineTo(data[i++] * mScale, data[i++] * mScale); break; case PATH_TYPE_CURVETO: path.cubicTo( data[i++] * mScale, data[i++] * mScale, data[i++] * mScale, data[i++] * mScale, data[i++] * mScale, data[i++] * mScale); break; case PATH_TYPE_ARC: { float x = data[i++] * mScale; float y = data[i++] * mScale; float r = data[i++] * mScale; float start = (float) Math.toDegrees(data[i++]); float end = (float) Math.toDegrees(data[i++]); boolean clockwise = data[i++] == 0f; if (!clockwise) { end = 360 - end; } float sweep = start - end; RectF oval = new RectF(x - r, y - r, x + r, y + r); path.addArc(oval, start, sweep); break; } default: throw new JSApplicationIllegalArgumentException( "Unrecognized drawing instruction " + type); } } return path; } }