add onLoad prop to Image component (#2293)

# Summary

Closes #1442

We want to add new props to the Image Component.

## Test Plan

Added the Test component. 
Manually test that in Android and IOS platforms on new and old
Architectures.

### What are the steps to reproduce (after prerequisites)?

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |         |
| Android |         |
This commit is contained in:
Bohdan Artiukhov
2024-06-27 16:10:28 +02:00
committed by GitHub
parent 7b5d4daaed
commit c0ee3e9ca0
13 changed files with 225 additions and 9 deletions

View File

@@ -4,6 +4,7 @@ import React from 'react';
import ColorTest from './src/ColorTest';
import PointerEventsBoxNone from './src/PointerEventsBoxNone';
import Test1374 from './src/Test1374';
import Test1442 from './src/Test1442';
import Test1451 from './src/Test1451';
import Test1718 from './src/Test1718';
import Test1813 from './src/Test1813';

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,116 @@
import React, {useState} from 'react';
import {ImageLoadEventData, Platform, Image as RNImage} from 'react-native';
import {Svg, Image} from 'react-native-svg';
export default function Test1442() {
return <TestWithStrictSize />;
}
function TestRNImage() {
const [state, setState] = useState<ImageLoadEventData['source']>();
console.log(`${Platform.OS} state:`, state);
return (
<RNImage
style={{width: state?.width || '100%', height: state?.height || '100%'}}
source={{
uri: 'https://image-placeholder.com/images/actual-size/75x75.png',
}}
onLoad={e => {
setState(e.nativeEvent.source as any);
console.log(
`RNImage:${Platform.OS} load PNG image from url with strict size`,
e.nativeEvent,
);
}}
/>
);
}
function TestWithStrictSize(): React.JSX.Element {
const [state, setState] = useState<
ImageLoadEventData['source'] | undefined
>();
console.log(`${Platform.OS} state:`, state);
return (
<Svg>
<Image
width={state?.width || '100%'}
height={state?.height || '100%'}
href={'https://image-placeholder.com/images/actual-size/75x75.png'}
onLoad={e => {
setState(e.nativeEvent);
console.log(
`Image:${Platform.OS} load PNG image from url with strict size`,
e.nativeEvent,
);
}}
/>
</Svg>
);
}
const PNGImageFromUrl = () => {
return (
<Svg>
<Image
opacity="1"
width={100}
height={100}
href={'https://static.thenounproject.com/png/1563361-200.png'}
onLoad={e =>
console.log(`${Platform.OS} load png image from url`, e.nativeEvent)
}
/>
</Svg>
);
};
const PNGImageFromFile = () => {
return (
<Svg>
<Image
opacity="1"
width={100}
height={100}
href={require('../images/arrow.png')}
onLoad={e =>
console.log(`${Platform.OS} load png image from file`, e.nativeEvent)
}
/>
</Svg>
);
};
const JPEGImageFromUrl = () => {
return (
<Svg>
<Image
opacity="1"
width={'100%'}
height={'100%'}
href={
'https://images.unsplash.com/photo-1614730321146-b6fa6a46bcb4?q=80&w=6561&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
}
onLoad={e =>
console.log(`${Platform.OS} load JPEG image from url`, e.nativeEvent)
}
/>
</Svg>
);
};
const JPEGImageFromFile = () => {
return (
<Svg>
<Image
opacity="1"
width={'100%'}
height={'100%'}
href={require('../images/earth.jpg')}
onLoad={e =>
console.log(`${Platform.OS} load JPEG image from file`, e.nativeEvent)
}
/>
</Svg>
);
};

View File

@@ -30,8 +30,12 @@ import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.imagehelper.ImageSource;
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
import com.horcrux.svg.events.SvgLoadEvent;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -137,6 +141,15 @@ class ImageView extends RenderableView {
new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(Bitmap bitmap) {
final EventDispatcher mEventDispatcher =
UIManagerHelper.getEventDispatcherForReactTag(mContext, getId());
mEventDispatcher.dispatchEvent(new SvgLoadEvent(
UIManagerHelper.getSurfaceId(ImageView.this),
getId(),
mContext,
uriString,
bitmap.getWidth(),
bitmap.getHeight()));
mLoading.set(false);
SvgView view = getSvgView();
if (view != null) {

View File

@@ -82,6 +82,7 @@ import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.DisplayMetricsHolder;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.MatrixMathHelper;
@@ -134,7 +135,10 @@ import com.facebook.react.viewmanagers.RNSVGTextPathManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGTextPathManagerInterface;
import com.facebook.react.viewmanagers.RNSVGUseManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGUseManagerInterface;
import com.horcrux.svg.events.SvgLoadEvent;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -926,6 +930,12 @@ class RenderableViewManager<T extends RenderableView> extends VirtualViewManager
public void setMeetOrSlice(ImageView node, int meetOrSlice) {
node.setMeetOrSlice(meetOrSlice);
}
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
Map<String, Object> eventTypes = new HashMap<>();
eventTypes.put(SvgLoadEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoad"));
return eventTypes;
}
}
static class CircleViewManager extends RenderableViewManager<CircleView>

View File

@@ -0,0 +1,47 @@
package com.horcrux.svg.events;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.imagehelper.ImageSource;
public class SvgLoadEvent extends Event<SvgLoadEvent> {
public static final String EVENT_NAME = "topLoad";
private final float width;
private final float height;
private final String uri;
public SvgLoadEvent(int surfaceId, int viewId, ReactContext mContext, String uriString, float width, float height) {
super(surfaceId, viewId);
ImageSource imageSource = new ImageSource(mContext, uriString);
this.uri = imageSource.getSource();;
this.width = width;
this.height = height;
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public short getCoalescingKey() {
return 0;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), getEventData());
}
protected WritableMap getEventData() {
WritableMap eventData = Arguments.createMap();
eventData.putDouble("width", width);
eventData.putDouble("height", height);
eventData.putString("uri", uri);
return eventData;
}
}

View File

@@ -32,5 +32,6 @@
@property (nonatomic, strong) RNSVGLength *imageheight;
@property (nonatomic, strong) NSString *align;
@property (nonatomic, assign) RNSVGVBMOS meetOrSlice;
@property (nonatomic, copy) RCTDirectEventBlock onLoad;
@end

View File

@@ -133,6 +133,10 @@ using namespace facebook::react;
// See for more info: T46311063.
return;
}
auto imageSource = _state->getData().getImageSource();
imageSource.size = {image.size.width, image.size.height};
static_cast<const RNSVGImageEventEmitter &>(*_eventEmitter).onLoad({imageSource.size.width, imageSource.size.height, imageSource.uri});
dispatch_async(dispatch_get_main_queue(), ^{
self->_image = CGImageRetain(image.CGImage);
self->_imageSize = CGSizeMake(CGImageGetWidth(self->_image), CGImageGetHeight(self->_image));
@@ -202,6 +206,12 @@ using namespace facebook::react;
dispatch_async(dispatch_get_main_queue(), ^{
self->_image = CGImageRetain(image.CGImage);
self->_imageSize = CGSizeMake(CGImageGetWidth(self->_image), CGImageGetHeight(self->_image));
RCTImageSource *sourceLoaded = [src imageSourceWithSize:image.size scale:image.scale];
self->_onLoad(@{
@"width" : @(sourceLoaded.size.width),
@"height" : @(sourceLoaded.size.height),
@"uri" : sourceLoaded.request.URL.absoluteString
});
[self invalidate];
});
}];

View File

@@ -36,5 +36,6 @@ RCT_CUSTOM_VIEW_PROPERTY(height, id, RNSVGImage)
RCT_EXPORT_VIEW_PROPERTY(src, RCTImageSource)
RCT_EXPORT_VIEW_PROPERTY(align, NSString)
RCT_EXPORT_VIEW_PROPERTY(meetOrSlice, RNSVGVBMOS)
RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock);
@end

