finish basic Text features on Android

This commit is contained in:
Horcrux
2016-09-17 20:59:50 +08:00
parent 62ab5f06ca
commit 09b3e1208d
5 changed files with 265 additions and 172 deletions
@@ -35,6 +35,7 @@ public class RNSVGRenderableViewManager extends ViewGroupManager<ViewGroup> {
/* package */ static final String CLASS_VIEW_BOX = "RNSVGViewBox";
/* package */ static final String CLASS_LINEAR_GRADIENT = "RNSVGLinearGradient";
/* package */ static final String CLASS_RADIAL_GRADIENT = "RNSVGRadialGradient";
/* package */ static final String CLASS_SPAN = "RNSVGSpan";
private final String mClassName;
@@ -96,6 +97,10 @@ public class RNSVGRenderableViewManager extends ViewGroupManager<ViewGroup> {
return new RNSVGRenderableViewManager(CLASS_RADIAL_GRADIENT);
}
public static RNSVGRenderableViewManager createRNSVGSpanManager() {
return new RNSVGRenderableViewManager(CLASS_SPAN);
}
private RNSVGRenderableViewManager(String className) {
mClassName = className;
}
@@ -150,6 +155,9 @@ public class RNSVGRenderableViewManager extends ViewGroupManager<ViewGroup> {
case CLASS_RADIAL_GRADIENT:
mVirtualNode = new RNSVGRadialGradientShadowNode();
break;
case CLASS_SPAN:
mVirtualNode = new RNSVGSpanShadowNode();
break;
default:
throw new IllegalStateException("Unexpected type " + mClassName);
}
@@ -189,6 +197,8 @@ public class RNSVGRenderableViewManager extends ViewGroupManager<ViewGroup> {
return RNSVGLinearGradientShadowNode.class;
case CLASS_RADIAL_GRADIENT:
return RNSVGRadialGradientShadowNode.class;
case CLASS_SPAN:
return RNSVGSpanShadowNode.class;
default:
throw new IllegalStateException("Unexpected type " + mClassName);
}
@@ -0,0 +1,155 @@
/**
* 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.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.util.Log;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.annotations.ReactProp;
import javax.annotation.Nullable;
/**
* Shadow node for virtual RNSVGPath view
*/
public class RNSVGSpanShadowNode extends RNSVGPathShadowNode {
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 float mDx;
private float mDy;
private @Nullable String mPx;
private @Nullable String mPy;
private ReadableMap mFont;
private String mContent;
@ReactProp(name = "dx")
public void setDx(float dx) {
mDx = dx * mScale;
markUpdated();
}
@ReactProp(name = "dy")
public void setDy(float dy) {
mDy = dy * mScale;
markUpdated();
}
@ReactProp(name = "px")
public void setPx(String px) {
mPx = px;
markUpdated();
}
@ReactProp(name = "py")
public void setPy(String py) {
mPy = py;
markUpdated();
}
@ReactProp(name = "font")
public void setFont(ReadableMap font) {
mFont = font;
markUpdated();
}
@ReactProp(name = "content")
public void setContent(String content) {
mContent = content;
markUpdated();
}
@Override
public void draw(Canvas canvas, Paint paint, float opacity) {
mPath = getPath(canvas, paint);
super.draw(canvas, paint, opacity);
}
@Override
protected Path getPath(Canvas canvas, Paint paint) {
Path path = new Path();
RNSVGTextShadowNode text = (RNSVGTextShadowNode)getParent();
if (text == null) {
return path;
}
applyTextPropertiesToPaint(paint);
paint.getTextPath(mContent, 0, mContent.length(), 0, 0, path);
if (!mContent.isEmpty()) {
if (mPx != null) {
text.setOffsetX(PropHelper.fromPercentageToFloat(mPx, mCanvasWidth, 0, mScale), false);
}
if (mPy != null) {
text.setOffsetY(PropHelper.fromPercentageToFloat(mPy, mCanvasHeight, 0, mScale) - paint.ascent(), false);
}
text.setOffsetX(mDx, true);
text.setOffsetY(mDy, true);
Matrix matrix = new Matrix();
matrix.setTranslate(text.getOffsetX(), text.getOffsetY());
text.setOffsetX(getBox(paint).width(), true);
path.transform(matrix);
} else {
text.setOffsetX(mDx, true);
text.setOffsetY(mDy, true);
}
return path;
}
private void applyTextPropertiesToPaint(Paint paint) {
paint.setTextAlign(Paint.Align.LEFT);
float fontSize = DEFAULT_FONT_SIZE;
if (mFont.hasKey(PROP_FONT_SIZE)) {
fontSize = (float) mFont.getDouble(PROP_FONT_SIZE);
}
paint.setTextSize(fontSize * mScale);
boolean isBold = mFont.hasKey(PROP_FONT_WEIGHT) && "bold".equals(mFont.getString(PROP_FONT_WEIGHT));
boolean isItalic = mFont.hasKey(PROP_FONT_STYLE) && "italic".equals(mFont.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(mFont.getString(PROP_FONT_FAMILY), fontStyle));
}
public RectF getBox(Paint paint) {
applyTextPropertiesToPaint(paint);
Rect bound = new Rect();
paint.getTextBounds(mContent, 0, mContent.length(), bound);
return new RectF(bound);
}
}
@@ -109,13 +109,25 @@ public class RNSVGSvgView extends ViewGroup {
}
private int getAbsoluteLeft(View view) {
int left = view.getLeft() - view.getScrollX();
if (view.getParent() == view.getRootView() || view.getParent() instanceof ReactRootView) {
return left;
}
View parent = (View) view.getParent();
return view.getLeft() - view.getScrollX() + (parent instanceof ReactRootView ? 0 : getAbsoluteLeft(parent));
return left + getAbsoluteLeft(parent);
}
private int getAbsoluteTop(View view) {
int top = view.getTop() - view.getScrollY();
if (view.getParent() == view.getRootView() || view.getParent() instanceof ReactRootView) {
return top;
}
View parent = (View) view.getParent();
return view.getTop() - view.getScrollY() + (parent instanceof ReactRootView ? 0 : getAbsoluteTop(parent));
return top + getAbsoluteTop(parent);
}
private void dispatch(MotionEvent ev, TouchEventType type) {
@@ -19,8 +19,10 @@ import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import com.facebook.react.bridge.ReadableArray;
@@ -30,31 +32,17 @@ import com.facebook.react.uimanager.annotations.ReactProp;
/**
* Shadow node for virtual RNSVGText view
*/
public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
public class RNSVGTextShadowNode extends RNSVGGroupShadowNode {
private static final String PROP_LINES = "lines";
private float mOffsetX = 0;
private float mOffsetY = 0;
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;
private Path mTextPath;
@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;
@@ -70,173 +58,100 @@ public class RNSVGTextShadowNode extends RNSVGPathShadowNode {
@Override
public void draw(Canvas canvas, Paint paint, float opacity) {
opacity *= mOpacity;
if (opacity > MIN_OPACITY_FOR_DRAW) {
String text = formatText();
if (text == null) {
return;
}
float shift = getShift(paint);
// only set up the canvas if we have something to draw
int count = saveAndSetupCanvas(canvas);
clip(canvas, paint);
RectF box = getBox(paint, text);
final int count = canvas.save();
if (setupStrokePaint(paint, opacity, box)) {
drawText(canvas, paint, text);
}
if (setupFillPaint(paint, opacity, box)) {
drawText(canvas, paint, text);
}
Matrix matrix = new Matrix();
matrix.postTranslate(-shift, 0);
canvas.concat(matrix);
super.draw(canvas, paint, opacity);
restoreCanvas(canvas, count);
markUpdateSeen();
}
}
private void drawText(Canvas canvas, Paint paint, String text) {
applyTextPropertiesToPaint(paint);
if (mTextPath == null) {
canvas.drawText(text, 0, -paint.ascent(), paint);
} else {
Matrix matrix = new Matrix();
matrix.setTranslate(0, -paint.getTextSize() * 1.1f);
mTextPath.transform(matrix);
canvas.drawTextOnPath(text, mTextPath, 0, -paint.ascent(), paint);
}
}
private String formatText() {
if (mFrame == null || !mFrame.hasKey(PROP_LINES)) {
return null;
}
ReadableArray linesProp = mFrame.getArray(PROP_LINES);
if (linesProp == null || linesProp.size() == 0) {
return null;
}
String[] lines = new String[linesProp.size()];
for (int i = 0; i < lines.length; i++) {
lines[i] = linesProp.getString(i);
}
return TextUtils.join("\n", lines);
}
private RectF getBox(Paint paint, String text) {
Rect bound = new Rect();
paint.getTextBounds(text, 0, text.length(), bound);
return new RectF(bound);
}
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));
}
}
}
restoreCanvas(canvas, count);
markUpdateSeen();
}
@Override
protected Path getPath(Canvas canvas, Paint paint) {
Path path = new Path();
Path path = getPathFromSuper(canvas, paint);
String text = formatText();
if (text == null) {
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);
path.transform(mMatrix);
}
float shift = getShift(paint);
Matrix matrix = new Matrix();
matrix.setTranslate(shift, 0);
path.transform(matrix);
return path;
}
@Override
public int hitTest(Point point, View view, @Nullable Matrix matrix) {
Bitmap bitmap = Bitmap.createBitmap(
mCanvasWidth,
mCanvasHeight,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (matrix != null) {
canvas.concat(matrix);
}
canvas.concat(mMatrix);
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;
private Path getPathFromSuper(Canvas canvas, Paint paint) {
Path path = super.getPath(canvas, paint);
// reset offsetX and offsetY
mOffsetX = mOffsetY = 0;
return path;
}
public void setOffsetX(float x, boolean increase) {
if (increase) {
mOffsetX += x;
} else {
mOffsetX = x;
}
}
@Override
public int hitTest(Point point, View view) {
return this.hitTest(point, view, null);
public void setOffsetY(float y, boolean increase) {
if (increase) {
mOffsetY += y;
} else {
mOffsetY = y;
}
}
public float getOffsetX() {
return mOffsetX;
}
public float getOffsetY() {
return mOffsetY;
}
private float getShift(Paint paint) {
Rect rect = new Rect();
for (int i = getChildCount() - 1; i >= 0; i--) {
if (!(getChildAt(i) instanceof RNSVGVirtualNode)) {
continue;
}
RectF box = ((RNSVGSpanShadowNode) getChildAt(i)).getBox(paint);
if (rect.top > box.top) {
rect.top = (int)box.top;
}
if (rect.right < box.right) {
rect.right = (int)box.right;
}
if (rect.bottom < box.bottom) {
rect.bottom = (int)box.bottom;
}
if (rect.left > box.left) {
rect.left = (int)box.left;
}
}
float width = rect.width();
float shift;
switch (mTextAlignment) {
case TEXT_ALIGNMENT_RIGHT:
shift = width;
break;
case TEXT_ALIGNMENT_LEFT:
shift = 0;
break;
default:
shift = width / 2;
}
return shift;
}
}
@@ -39,6 +39,7 @@ public class RNSvgPackage implements ReactPackage {
RNSVGRenderableViewManager.createRNSVGViewBoxViewManager(),
RNSVGRenderableViewManager.createRNSVGLinearGradientManager(),
RNSVGRenderableViewManager.createRNSVGRadialGradientManager(),
RNSVGRenderableViewManager.createRNSVGSpanManager(),
new RNSVGSvgViewManager());
}