diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 6cd3e9eff..e636cd1c0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -27,6 +27,8 @@ import com.facebook.react.modules.storage.AsyncStorageModule; import com.facebook.react.modules.toast.ToastModule; import com.facebook.react.modules.websocket.WebSocketModule; import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.views.art.ARTRenderableViewManager; +import com.facebook.react.views.art.ARTSurfaceViewManager; import com.facebook.react.views.drawer.ReactDrawerLayoutManager; import com.facebook.react.views.image.ReactImageManager; import com.facebook.react.views.progressbar.ReactProgressBarViewManager; @@ -74,6 +76,10 @@ public class MainReactPackage implements ReactPackage { @Override public List createViewManagers(ReactApplicationContext reactContext) { return Arrays.asList( + ARTRenderableViewManager.createARTGroupViewManager(), + ARTRenderableViewManager.createARTShapeViewManager(), + ARTRenderableViewManager.createARTTextViewManager(), + new ARTSurfaceViewManager(), new ReactDrawerLayoutManager(), new ReactHorizontalScrollViewManager(), new ReactImageManager(), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTGroupShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTGroupShadowNode.java new file mode 100644 index 000000000..9586a7b0f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTGroupShadowNode.java @@ -0,0 +1,37 @@ +/** + * 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 android.graphics.Canvas; +import android.graphics.Paint; + +/** + * Shadow node for virtual ARTGroup view + */ +public class ARTGroupShadowNode extends ARTVirtualNode { + + @Override + public boolean isVirtual() { + return true; + } + + public void draw(Canvas canvas, Paint paint, float opacity) { + opacity *= mOpacity; + if (opacity > MIN_OPACITY_FOR_DRAW) { + saveAndSetupCanvas(canvas); + // TODO(6352006): apply clipping (iOS doesn't do it yet, it seems to cause issues) + for (int i = 0; i < getChildCount(); i++) { + ARTVirtualNode child = (ARTVirtualNode) getChildAt(i); + child.draw(canvas, paint, opacity); + child.markUpdateSeen(); + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTRenderableViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTRenderableViewManager.java new file mode 100644 index 000000000..bd6a7df90 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTRenderableViewManager.java @@ -0,0 +1,88 @@ +/** + * 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 android.view.View; + +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.ReactShadowNode; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewManager; + +/** + * ViewManager for all shadowed ART views: Group, Shape and Text. Since these never get rendered + * into native views and don't need any logic (all the logic is in {@link ARTSurfaceView}), this + * "stubbed" ViewManager is used for all of them. + */ +public class ARTRenderableViewManager extends ViewManager { + + /* package */ static final String CLASS_GROUP = "ARTGroup"; + /* package */ static final String CLASS_SHAPE = "ARTShape"; + /* package */ static final String CLASS_TEXT = "ARTText"; + + private final String mClassName; + + public static ARTRenderableViewManager createARTGroupViewManager() { + return new ARTRenderableViewManager(CLASS_GROUP); + } + + public static ARTRenderableViewManager createARTShapeViewManager() { + return new ARTRenderableViewManager(CLASS_SHAPE); + } + + public static ARTRenderableViewManager createARTTextViewManager() { + return new ARTRenderableViewManager(CLASS_TEXT); + } + + private ARTRenderableViewManager(String className) { + mClassName = className; + } + + @Override + public String getName() { + return mClassName; + } + + @Override + public ReactShadowNode createShadowNodeInstance() { + if (mClassName == CLASS_GROUP) { + return new ARTGroupShadowNode(); + } else if (mClassName == CLASS_SHAPE) { + return new ARTShapeShadowNode(); + } else if (mClassName == CLASS_TEXT) { + return new ARTTextShadowNode(); + } else { + throw new IllegalStateException("Unexpected type " + mClassName); + } + } + + @Override + public Class getShadowNodeClass() { + if (mClassName == CLASS_GROUP) { + return ARTGroupShadowNode.class; + } else if (mClassName == CLASS_SHAPE) { + return ARTShapeShadowNode.class; + } else if (mClassName == CLASS_TEXT) { + return ARTTextShadowNode.class; + } else { + throw new IllegalStateException("Unexpected type " + mClassName); + } + } + + @Override + protected View createViewInstance(ThemedReactContext reactContext) { + throw new IllegalStateException("ARTShape does not map into a native view"); + } + + @Override + public void updateExtraData(View root, Object extraData) { + throw new IllegalStateException("ARTShape does not map into a native view"); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTShapeShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTShapeShadowNode.java new file mode 100644 index 000000000..76a64f726 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTShapeShadowNode.java @@ -0,0 +1,251 @@ +/** + * 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 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.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 (setupStrokePaint(paint, opacity)) { + canvas.drawPath(mPath, paint); + } + if (setupFillPaint(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) { + // TODO(6352067): Support dashes + FLog.w(ReactConstants.TAG, "ART: Dashes are not supported yet!"); + } + 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; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceView.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceView.java new file mode 100644 index 000000000..42aecf392 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceView.java @@ -0,0 +1,45 @@ +/** + * 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.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.view.View; + +/** + * Custom {@link View} implementation that draws an ARTSurface React view and its children. + */ +public class ARTSurfaceView extends View { + + private @Nullable Bitmap mBitmap; + + public ARTSurfaceView(Context context) { + super(context); + } + + public void setBitmap(Bitmap bitmap) { + if (mBitmap != null) { + mBitmap.recycle(); + } + mBitmap = bitmap; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mBitmap != null) { + canvas.drawBitmap(mBitmap, 0, 0, null); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceViewManager.java new file mode 100644 index 000000000..98fc5293b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceViewManager.java @@ -0,0 +1,61 @@ +/** + * 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 android.graphics.Bitmap; + +import com.facebook.csslayout.CSSNode; +import com.facebook.csslayout.MeasureOutput; +import com.facebook.react.uimanager.BaseViewManager; +import com.facebook.react.uimanager.ThemedReactContext; + +/** + * ViewManager for ARTSurfaceView React views. Renders as a {@link ARTSurfaceView} and handles + * invalidating the native view on shadow view updates happening in the underlying tree. + */ +public class ARTSurfaceViewManager extends + BaseViewManager { + + private static final String REACT_CLASS = "ARTSurfaceView"; + + private static final CSSNode.MeasureFunction MEASURE_FUNCTION = new CSSNode.MeasureFunction() { + @Override + public void measure(CSSNode node, float width, float height, MeasureOutput measureOutput) { + throw new IllegalStateException("SurfaceView should have explicit width and height set"); + } + }; + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + public ARTSurfaceViewShadowNode createShadowNodeInstance() { + ARTSurfaceViewShadowNode node = new ARTSurfaceViewShadowNode(); + node.setMeasureFunction(MEASURE_FUNCTION); + return node; + } + + @Override + public Class getShadowNodeClass() { + return ARTSurfaceViewShadowNode.class; + } + + @Override + protected ARTSurfaceView createViewInstance(ThemedReactContext reactContext) { + return new ARTSurfaceView(reactContext); + } + + @Override + public void updateExtraData(ARTSurfaceView root, Object extraData) { + root.setBitmap((Bitmap) extraData); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceViewShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceViewShadowNode.java new file mode 100644 index 000000000..b6004efa8 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTSurfaceViewShadowNode.java @@ -0,0 +1,56 @@ +/** + * 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 android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.UIViewOperationQueue; + +/** + * Shadow node for ART virtual tree root - ARTSurfaceView + */ +public class ARTSurfaceViewShadowNode extends LayoutShadowNode { + + @Override + public boolean isVirtual() { + return false; + } + + @Override + public boolean isVirtualAnchor() { + return true; + } + + @Override + public void onCollectExtraUpdates(UIViewOperationQueue uiUpdater) { + super.onCollectExtraUpdates(uiUpdater); + uiUpdater.enqueueUpdateExtraData(getReactTag(), drawOutput()); + } + + private Object drawOutput() { + // TODO(7255985): Use TextureView and pass Surface from the view to draw on it asynchronously + // instead of passing the bitmap (which is inefficient especially in terms of memory usage) + Bitmap bitmap = Bitmap.createBitmap( + (int) getLayoutWidth(), + (int) getLayoutHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + for (int i = 0; i < getChildCount(); i++) { + ARTVirtualNode child = (ARTVirtualNode) getChildAt(i); + child.draw(canvas, paint, 1f); + child.markUpdateSeen(); + } + return bitmap; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTTextShadowNode.java new file mode 100644 index 000000000..80be1a729 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTTextShadowNode.java @@ -0,0 +1,141 @@ +/** + * 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.Typeface; +import android.text.TextUtils; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.uimanager.ReactProp; + +/** + * Shadow node for virtual ARTText view + */ +public class ARTTextShadowNode extends ARTShapeShadowNode { + + private static final String PROP_LINES = "lines"; + + private static final String PROP_FONT = "font"; + private static final String PROP_FONT_FAMILY = "fontFamily"; + private static final String PROP_FONT_SIZE = "fontSize"; + private static final String PROP_FONT_STYLE = "fontStyle"; + private static final String PROP_FONT_WEIGHT = "fontWeight"; + + private static final int DEFAULT_FONT_SIZE = 12; + + private static final int TEXT_ALIGNMENT_CENTER = 2; + private static final int TEXT_ALIGNMENT_LEFT = 0; + private static final int TEXT_ALIGNMENT_RIGHT = 1; + + private @Nullable ReadableMap mFrame; + private int mTextAlignment = TEXT_ALIGNMENT_LEFT; + + @ReactProp(name = "frame") + public void setFrame(@Nullable ReadableMap frame) { + mFrame = frame; + } + + @ReactProp(name = "alignment", defaultInt = TEXT_ALIGNMENT_LEFT) + public void setAlignment(int alignment) { + mTextAlignment = alignment; + } + + @Override + public void draw(Canvas canvas, Paint paint, float opacity) { + if (mFrame == null) { + return; + } + opacity *= mOpacity; + if (opacity <= MIN_OPACITY_FOR_DRAW) { + return; + } + if (!mFrame.hasKey(PROP_LINES)) { + return; + } + ReadableArray linesProp = mFrame.getArray(PROP_LINES); + if (linesProp == null || linesProp.size() == 0) { + return; + } + + // only set up the canvas if we have something to draw + saveAndSetupCanvas(canvas); + String[] lines = new String[linesProp.size()]; + for (int i = 0; i < lines.length; i++) { + lines[i] = linesProp.getString(i); + } + String text = TextUtils.join("\n", lines); + if (setupStrokePaint(paint, opacity)) { + applyTextPropertiesToPaint(paint); + if (mPath == null) { + canvas.drawText(text, 0, -paint.ascent(), paint); + } else { + canvas.drawTextOnPath(text, mPath, 0, 0, paint); + } + } + if (setupFillPaint(paint, opacity)) { + applyTextPropertiesToPaint(paint); + if (mPath == null) { + canvas.drawText(text, 0, -paint.ascent(), paint); + } else { + canvas.drawTextOnPath(text, mPath, 0, 0, paint); + } + } + restoreCanvas(canvas); + markUpdateSeen(); + } + + private void applyTextPropertiesToPaint(Paint paint) { + int alignment = mTextAlignment; + switch (alignment) { + case TEXT_ALIGNMENT_LEFT: + paint.setTextAlign(Paint.Align.LEFT); + break; + case TEXT_ALIGNMENT_RIGHT: + paint.setTextAlign(Paint.Align.RIGHT); + break; + case TEXT_ALIGNMENT_CENTER: + paint.setTextAlign(Paint.Align.CENTER); + break; + } + if (mFrame != null) { + if (mFrame.hasKey(PROP_FONT)) { + ReadableMap font = mFrame.getMap(PROP_FONT); + if (font != null) { + float fontSize = DEFAULT_FONT_SIZE; + if (font.hasKey(PROP_FONT_SIZE)) { + fontSize = (float) font.getDouble(PROP_FONT_SIZE); + } + paint.setTextSize(fontSize * mScale); + boolean isBold = + font.hasKey(PROP_FONT_WEIGHT) && "bold".equals(font.getString(PROP_FONT_WEIGHT)); + boolean isItalic = + font.hasKey(PROP_FONT_STYLE) && "italic".equals(font.getString(PROP_FONT_STYLE)); + int fontStyle; + if (isBold && isItalic) { + fontStyle = Typeface.BOLD_ITALIC; + } else if (isBold) { + fontStyle = Typeface.BOLD; + } else if (isItalic) { + fontStyle = Typeface.ITALIC; + } else { + fontStyle = Typeface.NORMAL; + } + // NB: if the font family is null / unsupported, the default one will be used + paint.setTypeface(Typeface.create(font.getString(PROP_FONT_FAMILY), fontStyle)); + } + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTVirtualNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTVirtualNode.java new file mode 100644 index 000000000..b894e9845 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/ARTVirtualNode.java @@ -0,0 +1,114 @@ +/** + * 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.Matrix; +import android.graphics.Paint; + +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.CatalystStylesDiffMap; +import com.facebook.react.uimanager.DisplayMetricsHolder; +import com.facebook.react.uimanager.ReactProp; +import com.facebook.react.uimanager.ReactShadowNode; + +/** + * Base class for ARTView virtual nodes: {@link ARTGroupShadowNode}, {@link ARTShapeShadowNode} and + * indirectly for {@link ARTTextShadowNode}. + */ +public abstract class ARTVirtualNode extends ReactShadowNode { + + protected static final float MIN_OPACITY_FOR_DRAW = 0.01f; + + private static final float[] sMatrixData = new float[9]; + private static final float[] sRawMatrix = new float[9]; + + protected float mOpacity = 1f; + private @Nullable Matrix mMatrix = new Matrix(); + + protected final float mScale; + + public ARTVirtualNode() { + mScale = DisplayMetricsHolder.getDisplayMetrics().density; + } + + @Override + public boolean isVirtual() { + return true; + } + + public abstract void draw(Canvas canvas, Paint paint, float opacity); + + /** + * Sets up the transform matrix on the canvas before an element is drawn. + * + * NB: for perf reasons this does not apply opacity, as that would mean creating a new canvas + * layer (which allocates an offscreen bitmap) and having it composited afterwards. Instead, the + * drawing code should apply opacity recursively. + * + * @param canvas the canvas to set up + */ + protected final void saveAndSetupCanvas(Canvas canvas) { + canvas.save(); + if (mMatrix != null) { + canvas.concat(mMatrix); + } + } + + /** + * Restore the canvas after an element was drawn. This is always called in mirror with + * {@link #saveAndSetupCanvas}. + * + * @param canvas the canvas to restore + */ + protected void restoreCanvas(Canvas canvas) { + canvas.restore(); + } + + @ReactProp(name = "opacity", defaultFloat = 1f) + public void setOpacity(float opacity) { + mOpacity = opacity; + markUpdated(); + } + + @ReactProp(name = "transform") + public void setTransform(@Nullable ReadableArray transformArray) { + if (transformArray != null) { + int matrixSize = PropHelper.toFloatArray(transformArray, sMatrixData); + if (matrixSize == 6) { + setupMatrix(); + } else if (matrixSize != -1) { + throw new JSApplicationIllegalArgumentException("Transform matrices must be of size 6"); + } + } else { + mMatrix = null; + } + markUpdated(); + } + + protected void setupMatrix() { + sRawMatrix[0] = sMatrixData[0]; + sRawMatrix[1] = sMatrixData[2]; + sRawMatrix[2] = sMatrixData[4] * mScale; + sRawMatrix[3] = sMatrixData[1]; + sRawMatrix[4] = sMatrixData[3]; + sRawMatrix[5] = sMatrixData[5] * mScale; + sRawMatrix[6] = 0; + sRawMatrix[7] = 0; + sRawMatrix[8] = 1; + if (mMatrix == null) { + mMatrix = new Matrix(); + } + mMatrix.setValues(sRawMatrix); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/art/PropHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/art/PropHelper.java new file mode 100644 index 000000000..b8bc6246a --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/art/PropHelper.java @@ -0,0 +1,54 @@ +/** + * 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 com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.CatalystStylesDiffMap; + +/** + * Contains static helper methods for accessing props. + */ +/* package */ class PropHelper { + + /** + * Converts {@link ReadableArray} to an array of {@code float}. Returns newly created array. + * + * @return a {@code float[]} if converted successfully, or {@code null} if {@param value} was + * {@code null}. + */ + /*package*/ static @Nullable float[] toFloatArray(@Nullable ReadableArray value) { + if (value != null) { + float[] result = new float[value.size()]; + toFloatArray(value, result); + return result; + } + return null; + } + + /** + * Converts given {@link ReadableArray} to an array of {@code float}. Writes result to the array + * passed in {@param into}. This method will write to the output array up to the number of items + * from the input array. If the input array is longer than output the remaining part of the input + * will not be converted. + * + * @param value input array + * @param into output array + * @return number of items copied from input to the output array + */ + /*package*/ static int toFloatArray(ReadableArray value, float[] into) { + int length = value.size() > into.length ? into.length : value.size(); + for (int i = 0; i < length; i++) { + into[i] = (float) value.getDouble(i); + } + return value.size(); + } +}