View File

@@ -27,7 +27,7 @@ JSI_EXPORT extern const char RNSVGImageComponentName[];
class JSI_EXPORT RNSVGImageShadowNode final : public ConcreteViewShadowNode<
RNSVGImageComponentName,
RNSVGImageProps,
ViewEventEmitter,
RNSVGImageEventEmitter,
RNSVGImageState> {
public:
using ConcreteViewShadowNode::ConcreteViewShadowNode;

View File

@@ -1,11 +1,17 @@
import * as React from 'react';
import type { ImageProps as RNImageProps, NativeMethods } from 'react-native';
import type {
ImageProps as RNImageProps,
NativeMethods,
NativeSyntheticEvent,
} from 'react-native';
import { Image } from 'react-native';
import { alignEnum, meetOrSliceTypes } from '../lib/extract/extractViewBox';
import { withoutXY } from '../lib/extract/extractProps';
import type { CommonPathProps, NumberProp } from '../lib/extract/types';
import Shape from './Shape';
import RNSVGImage from '../fabric/ImageNativeComponent';
import RNSVGImage, {
type ImageLoadEventData,
} from '../fabric/ImageNativeComponent';
const spacesRegExp = /\s+/;
@@ -18,6 +24,7 @@ export interface ImageProps extends CommonPathProps {
href?: RNImageProps['source'] | string;
preserveAspectRatio?: string;
opacity?: NumberProp;
onLoad?: (e: NativeSyntheticEvent<ImageLoadEventData>) => void;
}
export default class SvgImage extends Shape<ImageProps> {
@@ -41,6 +48,7 @@ export default class SvgImage extends Shape<ImageProps> {
height,
xlinkHref,
href = xlinkHref,
onLoad,
} = props;
const modes = preserveAspectRatio
? preserveAspectRatio.trim().split(spacesRegExp)
@@ -53,6 +61,7 @@ export default class SvgImage extends Shape<ImageProps> {
y,
width,
height,
onLoad,
meetOrSlice: meetOrSliceTypes[meetOrSlice] || 0,
align: alignEnum[align] || 'xMidYMid',
src: !href

View File

@@ -5,6 +5,7 @@ import type {
ImageSourcePropType as ImageSource,
} from 'react-native';
import type {
DirectEventHandler,
Float,
Int32,
WithDefault,
@@ -14,6 +15,12 @@ import type { ViewProps } from './utils';
import type { UnsafeMixed } from './codegenUtils';
import { NumberProp } from '../lib/extract/types';
export type ImageLoadEventData = {
width: Float;
height: Float;
uri: string;
};
interface SvgNodeCommonProps {
name?: string;
opacity?: WithDefault<Float, 1.0>;
@@ -62,6 +69,7 @@ interface NativeProps
src?: ImageSource | null;
align?: string;
meetOrSlice?: Int32;
onLoad?: DirectEventHandler<ImageLoadEventData>;
}
export default codegenNativeComponent<NativeProps>('RNSVGImage', {