Skip to content

Commit

Permalink
Open-source ART for Android
Browse files Browse the repository at this point in the history
Summary:
This is already open-source on iOS, albeit badly documented. Now Android too!

Tested in open source by adding a `import com.facebook.react.ARTPackage;` and `.addPackage(new ARTPackage())` to UIExplorerActivity.java, running

```
./gradlew :Examples:UIExplorer:android:app:installDebug
```

and copying VectorWidget into the UIExplorer JS code as described in http://browniefed.com/blog/2015/05/03/getting-react-art-running-on-react-native/.

public

Reviewed By: foghina

Differential Revision: D2700481

fb-gh-sync-id: 2a5308b022869ecc1868a46dbecf397581ddbe04
  • Loading branch information
sophiebits authored and facebook-github-bot-4 committed Jan 5, 2016
1 parent 27d1022 commit 27ac047
Show file tree
Hide file tree
Showing 10 changed files with 853 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
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;
Expand Down Expand Up @@ -74,6 +76,10 @@ public List<Class<? extends JavaScriptModule>> createJSModules() {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
ARTRenderableViewManager.createARTGroupViewManager(),
ARTRenderableViewManager.createARTShapeViewManager(),
ARTRenderableViewManager.createARTTextViewManager(),
new ARTSurfaceViewManager(),
new ReactDrawerLayoutManager(),
new ReactHorizontalScrollViewManager(),
new ReactImageManager(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<View, ReactShadowNode> {

/* 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<? extends ReactShadowNode> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

2 comments on commit 27ac047

@hufeng
Copy link

@hufeng hufeng commented on 27ac047 Jan 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome

@grubstarstar
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to work great on iOS but on Android it does a small amount of animation and then crashes almost immediately on my Nexus 5X running Android 7.1.1. That's using the VectorWidget from the link suggested.

Please sign in to comment.