Add preserveAspectRatio prop for Image

Add preserveAspectRatio prop for Image.
Fix touch events for Image on Android.
Fix viewBox slice bug on Android.
This commit is contained in:
Horcrux
2016-08-13 14:03:08 +08:00
parent a4c0c60b7f
commit d1afb78da0
20 changed files with 251 additions and 42 deletions

View File

@@ -7,24 +7,48 @@ import Svg, {
Defs,
Circle,
ClipPath,
Rect,
Text
} from 'react-native-svg';
class ImageExample extends Component{
static title = 'Image';
static title = 'Draw Image with preserveAspectRatio prop';
render() {
return <Svg
height="100"
width="100"
style={{backgroundColor: 'red'}}
>
<Defs>
<ClipPath id="clip">
<Circle cx="50%" cy="50%" r="40%"/>
</ClipPath>
</Defs>
<Rect
x="5%"
y="5%"
width="50%"
height="90%"
/>
<Image
x="5%"
y="5%"
width="90%"
width="50%"
height="90%"
preserveAspectRatio="xMidYMid slice"
opacity="0.5"
href={require('../image.jpg')}
clipPath="url(#clip)"
/>
<Text
x="50"
y="50"
textAnchor="middle"
fontWeight="bold"
fontSize="16"
fill="blue"
>HOGWARTS</Text>
</Svg>;
}
}
@@ -43,6 +67,7 @@ class ClipImage extends Component{
</ClipPath>
</Defs>
<Image
onPress={() => alert('press on Image')}
x="5%"
y="5%"
width="90%"

View File

@@ -579,7 +579,7 @@ npm install
2. more Text features support (textPath, tref, tspan)
3. Pattern element
4. implement Animated elements
5. more Image features support
5. load Image from url
#### Thanks:

View File

@@ -12,10 +12,16 @@ package com.horcrux.svg;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.net.Uri;
import android.util.Log;
import com.facebook.common.executors.UiThreadImmediateExecutorService;
import com.facebook.common.logging.FLog;
import com.facebook.common.references.CloseableReference;
@@ -45,6 +51,9 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
private String mW;
private String mH;
private Uri mUri;
private float mImageRatio;
private String mAlign;
private int mMeetOrSlice;
private AtomicBoolean mLoading = new AtomicBoolean(false);
@ReactProp(name = "x")
@@ -81,12 +90,29 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
return;
}
mImageRatio = (float)src.getInt("width") / (float)src.getInt("height");
mUri = Uri.parse(uriString);
}
}
@ReactProp(name = "align")
public void setAlign(String align) {
mAlign = align;
markUpdated();
}
@ReactProp(name = "meetOrSlice")
public void setMeetOrSlice(int meetOrSlice) {
mMeetOrSlice = meetOrSlice;
markUpdated();
}
@Override
public void draw(final Canvas canvas, final Paint paint, final float opacity) {
mPath = new Path();
mPath.addRect(new RectF(getRect()), Path.Direction.CW);
if (!mLoading.get()) {
final ImageRequest request = ImageRequestBuilder.newBuilderWithSource(mUri).build();
@@ -140,14 +166,70 @@ public class RNSVGImageShadowNode extends RNSVGPathShadowNode {
private void doRender(@Nonnull final Canvas canvas, @Nonnull final Paint paint, @Nonnull final Bitmap bitmap, final float opacity) {
final int count = saveAndSetupCanvas(canvas);
clip(canvas, paint);
canvas.concat(mMatrix);
Paint alphaPaint = new Paint();
alphaPaint.setAlpha((int) (opacity * 255));
canvas.drawBitmap(bitmap, null, getRect(), alphaPaint);
// apply viewBox transform on Image render.
Rect rect = getRect();
float rectWidth = (float)rect.width();
float rectHeight = (float)rect.height();
float rectX = (float)rect.left;
float rectY = (float)rect.top;
float rectRatio = rectWidth / rectHeight;
RectF renderRect;
if (mImageRatio == rectRatio) {
renderRect = new RectF(rect);
} else if (mImageRatio < rectRatio) {
renderRect = new RectF(0, 0, (int)(rectHeight * mImageRatio), (int)rectHeight);
} else {
renderRect = new RectF(0, 0, (int)rectWidth, (int)(rectWidth / mImageRatio));
}
RNSVGViewBoxShadowNode viewBox = new RNSVGViewBoxShadowNode();
viewBox.setMinX("0");
viewBox.setMinY("0");
viewBox.setVbWidth(renderRect.width() / mScale + "");
viewBox.setVbHeight(renderRect.height() / mScale + "");
viewBox.setWidth(rectWidth / mScale);
viewBox.setHeight(rectHeight / mScale);
viewBox.setAlign(mAlign);
viewBox.setMeetOrSlice(mMeetOrSlice);
viewBox.setupDimensions(new Rect(0, 0, (int) rectWidth, (int) rectHeight));
Matrix transform = viewBox.getTransform();
transform.mapRect(renderRect);
Matrix translation = new Matrix();
translation.postTranslate(rectX, rectY);
translation.mapRect(renderRect);
Path clip = new Path();
Path clipPath = getClipPath(canvas, paint);
if (clipPath != null) {
// clip by the common area of clipPath and mPath
clip.setFillType(Path.FillType.INVERSE_EVEN_ODD);
Path inverseWindingPath = new Path();
inverseWindingPath.setFillType(Path.FillType.INVERSE_WINDING);
inverseWindingPath.addPath(mPath);
inverseWindingPath.addPath(clipPath);
Path evenOddPath = new Path();
evenOddPath.setFillType(Path.FillType.EVEN_ODD);
evenOddPath.addPath(mPath);
evenOddPath.addPath(clipPath);
canvas.clipPath(evenOddPath, Region.Op.DIFFERENCE);
canvas.clipPath(inverseWindingPath, Region.Op.DIFFERENCE);
} else {
canvas.clipPath(mPath, Region.Op.REPLACE);
}
canvas.drawBitmap(bitmap, null, renderRect, alphaPaint);
restoreCanvas(canvas, count);
markUpdateSeen();
}

View File

@@ -103,7 +103,7 @@ public class RNSVGRectShadowNode extends RNSVGPathShadowNode {
}
path.addRoundRect(new RectF(x, y, x + w, y + h), rx, ry, Path.Direction.CW);
} else {
path.addRect(x, y, x + w, y + h, Path.Direction.CW);
path.addRect(x, y, x + w, y + h, Path.Direction.CW);
}
return path;
}

View File

@@ -10,6 +10,7 @@
package com.horcrux.svg;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import com.facebook.react.bridge.ReadableArray;
@@ -85,8 +86,13 @@ public class RNSVGViewBoxShadowNode extends RNSVGGroupShadowNode {
@Override
public void draw(Canvas canvas, Paint paint, float opacity) {
// based on https://svgwg.org/svg2-draft/coords.html#ComputingAViewportsTransform
setupDimensions(canvas);
mMatrix = getTransform();
super.draw(canvas, paint, opacity);
}
public Matrix getTransform() {
// based on https://svgwg.org/svg2-draft/coords.html#ComputingAViewportsTransform
// Let vb-x, vb-y, vb-width, vb-height be the min-x, min-y, width and height values of the viewBox attribute respectively.
float vbX = PropHelper.fromPercentageToFloat(mMinX, mCanvasWidth, 0, mScale);
@@ -134,7 +140,7 @@ public class RNSVGViewBoxShadowNode extends RNSVGGroupShadowNode {
if (!mAlign.equals("none") && mMeetOrSlice == MOS_MEET) {
scaleX = scaleY = Math.min(scaleX, scaleY);
} else if (!mAlign.equals("none") && mMeetOrSlice == MOS_SLICE) {
scaleX = scaleY = Math.min(scaleX, scaleY);
scaleX = scaleY = Math.max(scaleX, scaleY);
}
// If align contains 'xMid', minus (e-width / scale-x - vb-width) / 2 from transform-x.
@@ -161,10 +167,10 @@ public class RNSVGViewBoxShadowNode extends RNSVGGroupShadowNode {
// The transform applied to content contained by the element is given by
// translate(translate-x, translate-y) scale(scale-x, scale-y).
mMatrix.reset();
mMatrix.postTranslate(-translateX * (mFromSymbol ? scaleX : 1), -translateY * (mFromSymbol ? scaleY : 1));
mMatrix.postScale(scaleX, scaleY);
super.draw(canvas, paint, opacity);
Matrix transform = new Matrix();
transform.postTranslate(-translateX * (mFromSymbol ? scaleX : 1), -translateY * (mFromSymbol ? scaleY : 1));
transform.postScale(scaleX, scaleY);
return transform;
}
@Override

View File

@@ -236,16 +236,21 @@ public abstract class RNSVGVirtualNode extends LayoutShadowNode {
}
}
protected void clip(Canvas canvas, Paint paint) {
protected @Nullable Path getClipPath(Canvas canvas, Paint paint) {
Path clip = mClipPath;
if (clip == null && mClipPathRef != null) {
RNSVGVirtualNode node = getSvgShadowNode().getDefinedClipPath(mClipPathRef);
clip = node.getPath(canvas, paint);
}
return clip;
}
protected void clip(Canvas canvas, Paint paint) {
Path clip = getClipPath(canvas, paint);
if (clip != null) {
canvas.clipPath(clip, Region.Op.REPLACE);
canvas.saveLayer(0f, 0f, 0f, 0f, paint, Canvas.CLIP_SAVE_FLAG);
}
}
@@ -286,6 +291,13 @@ public abstract class RNSVGVirtualNode extends LayoutShadowNode {
mCanvasHeight = canvas.getHeight();
}
protected void setupDimensions(Rect rect) {
mCanvasX = rect.left;
mCanvasY = rect.top;
mCanvasWidth = rect.width();
mCanvasHeight = rect.height();
}
protected void saveDefinition() {
if (mName != null) {
getSvgShadowNode().defineTemplate(this, mName);

View File

@@ -4,7 +4,8 @@ import {ImageAttributes} from '../lib/attributes';
import {numberProp, touchableProps, responderProps} from '../lib/props';
import Shape from './Shape';
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
import {meetOrSliceTypes, alignEnum} from './ViewBox';
const spacesRegExp = /\s+/;
class Image extends Shape {
static displayName = 'Image';
@@ -15,15 +16,16 @@ class Image extends Shape {
y: numberProp,
width: numberProp.isRequired,
height: numberProp.isRequired,
href: PropTypes.number.isRequired
//preserveAspectRatio: PropTypes.string
href: PropTypes.number.isRequired,
preserveAspectRatio: PropTypes.string
};
static defaultProps = {
x: 0,
y: 0,
width: 0,
height: 0
height: 0,
preserveAspectRatio: 'xMidYMid meet'
};
setNativeProps = (...args) => {
@@ -32,6 +34,10 @@ class Image extends Shape {
render() {
let {props} = this;
let modes = props.preserveAspectRatio.trim().split(spacesRegExp);
let meetOrSlice = meetOrSliceTypes[modes[1]] || 0;
let align = alignEnum[modes[0]] || 'xMidYMid';
return <RNSVGImage
ref={ele => {this.root = ele;}}
{...this.extractProps({...props, x: null, y: null}, {responder: true, transform: true})}
@@ -39,6 +45,8 @@ class Image extends Shape {
y={props.y.toString()}
width={props.width.toString()}
height={props.height.toString()}
meetOrSlice={meetOrSlice}
align={align}
src={resolveAssetSource(props.href)}
/>;
}

View File

@@ -73,3 +73,8 @@ const RNSVGViewBox = createReactNativeComponentClass({
export default ViewBox;
export {
meetOrSliceTypes,
alignEnum
}

View File

@@ -9,12 +9,16 @@
#import <Foundation/Foundation.h>
#import "RNSVGRenderable.h"
#import "RNSVGVBMOS.h"
@interface RNSVGImage : RNSVGRenderable
@property (nonatomic, assign) id src;
@property (nonatomic, strong) NSString* x;
@property (nonatomic, strong) NSString* y;
@property (nonatomic, strong) NSString* width;
@property (nonatomic, strong) NSString* height;
@property (nonatomic, strong) NSString *align;
@property (nonatomic, assign) RNSVGVBMOS meetOrSlice;
@end

View File

@@ -7,12 +7,15 @@
*/
#import "RNSVGImage.h"
#import "RCTImageSource.h"
#import "RCTConvert+RNSVG.h"
#import "RCTLog.h"
#import "RNSVGViewBox.h"
@implementation RNSVGImage
{
CGImageRef image;
CGImageRef _image;
CGFloat _imageRatio;
}
- (void)setSrc:(id)src
@@ -21,8 +24,10 @@
return;
}
_src = src;
CGImageRelease(image);
image = CGImageRetain([RCTConvert CGImage:src]);
CGImageRelease(_image);
RCTImageSource *source = [RCTConvert RCTImageSource:src];
_imageRatio = source.size.width / source.size.height;
_image = CGImageRetain([RCTConvert CGImage:src]);
[self invalidate];
}
@@ -62,29 +67,80 @@
_height = height;
}
- (void)setAlign:(NSString *)align
{
if (align == _align) {
return;
}
[self invalidate];
_align = align;
}
- (void)setMeetOrSlice:(RNSVGVBMOS)meetOrSlice
{
if (meetOrSlice == _meetOrSlice) {
return;
}
[self invalidate];
_meetOrSlice = meetOrSlice;
}
- (void)dealloc
{
CGImageRelease(image);
CGImageRelease(_image);
}
- (void)renderLayerTo:(CGContextRef)context
{
CGRect rect = [self getRect:context];
// add hit area
self.hitArea = CGPathCreateWithRect(rect, nil);
self.hitArea = CFAutorelease(CGPathCreateWithRect(rect, nil));
[self clip:context];
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0, rect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, rect, image);
CGContextTranslateCTM(context, 0, rect.size.height + 2 * rect.origin.y);
CGContextScaleCTM(context, 1, -1);
// apply viewBox transform on Image render.
CGFloat imageRatio = _imageRatio;
CGFloat rectWidth = rect.size.width;
CGFloat rectHeight = rect.size.height;
CGFloat rectX = rect.origin.x;
CGFloat rectY = rect.origin.y;
CGFloat rectRatio = rectWidth / rectHeight;
CGRect renderRect;
if (imageRatio == rectRatio) {
renderRect = rect;
} else if (imageRatio < rectRatio) {
renderRect = CGRectMake(0, 0, rectHeight * imageRatio, rectHeight);
} else {
renderRect = CGRectMake(0, 0, rectWidth, rectWidth / imageRatio);
}
RNSVGViewBox *viewBox = [[RNSVGViewBox alloc] init];
viewBox.minX = viewBox.minY = @"0";
viewBox.vbWidth = [NSString stringWithFormat:@"%f", renderRect.size.width];
viewBox.vbHeight = [NSString stringWithFormat:@"%f", renderRect.size.height];
viewBox.width = [NSString stringWithFormat:@"%f", rectWidth];
viewBox.height = [NSString stringWithFormat:@"%f", rectHeight];
viewBox.align = self.align;
viewBox.meetOrSlice = self.meetOrSlice;
[viewBox setBoundingBox:CGRectMake(0, 0, rectWidth, rectHeight)];
CGAffineTransform transform = [viewBox getTransform];
renderRect = CGRectApplyAffineTransform(renderRect, transform);
renderRect = CGRectApplyAffineTransform(renderRect, CGAffineTransformMakeTranslation(rectX, rectY));
CGContextClipToRect(context, rect);
CGContextDrawImage(context, renderRect, _image);
CGContextRestoreGState(context);
}
- (CGRect)getRect:(CGContextRef)context
{
[self setBoundingBox:context];
[self setBoundingBox:CGContextGetClipBoundingBox(context)];
CGFloat x = [self getWidthRelatedValue:self.x];
CGFloat y = [self getHeightRelatedValue:self.y];
CGFloat width = [self getWidthRelatedValue:self.width];

View File

@@ -29,7 +29,7 @@
@property (nonatomic, assign) CGPathRef hitArea;
@property (nonatomic, copy) NSArray<NSString *> *propList;
- (void)setBoundingBox:(CGContextRef)context;
- (void)setBoundingBox:(CGRect)boundingBox;
- (CGFloat)getWidthRelatedValue:(NSString *)string;
- (CGFloat)getHeightRelatedValue:(NSString *)string;
- (CGFloat)getContextWidth;

View File

@@ -205,11 +205,11 @@
}
}
- (void)setBoundingBox:(CGContextRef)context
- (void)setBoundingBox:(CGRect)boundingBox
{
_boundingBox = CGContextGetClipBoundingBox(context);
_widthConverter = [[RNSVGPercentageConverter alloc] initWithRelativeAndOffset:CGRectGetWidth(_boundingBox) offset:0];
_heightConverter = [[RNSVGPercentageConverter alloc] initWithRelativeAndOffset:CGRectGetHeight(_boundingBox) offset:0];
_boundingBox = boundingBox;
_widthConverter = [[RNSVGPercentageConverter alloc] initWithRelativeAndOffset:boundingBox.size.width offset:0];
_heightConverter = [[RNSVGPercentageConverter alloc] initWithRelativeAndOffset:boundingBox.size.height offset:0];
}
- (CGFloat)getWidthRelatedValue:(NSString *)string

View File

@@ -20,4 +20,6 @@
@property (nonatomic, strong) NSString *width;
@property (nonatomic, strong) NSString *height;
- (CGAffineTransform)getTransform;
@end

View File

@@ -70,9 +70,15 @@
}
- (void)renderTo:(CGContextRef)context
{
[self setBoundingBox:CGContextGetClipBoundingBox(context)];
self.matrix = [self getTransform];
[super renderTo:context];
}
- (CGAffineTransform)getTransform
{
// based on https://svgwg.org/svg2-draft/coords.html#ComputingAViewportsTransform
[self setBoundingBox:context];
// Let vb-x, vb-y, vb-width, vb-height be the min-x, min-y, width and height values of the viewBox attribute respectively.
CGFloat vbX = [self getWidthRelatedValue:self.minX];
@@ -150,9 +156,8 @@
}
}
self.matrix = CGAffineTransformMakeScale(scaleX, scaleY);
self.matrix = CGAffineTransformTranslate(self.matrix, -translateX * (_fromSymbol ? scaleX : 1), -translateY * (_fromSymbol ? scaleY : 1));
[super renderTo:context];
CGAffineTransform transform = CGAffineTransformMakeScale(scaleX, scaleY);
return CGAffineTransformTranslate(transform, -translateX * (_fromSymbol ? scaleX : 1), -translateY * (_fromSymbol ? scaleY : 1));
}
- (void)mergeProperties:(__kindof RNSVGNode *)target mergeList:(NSArray<NSString *> *)mergeList

View File

@@ -40,7 +40,7 @@
- (CGPathRef)getPath:(CGContextRef)context
{
[self setBoundingBox:context];
[self setBoundingBox:CGContextGetClipBoundingBox(context)];
CGMutablePathRef path = CGPathCreateMutable();
RNSVGPercentageConverter* convert = [[RNSVGPercentageConverter alloc] init];
CGFloat cx = [self getWidthRelatedValue:self.cx];

View File

@@ -49,7 +49,7 @@
- (CGPathRef)getPath:(CGContextRef)context
{
[self setBoundingBox:context];
[self setBoundingBox:CGContextGetClipBoundingBox(context)];
CGMutablePathRef path = CGPathCreateMutable();
CGFloat cx = [self getWidthRelatedValue:self.cx];
CGFloat cy = [self getHeightRelatedValue:self.cy];

View File

@@ -49,7 +49,7 @@
- (CGPathRef)getPath:(CGContextRef)context
{
[self setBoundingBox:context];
[self setBoundingBox:CGContextGetClipBoundingBox(context)];
CGMutablePathRef path = CGPathCreateMutable();
CGFloat x1 = [self getWidthRelatedValue:self.x1];
CGFloat y1 = [self getHeightRelatedValue:self.y1];

View File

@@ -67,7 +67,7 @@
- (CGPathRef)getPath:(CGContextRef)context
{
[self setBoundingBox:context];
[self setBoundingBox:CGContextGetClipBoundingBox(context)];
CGMutablePathRef path = CGPathCreateMutable();
CGFloat x = [self getWidthRelatedValue:self.x];
CGFloat y = [self getHeightRelatedValue:self.y];

View File

@@ -25,5 +25,7 @@ RCT_EXPORT_VIEW_PROPERTY(y, NSString)
RCT_EXPORT_VIEW_PROPERTY(width, NSString)
RCT_EXPORT_VIEW_PROPERTY(height, NSString)
RCT_EXPORT_VIEW_PROPERTY(src, id)
RCT_EXPORT_VIEW_PROPERTY(align, NSString)
RCT_EXPORT_VIEW_PROPERTY(meetOrSlice, RNSVGVBMOS)
@end

View File

@@ -160,7 +160,9 @@ const ImageAttributes = merge({
y: true,
width: true,
height: true,
src: true
src: true,
align: true,
meetOrSlice: true
}, RenderableAttributes);
const LineAttributes = merge({