Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add onLoad prop to Image component #2293

Merged
merged 18 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions TestsExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Binary file added TestsExample/images/arrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added TestsExample/images/earth.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
116 changes: 116 additions & 0 deletions TestsExample/src/Test1442.tsx
Original file line number Diff line number Diff line change
@@ -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
bohdanprog marked this conversation as resolved.
Show resolved Hide resolved
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>
);
};
13 changes: 13 additions & 0 deletions android/src/main/java/com/horcrux/svg/ImageView.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@
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;
Expand Down Expand Up @@ -137,6 +141,15 @@ private void loadBitmap(final ImagePipeline imagePipeline, final ImageRequest re
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) {
Expand Down
10 changes: 10 additions & 0 deletions android/src/main/java/com/horcrux/svg/RenderableViewManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
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;
Expand Down Expand Up @@ -134,7 +135,10 @@
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;

Expand Down Expand Up @@ -926,6 +930,12 @@ public void setAlign(ImageView node, String align) {
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>
Expand Down
47 changes: 47 additions & 0 deletions android/src/main/java/com/horcrux/svg/events/SvgLoadEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.horcrux.svg.events;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have 2 events? This one looks like the proper one.


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;
}
}
1 change: 1 addition & 0 deletions apple/Elements/RNSVGImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 16 additions & 6 deletions apple/Elements/RNSVGImage.mm
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(voi
// 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));
Expand Down Expand Up @@ -199,12 +203,18 @@ - (void)setSrc:(RCTImageSource *)src
_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));
[self invalidate];
});
}];
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];
});
}];
#endif // RCT_NEW_ARCH_ENABLED
}

Expand Down
1 change: 1 addition & 0 deletions apple/ViewManagers/RNSVGImageManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ - (RNSVGRenderable *)node
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 11 additions & 2 deletions src/elements/Image.tsx
Original file line number Diff line number Diff line change
@@ -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+/;

Expand All @@ -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> {
Expand All @@ -41,6 +48,7 @@ export default class SvgImage extends Shape<ImageProps> {
height,
xlinkHref,
href = xlinkHref,
onLoad,
} = props;
const modes = preserveAspectRatio
? preserveAspectRatio.trim().split(spacesRegExp)
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/fabric/ImageNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ImageSourcePropType as ImageSource,
} from 'react-native';
import type {
DirectEventHandler,
Float,
Int32,
WithDefault,
Expand All @@ -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>;
Expand Down Expand Up @@ -62,6 +69,7 @@ interface NativeProps
src?: ImageSource | null;
align?: string;
meetOrSlice?: Int32;
onLoad?: DirectEventHandler<ImageLoadEventData>;
}

export default codegenNativeComponent<NativeProps>('RNSVGImage', {
Expand Down
Loading