add touch responder system on android

This commit is contained in:
Horcrux
2016-05-18 17:33:54 +08:00
parent 75e03f87e9
commit ce74efc58d
21 changed files with 629 additions and 223 deletions

66
Example/android/app/BUCK Normal file
View File

@@ -0,0 +1,66 @@
import re
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
lib_deps = []
for jarfile in glob(['libs/*.jar']):
name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
lib_deps.append(':' + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)
for aarfile in glob(['libs/*.aar']):
name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
lib_deps.append(':' + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
android_library(
name = 'all-libs',
exported_deps = lib_deps
)
android_library(
name = 'app-code',
srcs = glob([
'src/main/java/**/*.java',
]),
deps = [
':all-libs',
':build_config',
':res',
],
)
android_build_config(
name = 'build_config',
package = 'com.artsvgexample',
)
android_resource(
name = 'res',
res = 'src/main/res',
package = 'com.artsvgexample',
)
android_binary(
name = 'app',
package_type = 'debug',
manifest = 'src/main/AndroidManifest.xml',
keystore = '//android/keystores:debug',
deps = [
':app-code',
],
)

View File

@@ -9,7 +9,7 @@ import com.android.build.OutputFile
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "react.gradle"` line.
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
@@ -26,7 +26,9 @@ import com.android.build.OutputFile
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property is in the format 'bundleIn${productFlavor}${buildType}'
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
@@ -57,11 +59,10 @@ import com.android.build.OutputFile
* ]
*/
apply from: "react.gradle"
apply from: "../../node_modules/react-native/react.gradle"
/**
* Set this to true to create three separate APKs instead of one:
* - A universal APK that works on all devices
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
@@ -91,9 +92,9 @@ android {
}
splits {
abi {
enable enableSeparateBuildPerCPUArchitecture
universalApk false
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86"
}
}
@@ -119,9 +120,15 @@ android {
}
dependencies {
compile project(':react-native-svg')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:0.20.+"
compile project(':react-native-svg')
compile "com.facebook.react:react-native:+" // From node_modules
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}

View File

@@ -61,7 +61,3 @@
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# stetho
-dontwarn com.facebook.stetho.**

View File

@@ -1,12 +1,12 @@
package com.artsvgexample;
import com.facebook.react.ReactActivity;
import com.horcrux.svg.RNSvgPackage;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.List;
import com.horcrux.svg.RNSvgPackage;
public class MainActivity extends ReactActivity {
@@ -35,8 +35,8 @@ public class MainActivity extends ReactActivity {
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new RNSvgPackage(),
new MainReactPackage()
new MainReactPackage(),
new RNSvgPackage()
);
}
}

View File

@@ -16,5 +16,9 @@ allprojects {
repositories {
mavenLocal()
jcenter()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$projectDir/../../node_modules/react-native/android"
}
}
}

View File

@@ -1,6 +1,5 @@
rootProject.name = 'ArtSvgExample'
include ':app'
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')

View File

@@ -62,19 +62,14 @@ public class RNSVGCircleShadowNode extends RNSVGPathShadowNode {
@Override
protected Path getPath(Canvas canvas, Paint paint) {
Path path = new Path();
Rect box = canvas.getClipBounds();
float height = box.height();
float width = box.width();
float cx = PropHelper.fromPercentageToFloat(mCx, width, 0, mScale);
float cy = PropHelper.fromPercentageToFloat(mCy, height, 0, mScale);
float cx = PropHelper.fromPercentageToFloat(mCx, mWidth, 0, mScale);
float cy = PropHelper.fromPercentageToFloat(mCy, mHeight, 0, mScale);
float r;
if (PropHelper.isPercentage(mR)) {
r = PropHelper.fromPercentageToFloat(mR, 1, 0, 1);
float powX = (float)Math.pow((width * r), 2);
float powY = (float)Math.pow((height * r), 2);
float powX = (float)Math.pow((mWidth * r), 2);
float powY = (float)Math.pow((mHeight * r), 2);
r = (float)Math.sqrt(powX + powY) / (float)Math.sqrt(2);
} else {
r = Float.parseFloat(mR) * mScale;

View File

@@ -65,15 +65,11 @@ public class RNSVGEllipseShadowNode extends RNSVGPathShadowNode {
protected Path getPath(Canvas canvas, Paint paint) {
Path path = new Path();
Rect box = canvas.getClipBounds();
float height = box.height();
float width = box.width();
// draw ellipse
float cx = PropHelper.fromPercentageToFloat(mCx, width, 0, mScale);
float cy = PropHelper.fromPercentageToFloat(mCy, height, 0, mScale);
float rx = PropHelper.fromPercentageToFloat(mRx, width, 0, mScale);
float ry = PropHelper.fromPercentageToFloat(mRy, height, 0, mScale);
float cx = PropHelper.fromPercentageToFloat(mCx, mWidth, 0, mScale);
float cy = PropHelper.fromPercentageToFloat(mCy, mHeight, 0, mScale);
float rx = PropHelper.fromPercentageToFloat(mRx, mWidth, 0, mScale);
float ry = PropHelper.fromPercentageToFloat(mRy, mHeight, 0, mScale);
RectF oval = new RectF(cx - rx, cy - ry, cx + rx, cy + ry);
path.addOval(oval, Path.Direction.CW);

View File

@@ -12,7 +12,10 @@ package com.horcrux.svg;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.uimanager.annotations.ReactProp;
@@ -29,12 +32,6 @@ public class RNSVGGroupShadowNode extends RNSVGVirtualNode {
markUpdated();
}
@Override
public boolean isVirtual() {
return true;
}
public void draw(Canvas canvas, Paint paint, float opacity) {
opacity *= mOpacity;
if (opacity > MIN_OPACITY_FOR_DRAW) {
@@ -43,6 +40,7 @@ public class RNSVGGroupShadowNode extends RNSVGVirtualNode {
if (mAsClipPath == null) {
for (int i = 0; i < getChildCount(); i++) {
RNSVGVirtualNode child = (RNSVGVirtualNode) getChildAt(i);
child.setDimensions(mWidth, mHeight);
child.draw(canvas, paint, opacity);
child.markUpdateSeen();
}
@@ -62,4 +60,23 @@ public class RNSVGGroupShadowNode extends RNSVGVirtualNode {
}
return path;
}
@Override
public int hitTest(Point point, View view) {
// TODO: run hit test only if necessary
// TODO: ClipPath never run hitTest
if (mClipPathId == null) {
return -1;
}
int viewTag = -1;
for (int i = getChildCount() - 1; i >= 0; i--) {
viewTag = ((RNSVGVirtualNode) getChildAt(i)).hitTest(point, ((ViewGroup) view).getChildAt(i));
if (viewTag != -1) {
break;
}
}
return viewTag;
}
}

View File

@@ -78,6 +78,7 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
bitmap = BitmapFactory.decodeStream(url.openConnection().getInputStream());
}
} catch (Exception e) {
Log.e("URI", "" + e);
}
return bitmap;
@@ -131,6 +132,7 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
@Override
public void draw(Canvas canvas, Paint paint, float opacity) {
canvas.saveLayer(0f, 0f, 0f, 0f, paint, Canvas.ALL_SAVE_FLAG);
Log.e("Count", "" + canvas.getSaveCount());
loadBitmap(getResourceDrawableId(getThemedContext(), null), canvas, paint);
}

View File

@@ -69,14 +69,10 @@ public class RNSVGLineShadowNode extends RNSVGPathShadowNode {
protected Path getPath(Canvas canvas, Paint paint) {
Path path = new Path();
Rect box = canvas.getClipBounds();
float height = box.height();
float width = box.width();
float x1 = PropHelper.fromPercentageToFloat(mX1, width, 0, mScale);
float y1 = PropHelper.fromPercentageToFloat(mY1, height, 0, mScale);
float x2 = PropHelper.fromPercentageToFloat(mX2, width, 0, mScale);
float y2 = PropHelper.fromPercentageToFloat(mY2, height, 0, mScale);
float x1 = PropHelper.fromPercentageToFloat(mX1, mWidth, 0, mScale);
float y1 = PropHelper.fromPercentageToFloat(mY1, mHeight, 0, mScale);
float x2 = PropHelper.fromPercentageToFloat(mX2, mWidth, 0, mScale);
float y2 = PropHelper.fromPercentageToFloat(mY2, mHeight, 0, mScale);
path.moveTo(x1, y1);
path.lineTo(x2, y2);

View File

@@ -11,6 +11,7 @@ package com.horcrux.svg;
import javax.annotation.Nullable;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
@@ -26,6 +27,8 @@ import android.graphics.Region;
import android.graphics.Shader;
import android.graphics.Matrix;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
@@ -53,14 +56,14 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
private @Nullable float[] mStrokeDasharray;
private float mStrokeWidth = 1;
private float mStrokeDashoffset = 0;
private int mStrokeLinecap = CAP_ROUND;
private int mStrokeLinejoin = JOIN_ROUND;
private int mFillRule = FILL_RULE_NONZERO;
private Paint.Cap mStrokeLinecap = Paint.Cap.ROUND;
private Paint.Join mStrokeLinejoin = Paint.Join.ROUND;
private Path.FillType mFillRule = Path.FillType.WINDING;
private boolean mFillRuleSet;
protected Path mPath;
private boolean mPathSet;
private float[] mD;
private Point mPaint;
@ReactProp(name = "d")
public void setPath(@Nullable ReadableArray shapePath) {
@@ -79,7 +82,17 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
@ReactProp(name = "fillRule", defaultInt = FILL_RULE_NONZERO)
public void setFillRule(int fillRule) {
mFillRule = fillRule;
switch (fillRule) {
case FILL_RULE_EVENODD:
mFillRule = Path.FillType.EVEN_ODD;
break;
case FILL_RULE_NONZERO:
break;
default:
throw new JSApplicationIllegalArgumentException(
"fillRule " + mFillRule + " unrecognized");
}
mFillRuleSet = true;
setupPath();
markUpdated();
@@ -117,13 +130,39 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
@ReactProp(name = "strokeLinecap", defaultInt = CAP_ROUND)
public void setStrokeLinecap(int strokeLinecap) {
mStrokeLinecap = strokeLinecap;
switch (strokeLinecap) {
case CAP_BUTT:
mStrokeLinecap = Paint.Cap.BUTT;
break;
case CAP_SQUARE:
mStrokeLinecap = Paint.Cap.SQUARE;
break;
case CAP_ROUND:
mStrokeLinecap = Paint.Cap.ROUND;
break;
default:
throw new JSApplicationIllegalArgumentException(
"strokeLinecap " + mStrokeLinecap + " unrecognized");
}
markUpdated();
}
@ReactProp(name = "strokeLinejoin", defaultInt = JOIN_ROUND)
public void setStrokeLinejoin(int strokeLinejoin) {
mStrokeLinejoin = strokeLinejoin;
switch (strokeLinejoin) {
case JOIN_MITER:
mStrokeLinejoin = Paint.Join.MITER;
break;
case JOIN_BEVEL:
mStrokeLinejoin = Paint.Join.BEVEL;
break;
case JOIN_ROUND:
mStrokeLinejoin = Paint.Join.ROUND;
break;
default:
throw new JSApplicationIllegalArgumentException(
"strokeLinejoin " + mStrokeLinejoin + " unrecognized");
}
markUpdated();
}
@@ -178,7 +217,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
/**
* Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true}
* Sets up paint 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, @Nullable RectF box) {
@@ -193,7 +232,7 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
}
/**
* Sets up {@link #mPaint} according to the props set on a shadow view. Returns {@code true}
* Sets up paint 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, @Nullable RectF box) {
@@ -203,35 +242,8 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
paint.reset();
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
switch (mStrokeLinecap) {
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(
"strokeLinecap " + mStrokeLinecap + " unrecognized");
}
switch (mStrokeLinejoin) {
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(
"strokeLinejoin " + mStrokeLinejoin + " unrecognized");
}
paint.setStrokeCap(mStrokeLinecap);
paint.setStrokeJoin(mStrokeLinejoin);
paint.setStrokeWidth(mStrokeWidth * mScale);
setupPaint(paint, opacity, mStrokeColor, box);
@@ -312,21 +324,67 @@ public class RNSVGPathShadowNode extends RNSVGVirtualNode {
// TODO: Support pattern.
FLog.w(ReactConstants.TAG, "RNSVG: Color type " + colorType + " not supported!");
}
}
protected Path getPath(@Nullable Canvas canvas, @Nullable Paint paint) {
Path path = new Path();
switch (mFillRule) {
case FILL_RULE_EVENODD:
path.setFillType(Path.FillType.EVEN_ODD);
break;
case FILL_RULE_NONZERO:
break;
default:
throw new JSApplicationIllegalArgumentException(
"fillRule " + mFillRule + " unrecognized");
}
path.setFillType(mFillRule);
super.createPath(mD, path);
return path;
}
@Override
public int hitTest(Point point, View view) {
Bitmap bitmap = Bitmap.createBitmap(
(int) mWidth,
(int) mHeight,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.concat(mMatrix);
Paint paint = new Paint();
clip(canvas, paint);
setHitTestFill(paint);
canvas.drawPath(mPath, paint);
if (setHitTestStroke(paint)) {
canvas.drawPath(mPath, paint);
}
canvas.setBitmap(bitmap);
try {
if (bitmap.getPixel(point.x, point.y) != 0) {
return view.getId();
}
} catch (Exception e) {
return -1;
} finally {
bitmap.recycle();
}
return -1;
}
protected void setHitTestFill(Paint paint) {
paint.reset();
paint.setARGB(255, 0, 0, 0);
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
}
protected boolean setHitTestStroke(Paint paint) {
if (mStrokeWidth == 0) {
return false;
}
paint.reset();
paint.setARGB(255, 0, 0, 0);
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStrokeWidth * mScale);
paint.setStrokeCap(mStrokeLinecap);
paint.setStrokeJoin(mStrokeLinejoin);
return true;
}
}

View File

@@ -31,9 +31,9 @@ public class RNSVGRectShadowNode extends RNSVGPathShadowNode {
private String mY;
private String mWidth;
private String mW;
private String mHeight;
private String mH;
private String mRx;
@@ -54,14 +54,14 @@ public class RNSVGRectShadowNode extends RNSVGPathShadowNode {
@ReactProp(name = "width")
public void setWidth(String width) {
mWidth = width;
mW = width;
markUpdated();
}
@ReactProp(name = "height")
public void setHeight(String height) {
mHeight = height;
mH = height;
markUpdated();
}
@@ -88,16 +88,12 @@ public class RNSVGRectShadowNode extends RNSVGPathShadowNode {
protected Path getPath(Canvas canvas, Paint paint) {
Path path = new Path();
Rect box = canvas.getClipBounds();
float height = box.height();
float width = box.width();
float x = PropHelper.fromPercentageToFloat(mX, width, 0, mScale);
float y = PropHelper.fromPercentageToFloat(mY, height, 0, mScale);
float w = PropHelper.fromPercentageToFloat(mWidth, width, 0, mScale);
float h = PropHelper.fromPercentageToFloat(mHeight, height, 0, mScale);
float rx = PropHelper.fromPercentageToFloat(mRx, width, 0, mScale);
float ry = PropHelper.fromPercentageToFloat(mRy, height, 0, mScale);
float x = PropHelper.fromPercentageToFloat(mX, mWidth, 0, mScale);
float y = PropHelper.fromPercentageToFloat(mY, mHeight, 0, mScale);
float w = PropHelper.fromPercentageToFloat(mW, mWidth, 0, mScale);
float h = PropHelper.fromPercentageToFloat(mH, mHeight, 0, mScale);
float rx = PropHelper.fromPercentageToFloat(mRx, mWidth, 0, mScale);
float ry = PropHelper.fromPercentageToFloat(mRy, mHeight, 0, mScale);
if (rx != 0 || ry != 0) {
if (rx == 0) {

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2015-present, Horcrux.
* All rights reserved.
*
* This source code is licensed under the MIT-style license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.horcrux.svg;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.common.SystemClock;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.TouchEvent;
import com.facebook.react.uimanager.events.TouchEventType;
import javax.annotation.Nullable;
// NativeGestureUtil.notifyNativeGestureStarted
/**
* Custom {@link View} implementation that draws an RNSVGSvg React view and its \children.
*/
public class RNSVGRenderableView extends ViewGroup {
public RNSVGRenderableView(Context context) {
super(context);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
}

View File

@@ -9,22 +9,32 @@
package com.horcrux.svg;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
//import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.common.SystemClock;
import com.facebook.react.touch.JSResponderHandler;
import com.facebook.react.touch.ReactInterceptingViewGroup;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.TouchEvent;
/**
* ViewManager for all shadowed RNSVG views: Group, Path and Text. Since these never get rendered
* into native views and don't need any logic (all the logic is in {@link RNSVGSvgView}), this
* "stubbed" ViewManager is used for all of them.
*/
public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNode> {
public class RNSVGRenderableViewManager extends ViewGroupManager<ViewGroup> {
/* package */ static final String CLASS_GROUP = "RNSVGGroup";
/* package */ static final String CLASS_SVG = "RNSVGPath";
/* package */ static final String CLASS_PATH = "RNSVGPath";
/* package */ static final String CLASS_TEXT = "RNSVGText";
/* package */ static final String CLASS_IMAGE = "RNSVGImage";
/* package */ static final String CLASS_CIRCLE = "RNSVGCircle";
@@ -34,12 +44,14 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
private final String mClassName;
protected RNSVGVirtualNode mVirtualNode;
public static RNSVGRenderableViewManager createRNSVGGroupViewManager() {
return new RNSVGRenderableViewManager(CLASS_GROUP);
}
public static RNSVGRenderableViewManager createRNSVGPathViewManager() {
return new RNSVGRenderableViewManager(CLASS_SVG);
return new RNSVGRenderableViewManager(CLASS_PATH);
}
public static RNSVGRenderableViewManager createRNSVGTextViewManager() {
@@ -76,58 +88,66 @@ public class RNSVGRenderableViewManager extends ViewManager<View, ReactShadowNod
}
@Override
public ReactShadowNode createShadowNodeInstance() {
if (mClassName == CLASS_GROUP) {
return new RNSVGGroupShadowNode();
} else if (mClassName == CLASS_SVG) {
return new RNSVGPathShadowNode();
} else if (mClassName == CLASS_CIRCLE) {
return new RNSVGCircleShadowNode();
} else if (mClassName == CLASS_ELLIPSE) {
return new RNSVGEllipseShadowNode();
} else if (mClassName == CLASS_LINE) {
return new RNSVGLineShadowNode();
} else if (mClassName == CLASS_RECT) {
return new RNSVGRectShadowNode();
} else if (mClassName == CLASS_TEXT) {
return new RNSVGTextShadowNode();
} else if (mClassName == CLASS_IMAGE) {
return new RNSVGImageShadowNode();
} else {
public RNSVGVirtualNode createShadowNodeInstance() {
switch (mClassName) {
case CLASS_GROUP:
mVirtualNode = new RNSVGGroupShadowNode();
break;
case CLASS_PATH:
mVirtualNode = new RNSVGPathShadowNode();
break;
case CLASS_CIRCLE:
mVirtualNode = new RNSVGCircleShadowNode();
break;
case CLASS_ELLIPSE:
mVirtualNode = new RNSVGEllipseShadowNode();
break;
case CLASS_LINE:
mVirtualNode = new RNSVGLineShadowNode();
break;
case CLASS_RECT:
mVirtualNode = new RNSVGRectShadowNode();
break;
case CLASS_TEXT:
mVirtualNode = new RNSVGTextShadowNode();
break;
case CLASS_IMAGE:
mVirtualNode = new RNSVGImageShadowNode();
break;
default:
throw new IllegalStateException("Unexpected type " + mClassName);
}
return mVirtualNode;
}
@Override
public Class<? extends ReactShadowNode> getShadowNodeClass() {
if (mClassName == CLASS_GROUP) {
public Class<? extends RNSVGVirtualNode> getShadowNodeClass() {
switch (mClassName) {
case CLASS_GROUP:
return RNSVGGroupShadowNode.class;
} else if (mClassName == CLASS_SVG) {
case CLASS_PATH:
return RNSVGPathShadowNode.class;
} else if (mClassName == CLASS_CIRCLE) {
case CLASS_CIRCLE:
return RNSVGCircleShadowNode.class;
} else if (mClassName == CLASS_ELLIPSE) {
case CLASS_ELLIPSE:
return RNSVGEllipseShadowNode.class;
} else if (mClassName == CLASS_LINE) {
case CLASS_LINE:
return RNSVGLineShadowNode.class;
} else if (mClassName == CLASS_RECT) {
case CLASS_RECT:
return RNSVGRectShadowNode.class;
} else if (mClassName == CLASS_TEXT) {
case CLASS_TEXT:
return RNSVGTextShadowNode.class;
} else if (mClassName == CLASS_IMAGE) {
case CLASS_IMAGE:
return RNSVGImageShadowNode.class;
} else {
default:
throw new IllegalStateException("Unexpected type " + mClassName);
}
}
@Override
protected View createViewInstance(ThemedReactContext reactContext) {
throw new IllegalStateException("RNSVGPath does not map into a native view");
}
@Override
public void updateExtraData(View root, Object extraData) {
throw new IllegalStateException("RNSVGPath does not map into a native view");
protected ViewGroup createViewInstance(ThemedReactContext reactContext) {
return new RNSVGRenderableView(reactContext);
}
}

View File

@@ -14,15 +14,43 @@ import javax.annotation.Nullable;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.common.SystemClock;
import com.facebook.react.touch.OnInterceptTouchEventListener;
import com.facebook.react.touch.ReactInterceptingViewGroup;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.TouchTargetHelper;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.TouchEvent;
import com.facebook.react.uimanager.events.TouchEventType;
import com.facebook.react.views.view.ReactClippingViewGroup;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.NativeGestureUtil;
// NativeGestureUtil.notifyNativeGestureStarted
/**
* Custom {@link View} implementation that draws an RNSVGSvg React view and its children.
* Custom {@link View} implementation that draws an RNSVGSvg React view and its \children.
*/
public class RNSVGSvgView extends View {
public class RNSVGSvgView extends ViewGroup {
private @Nullable Bitmap mBitmap;
private RNSVGSvgViewShadowNode mSvgViewShadowNode;
private int mTargetTag;
public RNSVGSvgView(Context context, RNSVGSvgViewShadowNode shadowNode) {
super(context);
mSvgViewShadowNode = shadowNode;
}
public RNSVGSvgView(Context context) {
super(context);
}
@@ -42,4 +70,113 @@ public class RNSVGSvgView extends View {
canvas.drawBitmap(mBitmap, 0, 0, null);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
mTargetTag = mSvgViewShadowNode.hitTest(new Point((int) event.getX(), (int) event.getY()), this);
if (mTargetTag != -1) {
EventDispatcher eventDispatcher = ((ThemedReactContext) this.getContext()).getNativeModule(UIManagerModule.class).getEventDispatcher();
handleTouchEvent(event, eventDispatcher);
return true;
}
return super.dispatchTouchEvent(event);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
public void handleTouchEvent(MotionEvent ev, EventDispatcher eventDispatcher) {
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_DOWN) {
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
mTargetTag,
SystemClock.nanoTime(),
TouchEventType.START,
ev,
ev.getX(),
ev.getX()));
} else if (mTargetTag == -1) {
// All the subsequent action types are expected to be called after ACTION_DOWN thus target
// is supposed to be set for them.
Log.e(
"error",
"Unexpected state: received touch event but didn't get starting ACTION_DOWN for this " +
"gesture before");
} else if (action == MotionEvent.ACTION_UP) {
// End of the gesture. We reset target tag to -1 and expect no further event associated with
// this gesture.
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
mTargetTag,
SystemClock.nanoTime(),
TouchEventType.END,
ev,
ev.getX(),
ev.getY()));
mTargetTag = -1;
} else if (action == MotionEvent.ACTION_MOVE) {
// Update pointer position for current gesture
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
mTargetTag,
SystemClock.nanoTime(),
TouchEventType.MOVE,
ev,
ev.getX(),
ev.getY()));
} else if (action == MotionEvent.ACTION_POINTER_DOWN) {
// New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
mTargetTag,
SystemClock.nanoTime(),
TouchEventType.START,
ev,
ev.getX(),
ev.getY()));
} else if (action == MotionEvent.ACTION_POINTER_UP) {
// Exactly onw of the pointers goes up
eventDispatcher.dispatchEvent(
TouchEvent.obtain(
mTargetTag,
SystemClock.nanoTime(),
TouchEventType.END,
ev,
ev.getX(),
ev.getY()));
} else if (action == MotionEvent.ACTION_CANCEL) {
dispatchCancelEvent(ev, eventDispatcher);
mTargetTag = -1;
} else {
Log.w(
"IGNORE",
"Warning : touch event was ignored. Action=" + action + " Target=" + mTargetTag);
}
}
private void dispatchCancelEvent(MotionEvent androidEvent, EventDispatcher eventDispatcher) {
// This means the gesture has already ended, via some other CANCEL or UP event. This is not
// expected to happen very often as it would mean some child View has decided to intercept the
// touch stream and start a native gesture only upon receiving the UP/CANCEL event.
if (mTargetTag == -1) {
Log.w(
"error",
"Can't cancel already finished gesture. Is a child View trying to start a gesture from " +
"an UP/CANCEL event?");
return;
}
Assertions.assertNotNull(eventDispatcher).dispatchEvent(
TouchEvent.obtain(
mTargetTag,
SystemClock.nanoTime(),
TouchEventType.CANCEL,
androidEvent,
androidEvent.getX(),
androidEvent.getY()));
}
}

View File

@@ -10,27 +10,27 @@
package com.horcrux.svg;
import android.graphics.Bitmap;
import android.util.Log;
import com.facebook.csslayout.CSSNode;
import com.facebook.csslayout.MeasureOutput;
import com.facebook.react.uimanager.BaseViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import java.util.ArrayList;
/**
* ViewManager for RNSVGSvgView React views. Renders as a {@link RNSVGSvgView} and handles
* invalidating the native view on shadow view updates happening in the underlying tree.
*/
public class RNSVGSvgViewManager extends
BaseViewManager<RNSVGSvgView, RNSVGSvgViewShadowNode> {
public class RNSVGSvgViewManager extends ViewGroupManager<RNSVGSvgView> {
private static final String REACT_CLASS = "RNSVGSvgView";
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("SvgView should have explicit width and height set");
}
};
// TODO: use an ArrayList to connect RNSVGSvgViewShadowNode with RNSVGSvgView, not sure if there will be a race condition.
// TODO: find a better way to replace this
private ArrayList<RNSVGSvgViewShadowNode> SvgShadowNodes = new ArrayList<>();
@Override
public String getName() {
@@ -40,7 +40,7 @@ public class RNSVGSvgViewManager extends
@Override
public RNSVGSvgViewShadowNode createShadowNodeInstance() {
RNSVGSvgViewShadowNode node = new RNSVGSvgViewShadowNode();
node.setMeasureFunction(MEASURE_FUNCTION);
SvgShadowNodes.add(node);
return node;
}
@@ -51,7 +51,9 @@ public class RNSVGSvgViewManager extends
@Override
protected RNSVGSvgView createViewInstance(ThemedReactContext reactContext) {
return new RNSVGSvgView(reactContext);
RNSVGSvgViewShadowNode shadowNode = SvgShadowNodes.get(0);
SvgShadowNodes.remove(0);
return new RNSVGSvgView(reactContext, shadowNode);
}
@Override

View File

@@ -12,6 +12,9 @@ package com.horcrux.svg;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.Log;
import android.view.ViewGroup;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.UIViewOperationQueue;
@@ -20,17 +23,6 @@ import com.facebook.react.uimanager.UIViewOperationQueue;
* Shadow node for RNSVG virtual tree root - RNSVGSvgView
*/
public class RNSVGSvgViewShadowNode extends LayoutShadowNode {
@Override
public boolean isVirtual() {
return false;
}
@Override
public boolean isVirtualAnchor() {
return true;
}
@Override
public void onCollectExtraUpdates(UIViewOperationQueue uiUpdater) {
super.onCollectExtraUpdates(uiUpdater);
@@ -40,17 +32,35 @@ public class RNSVGSvgViewShadowNode extends LayoutShadowNode {
private Object drawOutput() {
// TODO(7255985): Use TextureView and pass Svg from the view to draw on it asynchronously
// instead of passing the bitmap (which is inefficient especially in terms of memory usage)
float width = (int) getLayoutWidth();
float height = (int) getLayoutHeight();
Bitmap bitmap = Bitmap.createBitmap(
(int) getLayoutWidth(),
(int) getLayoutHeight(),
(int) width,
(int) height,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
for (int i = 0; i < getChildCount(); i++) {
RNSVGVirtualNode child = (RNSVGVirtualNode) getChildAt(i);
child.setDimensions(width, height);
child.draw(canvas, paint, 1f);
child.markUpdateSeen();
}
return bitmap;
}
public int hitTest(Point point, ViewGroup view) {
int count = getChildCount();
int viewTag = -1;
for (int i = count - 1; i >= 0; i--) {
viewTag = ((RNSVGVirtualNode) getChildAt(i)).hitTest(point, view.getChildAt(i));
if (viewTag != -1) {
break;
}
}
return viewTag;
}
}

View File

@@ -11,14 +11,18 @@ package com.horcrux.svg;
import javax.annotation.Nullable;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
@@ -84,14 +88,17 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
RectF box = getBox(paint, text);
if (setupStrokePaint(paint, opacity, box)) {
applyTextPropertiesToPaint(paint);
if (mPath == null) {
canvas.drawText(text, 0, -paint.ascent(), paint);
} else {
canvas.drawTextOnPath(text, mPath, 0, -paint.ascent(), paint);
}
drawText(canvas, paint, text);
}
if (setupFillPaint(paint, opacity, box)) {
drawText(canvas, paint, text);
}
restoreCanvas(canvas);
markUpdateSeen();
}
private void drawText(Canvas canvas, Paint paint, String text) {
applyTextPropertiesToPaint(paint);
if (mPath == null) {
@@ -100,9 +107,6 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
canvas.drawTextOnPath(text, mPath, 0, -paint.ascent(), paint);
}
}
restoreCanvas(canvas);
markUpdateSeen();
}
private String formatText() {
if (mFrame == null || !mFrame.hasKey(PROP_LINES)) {
@@ -170,6 +174,8 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
}
}
@Override
protected Path getPath(Canvas canvas, Paint paint) {
Path path = new Path();
@@ -178,6 +184,7 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
return path;
}
// TODO: get path while TextPath is set.
if (setupFillPaint(paint, 1.0f, getBox(paint, text))) {
applyTextPropertiesToPaint(paint);
paint.getTextPath(text, 0, text.length(), 0, -paint.ascent(), path);
@@ -186,4 +193,43 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
return path;
}
@Override
public int hitTest(Point point, View view) {
Bitmap bitmap = Bitmap.createBitmap(
(int) mWidth,
(int) mHeight,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.concat(mMatrix);
// todo clip detect
String text = formatText();
if (text == null) {
return -1;
}
Paint paint = new Paint();
clip(canvas, paint);
setHitTestFill(paint);
drawText(canvas, paint, text);
if (setHitTestStroke(paint)) {
drawText(canvas, paint, text);
}
canvas.setBitmap(bitmap);
try {
if (bitmap.getPixel(point.x, point.y) != 0) {
return view.getId();
}
} catch (Exception e) {
return -1;
} finally {
bitmap.recycle();
}
return -1;
}
}

View File

@@ -16,19 +16,21 @@ import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.Region;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.DisplayMetricsHolder;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.ReactShadowNode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -36,18 +38,19 @@ import java.util.Map;
* Base class for RNSVGView virtual nodes: {@link RNSVGGroupShadowNode}, {@link RNSVGPathShadowNode} and
* indirectly for {@link RNSVGTextShadowNode}.
*/
public abstract class RNSVGVirtualNode extends ReactShadowNode {
public abstract class RNSVGVirtualNode extends LayoutShadowNode {
protected static Map<String, Path> CLIP_PATHS = new HashMap<>();
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];
private @Nullable String mDefinedClipPathId;
protected float mOpacity = 1f;
protected @Nullable Matrix mMatrix = new Matrix();
private @Nullable String mDefinedClipPathId;
protected @Nullable Path mClipPath;
private @Nullable String mClipPathId;
protected @Nullable String mClipPathId;
private static final int PATH_TYPE_ARC = 4;
private static final int PATH_TYPE_CLOSE = 1;
private static final int PATH_TYPE_CURVETO = 3;
@@ -62,15 +65,13 @@ public abstract class RNSVGVirtualNode extends ReactShadowNode {
private boolean mClipRuleSet;
private boolean mClipDataSet;
protected float mWidth = 0;
protected float mHeight = 0;
public RNSVGVirtualNode() {
mScale = DisplayMetricsHolder.getWindowDisplayMetrics().density;
}
@Override
public boolean isVirtual() {
return true;
}
public abstract void draw(Canvas canvas, Paint paint, float opacity);
/**
@@ -127,8 +128,8 @@ public abstract class RNSVGVirtualNode extends ReactShadowNode {
markUpdated();
}
@ReactProp(name = "transform")
public void setTransform(@Nullable ReadableArray transformArray) {
@ReactProp(name = "trans")
public void setTrans(@Nullable ReadableArray transformArray) {
if (transformArray != null) {
int matrixSize = PropHelper.toFloatArray(transformArray, sMatrixData);
if (matrixSize == 6) {
@@ -155,7 +156,6 @@ public abstract class RNSVGVirtualNode extends ReactShadowNode {
throw new JSApplicationIllegalArgumentException(
"clipRule " + mClipRule + " unrecognized");
}
createPath(mClipData, mClipPath);
}
}
@@ -204,7 +204,6 @@ public abstract class RNSVGVirtualNode extends ReactShadowNode {
int i = 0;
while (i < data.length) {
int type = (int) data[i++];
switch (type) {
case PATH_TYPE_MOVETO:
@@ -270,16 +269,23 @@ public abstract class RNSVGVirtualNode extends ReactShadowNode {
}
}
public void setDimensions(float width, float height) {
mWidth = width;
mHeight = height;
}
abstract public int hitTest(Point point, View view);
protected void defineClipPath(Path clipPath, String clipPathId) {
CLIP_PATHS.put(clipPathId, clipPath);
mDefinedClipPathId = clipPathId;
}
abstract protected Path getPath(Canvas canvas, Paint paint);
protected void finalize() {
if (mDefinedClipPathId != null) {
CLIP_PATHS.remove(mDefinedClipPathId);
}
}
abstract protected Path getPath(Canvas canvas, Paint paint);
}

View File

@@ -1,10 +1,16 @@
import React, {Children, Component, cloneElement, PropTypes} from 'react';
import {View, requireNativeComponent} from 'react-native';
import {View, requireNativeComponent, StyleSheet} from 'react-native';
import ViewBox from './ViewBox';
// Svg - Root node of all Svg elements
let id = 0;
const styles = StyleSheet.create({
svg: {
backgroundColor: 'transparent'
}
});
class Svg extends Component{
static displayName = 'Svg';
static propTypes = {
@@ -75,6 +81,7 @@ class Svg extends Component{
preserveAspectRatio={null}
ref={ele => this.root = ele}
style={[
styles.svg,
props.style,
!isNaN(opacity) && {
opacity