fix: animated transform last frame (#2553)

# Summary

When using the Animated API for animations, it sends the last frame as
JavaScript-parsed (matrix) updates in addition to native (transform)
updates. ~~As a result, we need to ignore one of them.~~ I believe
there's no need to differentiate between native and JavaScript
updates—we can simply save both to the same value (mMatrix). By doing
so, we can avoid duplicating the transforms.

| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/868cc778-4b88-4473-85b5-9665b4b241aa">
| <video
src="https://github.com/user-attachments/assets/c6d17b7b-7c9a-47c3-8286-2d9b5720f261">
|




## Test Plan

```jsx
import React, {useEffect} from 'react';
import {Animated, useAnimatedValue, View} from 'react-native';
import {Rect, Svg} from 'react-native-svg';

const AnimatedRect = Animated.createAnimatedComponent(Rect);
function AnimatedJumpIssue() {
  const animatedValue = useAnimatedValue(100);
  return (
    <>
      <View style={{borderColor: 'black', borderWidth: 1}}>
        <Svg height="100" width="400">
          <AnimatedRect
            x="0"
            y="0"
            width="100"
            height="100"
            fill="black"
            transform={[{translateX: animatedValue}]}
          />
        </Svg>
      </View>
      <Button
        title="Press me"
        onPress={() => {
          Animated.timing(animatedValue, {
            toValue: 200,
            duration: 3000,
            useNativeDriver: true,
          }).start();
        }}
      />
    </>
  );
}
```

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| Android |          |
| iOS |          |
| macOS |          |
This commit is contained in:
Jakub Grzywacz
2024-11-28 12:44:55 +01:00
committed by GitHub
parent d125be1819
commit c617dec1c5
15 changed files with 24 additions and 46 deletions

View File

@@ -58,9 +58,6 @@ class GroupView extends RenderableView {
if (mMatrix != null) { if (mMatrix != null) {
mMatrix.mapRect(clipBounds); mMatrix.mapRect(clipBounds);
} }
if (mTransform != null) {
mTransform.mapRect(clipBounds);
}
mGlyphContext = new GlyphContext(mScale, clipBounds.width(), clipBounds.height()); mGlyphContext = new GlyphContext(mScale, clipBounds.width(), clipBounds.height());
} }
@@ -258,7 +255,7 @@ class GroupView extends RenderableView {
@Override @Override
int hitTest(final float[] src) { int hitTest(final float[] src) {
if (!mInvertible || !mTransformInvertible) { if (!mInvertible) {
return -1; return -1;
} }

View File

@@ -677,7 +677,7 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
@Override @Override
int hitTest(final float[] src) { int hitTest(final float[] src) {
if (mPath == null || !mInvertible || !mTransformInvertible) { if (mPath == null || !mInvertible) {
return -1; return -1;
} }

View File

@@ -518,8 +518,8 @@ class VirtualViewManager<V extends VirtualView> extends ViewGroupManager<Virtual
} }
Matrix m = node.getMatrix(); Matrix m = node.getMatrix();
node.mTransform = m; node.mMatrix = m;
node.mTransformInvertible = m.invert(node.mInvTransform); node.mInvertible = m.invert(node.mInvMatrix);
} }
@ReactProp(name = "transform") @ReactProp(name = "transform")

View File

@@ -1212,7 +1212,7 @@ class TSpanView extends TextView {
if (mContent == null) { if (mContent == null) {
return super.hitTest(src); return super.hitTest(src);
} }
if (mPath == null || !mInvertible || !mTransformInvertible) { if (mPath == null || !mInvertible) {
return -1; return -1;
} }

View File

@@ -101,7 +101,7 @@ class UseView extends RenderableView {
@Override @Override
int hitTest(float[] src) { int hitTest(float[] src) {
if (!mInvertible || !mTransformInvertible) { if (!mInvertible) {
return -1; return -1;
} }

View File

@@ -54,13 +54,11 @@ public abstract class VirtualView extends ReactViewGroup {
float mOpacity = 1f; float mOpacity = 1f;
Matrix mCTM = new Matrix(); Matrix mCTM = new Matrix();
Matrix mMatrix = new Matrix(); Matrix mMatrix = new Matrix();
Matrix mTransform = new Matrix();
Matrix mInvCTM = new Matrix(); Matrix mInvCTM = new Matrix();
Matrix mInvMatrix = new Matrix(); Matrix mInvMatrix = new Matrix();
final Matrix mInvTransform = new Matrix(); final Matrix mInvTransform = new Matrix();
boolean mInvertible = true; boolean mInvertible = true;
boolean mCTMInvertible = true; boolean mCTMInvertible = true;
boolean mTransformInvertible = true;
private RectF mClientRect; private RectF mClientRect;
int mClipRule; int mClipRule;
@@ -249,7 +247,7 @@ public abstract class VirtualView extends ReactViewGroup {
*/ */
int saveAndSetupCanvas(Canvas canvas, Matrix ctm) { int saveAndSetupCanvas(Canvas canvas, Matrix ctm) {
int count = canvas.save(); int count = canvas.save();
mCTM.setConcat(mMatrix, mTransform); mCTM.set(mMatrix);
canvas.concat(mCTM); canvas.concat(mCTM);
mCTM.preConcat(ctm); mCTM.preConcat(ctm);
mCTMInvertible = mCTM.invert(mInvCTM); mCTMInvertible = mCTM.invert(mInvCTM);
@@ -360,7 +358,6 @@ public abstract class VirtualView extends ReactViewGroup {
? mClipNode.getPath(canvas, paint) ? mClipNode.getPath(canvas, paint)
: mClipNode.getPath(canvas, paint, Region.Op.UNION); : mClipNode.getPath(canvas, paint, Region.Op.UNION);
clipPath.transform(mClipNode.mMatrix); clipPath.transform(mClipNode.mMatrix);
clipPath.transform(mClipNode.mTransform);
switch (mClipRule) { switch (mClipRule) {
case CLIP_RULE_EVENODD: case CLIP_RULE_EVENODD:
clipPath.setFillType(Path.FillType.EVEN_ODD); clipPath.setFillType(Path.FillType.EVEN_ODD);

View File

@@ -160,9 +160,8 @@ using namespace facebook::react;
self.ctm = svgToClientTransform; self.ctm = svgToClientTransform;
self.screenCTM = current; self.screenCTM = current;
CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms);
CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
CGPoint center = CGPointApplyAffineTransform(mid, transform); CGPoint center = CGPointApplyAffineTransform(mid, self.matrix);
self.bounds = bounds; self.bounds = bounds;
if (!isnan(center.x) && !isnan(center.y)) { if (!isnan(center.x) && !isnan(center.y)) {

View File

@@ -139,9 +139,8 @@ using namespace facebook::react;
self.ctm = svgToClientTransform; self.ctm = svgToClientTransform;
self.screenCTM = current; self.screenCTM = current;
CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms);
CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
CGPoint center = CGPointApplyAffineTransform(mid, transform); CGPoint center = CGPointApplyAffineTransform(mid, self.matrix);
self.bounds = bounds; self.bounds = bounds;
if (!isnan(center.x) && !isnan(center.y)) { if (!isnan(center.x) && !isnan(center.y)) {
@@ -163,7 +162,6 @@ using namespace facebook::react;
} }
#endif // macOS] #endif // macOS]
clipBounds = CGRectApplyAffineTransform(clipBounds, self.matrix); clipBounds = CGRectApplyAffineTransform(clipBounds, self.matrix);
clipBounds = CGRectApplyAffineTransform(clipBounds, self.transforms);
CGFloat width = CGRectGetWidth(clipBounds); CGFloat width = CGRectGetWidth(clipBounds);
CGFloat height = CGRectGetHeight(clipBounds); CGFloat height = CGRectGetHeight(clipBounds);
@@ -200,7 +198,7 @@ using namespace facebook::react;
CGMutablePathRef __block path = CGPathCreateMutable(); CGMutablePathRef __block path = CGPathCreateMutable();
[self traverseSubviews:^(RNSVGNode *node) { [self traverseSubviews:^(RNSVGNode *node) {
if ([node isKindOfClass:[RNSVGNode class]] && ![node isKindOfClass:[RNSVGMask class]]) { if ([node isKindOfClass:[RNSVGNode class]] && ![node isKindOfClass:[RNSVGMask class]]) {
CGAffineTransform transform = CGAffineTransformConcat(node.matrix, node.transforms); CGAffineTransform transform = node.matrix;
CGPathAddPath(path, &transform, [node getPath:context]); CGPathAddPath(path, &transform, [node getPath:context]);
CGPathAddPath(path, &transform, [node markerPath]); CGPathAddPath(path, &transform, [node markerPath]);
node.dirty = false; node.dirty = false;
@@ -216,7 +214,6 @@ using namespace facebook::react;
- (RNSVGPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event - (RNSVGPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{ {
CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix); CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix);
transformed = CGPointApplyAffineTransform(transformed, self.invTransform);
if (!CGRectContainsPoint(self.pathBounds, transformed)) { if (!CGRectContainsPoint(self.pathBounds, transformed)) {
return nil; return nil;

View File

@@ -356,9 +356,8 @@ using namespace facebook::react;
self.ctm = svgToClientTransform; self.ctm = svgToClientTransform;
self.screenCTM = current; self.screenCTM = current;
CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms);
CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
CGPoint center = CGPointApplyAffineTransform(mid, transform); CGPoint center = CGPointApplyAffineTransform(mid, self.matrix);
self.bounds = bounds; self.bounds = bounds;
if (!isnan(center.x) && !isnan(center.y)) { if (!isnan(center.x) && !isnan(center.y)) {

View File

@@ -176,9 +176,8 @@ using namespace facebook::react;
self.ctm = svgToClientTransform; self.ctm = svgToClientTransform;
self.screenCTM = current; self.screenCTM = current;
CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms);
CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
CGPoint center = CGPointApplyAffineTransform(mid, transform); CGPoint center = CGPointApplyAffineTransform(mid, self.matrix);
self.bounds = bounds; self.bounds = bounds;
if (!isnan(center.x) && !isnan(center.y)) { if (!isnan(center.x) && !isnan(center.y)) {
@@ -190,7 +189,6 @@ using namespace facebook::react;
- (RNSVGPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event - (RNSVGPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{ {
CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix); CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix);
transformed = CGPointApplyAffineTransform(transformed, self.invTransform);
RNSVGNode const *definedTemplate = [self.svgView getDefinedTemplate:self.href]; RNSVGNode const *definedTemplate = [self.svgView getDefinedTemplate:self.href];
if (event) { if (event) {
self.active = NO; self.active = NO;

View File

@@ -48,9 +48,7 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE;
@property (nonatomic, assign) CGAffineTransform ctm; @property (nonatomic, assign) CGAffineTransform ctm;
@property (nonatomic, assign) CGAffineTransform screenCTM; @property (nonatomic, assign) CGAffineTransform screenCTM;
@property (nonatomic, assign) CGAffineTransform matrix; @property (nonatomic, assign) CGAffineTransform matrix;
@property (nonatomic, assign) CGAffineTransform transforms;
@property (nonatomic, assign) CGAffineTransform invmatrix; @property (nonatomic, assign) CGAffineTransform invmatrix;
@property (nonatomic, assign) CGAffineTransform invTransform;
@property (nonatomic, assign) BOOL active; @property (nonatomic, assign) BOOL active;
@property (nonatomic, assign) BOOL dirty; @property (nonatomic, assign) BOOL dirty;
@property (nonatomic, assign) BOOL merging; @property (nonatomic, assign) BOOL merging;
@@ -148,4 +146,6 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE;
- (CGFloat)getCanvasHeight; - (CGFloat)getCanvasHeight;
- (void)setTransforms:(CGAffineTransform)transforms;
@end @end

View File

@@ -38,8 +38,6 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12;
self.opaque = false; self.opaque = false;
#endif #endif
self.matrix = CGAffineTransformIdentity; self.matrix = CGAffineTransformIdentity;
self.transforms = CGAffineTransformIdentity;
self.invTransform = CGAffineTransformIdentity;
_merging = false; _merging = false;
_dirty = false; _dirty = false;
} }
@@ -250,11 +248,11 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12;
- (void)setTransforms:(CGAffineTransform)transforms - (void)setTransforms:(CGAffineTransform)transforms
{ {
if (CGAffineTransformEqualToTransform(transforms, _transforms)) { if (CGAffineTransformEqualToTransform(transforms, _matrix)) {
return; return;
} }
_transforms = transforms; _matrix = transforms;
[self invalidate]; [self invalidate];
} }
@@ -377,7 +375,7 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12;
if (_cachedClipPath) { if (_cachedClipPath) {
CGPathRelease(_cachedClipPath); CGPathRelease(_cachedClipPath);
} }
CGAffineTransform transform = CGAffineTransformConcat(_clipNode.matrix, _clipNode.transforms); CGAffineTransform transform = _clipNode.matrix;
_cachedClipPath = CGPathCreateCopyByTransformingPath([_clipNode getPath:context], &transform); _cachedClipPath = CGPathCreateCopyByTransformingPath([_clipNode getPath:context], &transform);
} }
@@ -630,8 +628,6 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12;
self.opaque = false; self.opaque = false;
#endif #endif
self.matrix = CGAffineTransformIdentity; self.matrix = CGAffineTransformIdentity;
self.transforms = CGAffineTransformIdentity;
self.invTransform = CGAffineTransformIdentity;
_merging = false; _merging = false;
_dirty = false; _dirty = false;
@@ -652,9 +648,7 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12;
_ctm = CGAffineTransformIdentity; _ctm = CGAffineTransformIdentity;
_screenCTM = CGAffineTransformIdentity; _screenCTM = CGAffineTransformIdentity;
_matrix = CGAffineTransformIdentity; _matrix = CGAffineTransformIdentity;
_transforms = CGAffineTransformIdentity;
_invmatrix = CGAffineTransformIdentity; _invmatrix = CGAffineTransformIdentity;
_invTransform = CGAffineTransformIdentity;
_active = NO; _active = NO;
_skip = NO; _skip = NO;
if (_markerPath) { if (_markerPath) {

View File

@@ -255,7 +255,6 @@ UInt32 saturate(CGFloat value)
// This needs to be painted on a layer before being composited. // This needs to be painted on a layer before being composited.
CGContextSaveGState(context); CGContextSaveGState(context);
CGContextConcatCTM(context, self.matrix); CGContextConcatCTM(context, self.matrix);
CGContextConcatCTM(context, self.transforms);
CGContextSetAlpha(context, self.opacity); CGContextSetAlpha(context, self.opacity);
[self beginTransparencyLayer:context]; [self beginTransparencyLayer:context];
@@ -530,8 +529,7 @@ UInt32 saturate(CGFloat value)
} }
CGAffineTransform vbmatrix = self.svgView.getViewBoxTransform; CGAffineTransform vbmatrix = self.svgView.getViewBoxTransform;
CGAffineTransform transform = CGAffineTransformConcat(self.matrix, self.transforms); CGAffineTransform matrix = CGAffineTransformConcat(self.matrix, vbmatrix);
CGAffineTransform matrix = CGAffineTransformConcat(transform, vbmatrix);
CGRect bounds = CGRectMake(0, 0, CGRectGetWidth(clientRect), CGRectGetHeight(clientRect)); CGRect bounds = CGRectMake(0, 0, CGRectGetWidth(clientRect), CGRectGetHeight(clientRect));
CGPoint mid = CGPointMake(CGRectGetMidX(pathBounds), CGRectGetMidY(pathBounds)); CGPoint mid = CGPointMake(CGRectGetMidX(pathBounds), CGRectGetMidY(pathBounds));
@@ -687,7 +685,6 @@ UInt32 saturate(CGFloat value)
} }
CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix); CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix);
transformed = CGPointApplyAffineTransform(transformed, self.invTransform);
if (!CGRectContainsPoint(self.pathBounds, transformed) && !CGRectContainsPoint(self.markerBounds, transformed)) { if (!CGRectContainsPoint(self.pathBounds, transformed) && !CGRectContainsPoint(self.markerBounds, transformed)) {
return nil; return nil;

View File

@@ -71,11 +71,12 @@ void setCommonNodeProps(const T &nodeProps, RNSVGNode *node)
nodeProps.matrix.at(4), nodeProps.matrix.at(4),
nodeProps.matrix.at(5)); nodeProps.matrix.at(5));
} }
if (nodeProps.transform.operations.size() > 0) {
auto newTransform = nodeProps.resolveTransform(MinimalLayoutMetrics); auto newTransform = nodeProps.resolveTransform(MinimalLayoutMetrics);
CATransform3D transform3d = RCTCATransform3DFromTransformMatrix(newTransform); CATransform3D transform3d = RCTCATransform3DFromTransformMatrix(newTransform);
CGAffineTransform transform = CATransform3DGetAffineTransform(transform3d); CGAffineTransform transform = CATransform3DGetAffineTransform(transform3d);
node.invTransform = CGAffineTransformInvert(transform);
node.transforms = transform; node.transforms = transform;
}
node.mask = RCTNSStringFromStringNilIfEmpty(nodeProps.mask); node.mask = RCTNSStringFromStringNilIfEmpty(nodeProps.mask);
node.markerStart = RCTNSStringFromStringNilIfEmpty(nodeProps.markerStart); node.markerStart = RCTNSStringFromStringNilIfEmpty(nodeProps.markerStart);
node.markerMid = RCTNSStringFromStringNilIfEmpty(nodeProps.markerMid); node.markerMid = RCTNSStringFromStringNilIfEmpty(nodeProps.markerMid);

View File

@@ -32,7 +32,6 @@ RCT_CUSTOM_VIEW_PROPERTY(transform, CATransform3D, RNSVGNode)
{ {
CATransform3D transform3d = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform; CATransform3D transform3d = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform;
CGAffineTransform transform = CATransform3DGetAffineTransform(transform3d); CGAffineTransform transform = CATransform3DGetAffineTransform(transform3d);
view.invTransform = CGAffineTransformInvert(transform);
view.transforms = transform; view.transforms = transform;
} }
RCT_EXPORT_VIEW_PROPERTY(mask, NSString) RCT_EXPORT_VIEW_PROPERTY(mask, NSString)