Files
react-native-svg/apple/Elements/RNSVGImage.mm
Jakub Grzywacz c617dec1c5 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 |          |
2024-11-28 12:44:55 +01:00

398 lines
11 KiB
Plaintext

/**
* 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.
*/
#import "RNSVGImage.h"
#import "RCTConvert+RNSVG.h"
#if __has_include(<React/RCTImageLoader.h>)
#import <React/RCTImageLoader.h>
#else
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTImageShadowView.h>
#import <React/RCTImageURLLoader.h>
#import <React/RCTImageView.h>
#endif // RCT_NEW_ARCH_ENABLED
#import <React/RCTBridge.h>
#import <React/RCTLog.h>
#import "RNSVGViewBox.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <React/RCTImageResponseObserverProxy.h>
#import <React/RCTImageSource.h>
#import <react/renderer/components/rnsvg/ComponentDescriptors.h>
#import <rnsvg/RNSVGImageComponentDescriptor.h>
#import "RNSVGFabricConversions.h"
using namespace facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSVGImage {
CGImageRef _image;
CGSize _imageSize;
RCTImageLoaderCancellationBlock _reloadImageCancellationBlock;
#ifdef RCT_NEW_ARCH_ENABLED
RNSVGImageShadowNode::ConcreteState::Shared _state;
RCTImageResponseObserverProxy _imageResponseObserverProxy;
#endif // RCT_NEW_ARCH_ENABLED
}
#ifdef RCT_NEW_ARCH_ENABLED
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNSVGImageProps>();
_props = defaultProps;
_imageResponseObserverProxy = RCTImageResponseObserverProxy(self);
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNSVGImageComponentDescriptor>();
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &newProps = static_cast<const RNSVGImageProps &>(*props);
id x = RNSVGConvertFollyDynamicToId(newProps.x);
if (x != nil) {
self.x = [RCTConvert RNSVGLength:x];
}
id y = RNSVGConvertFollyDynamicToId(newProps.y);
if (y != nil) {
self.y = [RCTConvert RNSVGLength:y];
}
id height = RNSVGConvertFollyDynamicToId(newProps.height);
if (height != nil) {
self.imageheight = [RCTConvert RNSVGLength:height];
}
id width = RNSVGConvertFollyDynamicToId(newProps.width);
if (width != nil) {
self.imagewidth = [RCTConvert RNSVGLength:width];
}
self.align = RCTNSStringFromStringNilIfEmpty(newProps.align);
self.meetOrSlice = intToRNSVGVBMOS(newProps.meetOrSlice);
setCommonRenderableProps(newProps, self);
_props = std::static_pointer_cast<RNSVGImageProps const>(props);
}
- (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState
{
RCTAssert(state, @"`state` must not be null.");
RCTAssert(
std::dynamic_pointer_cast<RNSVGImageShadowNode::ConcreteState const>(state),
@"`state` must be a pointer to `RNSVGImageShadowNode::ConcreteState`.");
auto oldImageState = std::static_pointer_cast<RNSVGImageShadowNode::ConcreteState const>(_state);
auto newImageState = std::static_pointer_cast<RNSVGImageShadowNode::ConcreteState const>(state);
[self _setStateAndResubscribeImageResponseObserver:newImageState];
}
- (void)_setStateAndResubscribeImageResponseObserver:(RNSVGImageShadowNode::ConcreteState::Shared const &)state
{
if (_state) {
auto &observerCoordinator = _state->getData().getImageRequest().getObserverCoordinator();
observerCoordinator.removeObserver(_imageResponseObserverProxy);
}
_state = state;
if (_state) {
auto &observerCoordinator = _state->getData().getImageRequest().getObserverCoordinator();
observerCoordinator.addObserver(_imageResponseObserverProxy);
}
}
#pragma mark - RCTImageResponseDelegate
- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(void const *)observer
{
if (!_eventEmitter || !_state) {
// Notifications are delivered asynchronously and might arrive after the view is already recycled.
// In the future, we should incorporate an `EventEmitter` into a separate object owned by `ImageRequest` or `State`.
// See for more info: T46311063.
return;
}
auto imageSource = _state->getData().getImageSource();
imageSource.size = {image.size.width, image.size.height};
if (_eventEmitter != nullptr) {
static_cast<const RNSVGImageEventEmitter &>(*_eventEmitter)
.onLoad(
{.source = {
.width = imageSource.size.width * imageSource.scale,
.height = imageSource.size.height * imageSource.scale,
.uri = imageSource.uri,
}});
}
dispatch_async(dispatch_get_main_queue(), ^{
self->_image = CGImageRetain(image.CGImage);
self->_imageSize = CGSizeMake(CGImageGetWidth(self->_image), CGImageGetHeight(self->_image));
[self invalidate];
});
}
- (void)didReceiveFailure:(nonnull NSError *)error fromObserver:(nonnull const void *)observer
{
if (_image) {
CGImageRelease(_image);
}
_image = nil;
}
- (void)didReceiveProgress:(float)progress
loaded:(int64_t)loaded
total:(int64_t)total
fromObserver:(nonnull const void *)observer
{
}
#pragma mark - RCTImageResponseDelegate - < RN 0.75
- (void)didReceiveProgress:(float)progress fromObserver:(void const *)observer
{
}
- (void)didReceiveFailureFromObserver:(void const *)observer
{
if (_image) {
CGImageRelease(_image);
}
_image = nil;
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
[self _setStateAndResubscribeImageResponseObserver:nullptr];
_x = nil;
_y = nil;
_imageheight = nil;
_imagewidth = nil;
_src = nil;
_align = nil;
_meetOrSlice = kRNSVGVBMOSMeet;
if (_image) {
CGImageRelease(_image);
}
_image = nil;
_imageSize = CGSizeZero;
_reloadImageCancellationBlock = nil;
}
#endif // RCT_NEW_ARCH_ENABLED
- (void)setSrc:(RCTImageSource *)src
{
#ifdef RCT_NEW_ARCH_ENABLED
#else
if (src == _src) {
return;
}
_src = src;
CGImageRelease(_image);
_image = nil;
if (src.size.width != 0 && src.size.height != 0) {
_imageSize = src.size;
} else {
_imageSize = CGSizeMake(0, 0);
}
RCTImageLoaderCancellationBlock previousCancellationBlock = _reloadImageCancellationBlock;
if (previousCancellationBlock) {
previousCancellationBlock();
_reloadImageCancellationBlock = nil;
}
_reloadImageCancellationBlock = [[self.bridge moduleForName:@"ImageLoader"]
loadImageWithURLRequest:src.request
callback:^(NSError *error, UIImage *image) {
dispatch_async(dispatch_get_main_queue(), ^{
self->_image = CGImageRetain(image.CGImage);
self->_imageSize = CGSizeMake(CGImageGetWidth(self->_image), CGImageGetHeight(self->_image));
if (self->_onLoad) {
RCTImageSource *sourceLoaded;
#if TARGET_OS_OSX // [macOS]
sourceLoaded = [src imageSourceWithSize:image.size scale:1];
#else
sourceLoaded = [src imageSourceWithSize:image.size scale:image.scale];
#endif
NSDictionary *dict = @{
@"uri" : sourceLoaded.request.URL.absoluteString,
@"width" : @(sourceLoaded.size.width),
@"height" : @(sourceLoaded.size.height),
};
self->_onLoad(@{@"source" : dict});
}
[self invalidate];
});
}];
#endif // RCT_NEW_ARCH_ENABLED
}
- (void)setX:(RNSVGLength *)x
{
if ([x isEqualTo:_x]) {
return;
}
[self invalidate];
_x = x;
}
- (void)setY:(RNSVGLength *)y
{
if ([y isEqualTo:_y]) {
return;
}
[self invalidate];
_y = y;
}
- (void)setImagewidth:(RNSVGLength *)width
{
if ([width isEqualTo:_imagewidth]) {
return;
}
[self invalidate];
_imagewidth = width;
}
- (void)setImageheight:(RNSVGLength *)height
{
if ([height isEqualTo:_imageheight]) {
return;
}
[self invalidate];
_imageheight = height;
}
- (void)setAlign:(NSString *)align
{
if ([align isEqualToString:_align]) {
return;
}
[self invalidate];
_align = align;
}
- (void)setMeetOrSlice:(RNSVGVBMOS)meetOrSlice
{
if (meetOrSlice == _meetOrSlice) {
return;
}
[self invalidate];
_meetOrSlice = meetOrSlice;
}
- (void)dealloc
{
CGImageRelease(_image);
}
- (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect
{
if (CGSizeEqualToSize(CGSizeZero, _imageSize)) {
return;
}
CGContextSaveGState(context);
// add hit area
CGRect hitArea = [self getHitArea];
CGPathRef hitAreaPath = CGPathCreateWithRect(hitArea, nil);
[self setHitArea:hitAreaPath];
CGPathRelease(hitAreaPath);
self.pathBounds = hitArea;
self.fillBounds = hitArea;
self.strokeBounds = hitArea;
// apply viewBox transform on Image render.
CGRect imageBounds = CGRectMake(0, 0, _imageSize.width, _imageSize.height);
CGAffineTransform viewbox = [RNSVGViewBox getTransform:imageBounds
eRect:hitArea
align:self.align
meetOrSlice:self.meetOrSlice];
[self clip:context];
CGContextClipToRect(context, hitArea);
CGContextConcatCTM(context, viewbox);
CGContextTranslateCTM(context, 0, imageBounds.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextDrawImage(context, imageBounds, _image);
CGContextRestoreGState(context);
CGRect bounds = hitArea;
self.clientRect = bounds;
CGAffineTransform current = CGContextGetCTM(context);
CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM);
self.ctm = svgToClientTransform;
self.screenCTM = current;
CGPoint mid = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
CGPoint center = CGPointApplyAffineTransform(mid, self.matrix);
self.bounds = bounds;
if (!isnan(center.x) && !isnan(center.y)) {
self.center = center;
}
self.frame = bounds;
}
- (CGRect)getHitArea
{
CGFloat x = [self relativeOnWidth:self.x];
CGFloat y = [self relativeOnHeight:self.y];
CGFloat width = [self relativeOnWidth:self.imagewidth];
CGFloat height = [self relativeOnHeight:self.imageheight];
if (width == 0) {
width = _imageSize.width;
}
if (height == 0) {
height = _imageSize.height;
}
return CGRectMake(x, y, width, height);
}
- (CGPathRef)getPath:(CGContextRef)context
{
return (CGPathRef)CFAutorelease(CGPathCreateWithRect([self getHitArea], nil));
}
@end
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSVGImageCls(void)
{
return RNSVGImage.class;
}
#endif // RCT_NEW_ARCH_ENABLED