Skip to content

Commit 4abfda1

Browse files
anupriya13acoates-ms
authored andcommitted
[Fabric] Implement onProgress for Image (microsoft#14493)
1 parent cb60392 commit 4abfda1

File tree

6 files changed

+216
-22
lines changed

6 files changed

+216
-22
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Implement onProgress for Image",
4+
"packageName": "react-native-windows",
5+
"email": "54227869+anupriya13@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/playground/Samples/image.tsx

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@ import {
1818
const largeImageUri =
1919
'https://cdn.freebiesupply.com/logos/large/2x/react-logo-png-transparent.png';
2020

21-
const smallImageUri =
22-
'https://facebook.github.io/react-native/img/header_logo.png';
21+
const smallImageUri = 'https://reactnative.dev/img/tiny_logo.png';
22+
23+
const flowerImageUri =
24+
'https://cdn.pixabay.com/photo/2021/08/02/00/10/flowers-6515538_1280.jpg';
25+
26+
const reactLogoUri = 'https://reactjs.org/logo-og.png';
27+
28+
const svgUri =
29+
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iIzYxREFGQiIvPjwvc3ZnPg==';
2330

2431
const dataImageUri =
2532
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEXRFWHRTb2Z0d2FyZQBwbmdjcnVzaEB1SfMAAABQSURBVGje7dSxCQBACARB+2/ab8BEeQNhFi6WSYzYLYudDQYGBgYGBgYGBgYGBgYGBgZmcvDqYGBgmhivGQYGBgYGBgYGBgYGBgYGBgbmQw+P/eMrC5UTVAAAAABJRU5ErkJggg==';
@@ -60,6 +67,8 @@ export default class Bootstrap extends React.Component<
6067

6168
if (value === 'small') {
6269
imageUri = smallImageUri;
70+
} else if (value === 'flower') {
71+
imageUri = flowerImageUri;
6372
} else if (value === 'large') {
6473
imageUri = largeImageUri;
6574
} else if (value === 'data-svg') {
@@ -71,7 +80,93 @@ export default class Bootstrap extends React.Component<
7180
this.setState({imageUri});
7281
};
7382

83+
setModalVisible = (visible: boolean, pickerName: string | null = null) => {
84+
this.setState({modalVisible: visible, currentPicker: pickerName});
85+
};
86+
87+
setSelection = (value: any) => {
88+
const {currentPicker} = this.state;
89+
switch (currentPicker) {
90+
case 'resizeMode':
91+
this.setState({selectedResizeMode: value});
92+
break;
93+
case 'imageSource':
94+
this.switchImageUri(value);
95+
break;
96+
case 'blurRadius':
97+
this.setState({blurRadius: value});
98+
break;
99+
case 'tintColor':
100+
this.setState({tintColor: value});
101+
break;
102+
default:
103+
break;
104+
}
105+
106+
this.setModalVisible(false);
107+
};
108+
109+
handleResizeModesSelect = (value: any) => {
110+
this.setState({selectedResizeMode: value});
111+
this.state.currentPicker = 'resizeMode';
112+
this.setSelection(value);
113+
};
114+
115+
handleImageSourcesSelect = (value: any) => {
116+
this.setState({selectedSource: value});
117+
this.state.currentPicker = 'imageSource';
118+
this.setSelection(value);
119+
};
120+
121+
handleBlurRadiusSelect = (value: any) => {
122+
this.setState({blurRadius: value});
123+
this.state.currentPicker = 'blurRadius';
124+
this.setSelection(value);
125+
};
126+
127+
handleTintColorSelect = (value: any) => {
128+
this.setState({tintColor: value});
129+
this.state.currentPicker = 'tintColor';
130+
this.setSelection(value);
131+
};
132+
133+
handleOnProgress = (event: any) => {
134+
const {progress, loaded, total} = event.nativeEvent;
135+
console.log(`Progress: ${progress}, Loaded = ${loaded} , Total = ${total}`);
136+
};
74137
render() {
138+
const resizeModes = [
139+
{label: 'center', value: 'center'},
140+
{label: 'cover', value: 'cover'},
141+
{label: 'contain', value: 'contain'},
142+
{label: 'stretch', value: 'stretch'},
143+
{label: 'repeat', value: 'repeat'},
144+
];
145+
146+
const imageSources = [
147+
{label: 'small', value: 'small'},
148+
{label: 'flower', value: 'flower'},
149+
{label: 'large', value: 'large'},
150+
{label: 'data', value: 'data'},
151+
{label: 'data-svg', value: 'data-svg'},
152+
{label: 'svg', value: 'svg'},
153+
{label: 'react-logo', value: 'react-logo'},
154+
];
155+
156+
const blurRadiusOptions = [
157+
{label: '0', value: 0},
158+
{label: '5', value: 5},
159+
{label: '10', value: 10},
160+
];
161+
162+
const tintColors = [
163+
{label: 'None', value: 'transparent'},
164+
{label: 'Purple', value: 'purple'},
165+
{label: 'Green', value: 'green'},
166+
{label: 'AccentDark1', value: 'accentDark1'},
167+
{label: 'TextFillColorPrimary', value: 'textFillColorPrimary'},
168+
];
169+
75170
return (
76171
<View style={styles.container}>
77172
<View style={styles.rowContainer}>
@@ -151,6 +246,49 @@ export default class Bootstrap extends React.Component<
151246
resizeMode={this.state.selectedResizeMode}
152247
blurRadius={this.state.blurRadius}
153248
/>
249+
<Text>Include defaultSource Only</Text>
250+
</View>
251+
<View
252+
style={
253+
this.state.includedefaultSourceOnly
254+
? styles.imageContainerDefault
255+
: styles.imageContainer
256+
}>
257+
{this.state.includedefaultSourceOnly ? (
258+
<Image
259+
style={[
260+
styles.image,
261+
this.state.includeBorder ? styles.imageWithBorder : {},
262+
this.state.tintColor === 'accentDark1'
263+
? styles.imageWithPlatformColor
264+
: this.state.tintColor === 'textFillColorPrimary'
265+
? styles.imageWithPlatformColorPrimary
266+
: {tintColor: this.state.tintColor},
267+
]}
268+
defaultSource={{uri: this.state.defaultImageUri}}
269+
/>
270+
) : (
271+
<Image
272+
style={[
273+
styles.image,
274+
this.state.includeBorder ? styles.imageWithBorder : {},
275+
this.state.tintColor === 'accentDark1'
276+
? styles.imageWithPlatformColor
277+
: this.state.tintColor === 'textFillColorPrimary'
278+
? styles.imageWithPlatformColorPrimary
279+
: {tintColor: this.state.tintColor},
280+
]}
281+
defaultSource={{uri: this.state.defaultImageUri}}
282+
source={{uri: this.state.imageUri}}
283+
loadingIndicatorSource={{uri: loadingImageUri}}
284+
resizeMode={this.state.selectedResizeMode}
285+
blurRadius={this.state.blurRadius}
286+
onLoad={() => console.log('onLoad')}
287+
onLoadStart={() => console.log('onLoadStart')}
288+
onLoadEnd={() => console.log('onLoadEnd')}
289+
onProgress={this.handleOnProgress}
290+
/>
291+
)}
154292
</View>
155293
</View>
156294
);

vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,29 @@ extern "C" HRESULT WINAPI WICCreateImagingFactory_Proxy(UINT SDKVersion, IWICIma
3131
namespace winrt::Microsoft::ReactNative::Composition::implementation {
3232

3333
ImageComponentView::WindowsImageResponseObserver::WindowsImageResponseObserver(
34+
winrt::Microsoft::ReactNative::ReactContext const &reactContext,
3435
winrt::weak_ref<winrt::Microsoft::ReactNative::Composition::implementation::ImageComponentView> wkImage)
35-
: m_wkImage(std::move(wkImage)) {}
36+
: m_reactContext(reactContext), m_wkImage(std::move(wkImage)) {}
3637

3738
void ImageComponentView::WindowsImageResponseObserver::didReceiveProgress(float progress, int64_t loaded, int64_t total)
3839
const {
39-
// TODO progress?
40+
int loadedInt = static_cast<int>(loaded);
41+
int totalInt = static_cast<int>(total);
42+
m_reactContext.UIDispatcher().Post([progress, wkImage = m_wkImage, loadedInt, totalInt]() {
43+
if (auto image = wkImage.get()) {
44+
image->didReceiveProgress(progress, loadedInt, totalInt);
45+
}
46+
});
4047
}
4148

4249
void ImageComponentView::WindowsImageResponseObserver::didReceiveImage(
4350
facebook::react::ImageResponse const &imageResponse) const {
44-
if (auto imgComponentView{m_wkImage.get()}) {
45-
auto imageResponseImage = std::static_pointer_cast<ImageResponseImage>(imageResponse.getImage());
46-
imgComponentView->m_reactContext.UIDispatcher().Post([imageResponseImage, wkImage = m_wkImage]() {
47-
if (auto image{wkImage.get()}) {
48-
image->didReceiveImage(imageResponseImage);
49-
}
50-
});
51-
}
51+
auto imageResponseImage = std::static_pointer_cast<ImageResponseImage>(imageResponse.getImage());
52+
m_reactContext.UIDispatcher().Post([imageResponseImage, wkImage = m_wkImage]() {
53+
if (auto image{wkImage.get()}) {
54+
image->didReceiveImage(imageResponseImage);
55+
}
56+
});
5257
}
5358

5459
void ImageComponentView::WindowsImageResponseObserver::didReceiveFailure(
@@ -95,6 +100,21 @@ void ImageComponentView::ImageLoadStart() noexcept {
95100
}
96101
}
97102

103+
void ImageComponentView::ImageLoaded() noexcept {
104+
auto imageEventEmitter = std::static_pointer_cast<facebook::react::ImageEventEmitter const>(m_eventEmitter);
105+
if (imageEventEmitter) {
106+
imageEventEmitter->onLoadEnd();
107+
}
108+
}
109+
110+
void ImageComponentView::didReceiveProgress(float progress, int loaded, int total) noexcept {
111+
auto imageEventEmitter = std::static_pointer_cast<facebook::react::ImageEventEmitter const>(m_eventEmitter);
112+
if (imageEventEmitter) {
113+
imageEventEmitter->onProgress(progress, loaded, total);
114+
}
115+
ensureDrawingSurface();
116+
}
117+
98118
void ImageComponentView::didReceiveImage(const std::shared_ptr<ImageResponseImage> &imageResponseImage) noexcept {
99119
// TODO check for recycled?
100120

@@ -152,7 +172,7 @@ void ImageComponentView::updateState(
152172
auto newImageState = std::static_pointer_cast<facebook::react::ImageShadowNode::ConcreteState const>(state);
153173

154174
if (!m_imageResponseObserver) {
155-
m_imageResponseObserver = std::make_shared<WindowsImageResponseObserver>(get_weak());
175+
m_imageResponseObserver = std::make_shared<WindowsImageResponseObserver>(this->m_reactContext, get_weak());
156176
}
157177

158178
setStateAndResubscribeImageResponseObserver(newImageState);

vnext/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,23 @@ struct ImageComponentView : ImageComponentViewT<ImageComponentView, ViewComponen
7171
struct WindowsImageResponseObserver : facebook::react::ImageResponseObserver {
7272
public:
7373
WindowsImageResponseObserver(
74+
winrt::Microsoft::ReactNative::ReactContext const &reactContext,
7475
winrt::weak_ref<winrt::Microsoft::ReactNative::Composition::implementation::ImageComponentView> wkImage);
7576
void didReceiveProgress(float progress, int64_t loaded, int64_t total) const override;
7677
void didReceiveImage(const facebook::react::ImageResponse &imageResponse) const override;
7778
void didReceiveFailure(const facebook::react::ImageLoadError &error) const override;
7879

7980
private:
8081
winrt::weak_ref<winrt::Microsoft::ReactNative::Composition::implementation::ImageComponentView> m_wkImage;
82+
winrt::Microsoft::ReactNative::ReactContext m_reactContext;
8183
};
8284

8385
void ensureDrawingSurface() noexcept;
8486
void DrawImage() noexcept;
8587

8688
void ImageLoadStart() noexcept;
8789
void ImageLoaded() noexcept;
90+
void didReceiveProgress(float progress, int loaded, int total) noexcept;
8891
void didReceiveImage(const std::shared_ptr<ImageResponseImage> &wicbmp) noexcept;
8992
void didReceiveFailureFromObserver(const facebook::react::ImageLoadError &error) noexcept;
9093
void setStateAndResubscribeImageResponseObserver(

vnext/Microsoft.ReactNative/Fabric/WindowsImageManager.cpp

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ wicBitmapSourceFromStream(const winrt::Windows::Storage::Streams::IRandomAccessS
7878
}
7979

8080
winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::ImageResponse>
81-
WindowsImageManager::GetImageRandomAccessStreamAsync(ReactImageSource source) const {
81+
WindowsImageManager::GetImageRandomAccessStreamAsync(
82+
ReactImageSource source,
83+
std::function<void(uint64_t loaded, uint64_t total)> progressCallback) const {
8284
co_await winrt::resume_background();
8385

8486
winrt::Windows::Foundation::Uri uri(winrt::to_hstring(source.uri));
@@ -130,8 +132,29 @@ WindowsImageManager::GetImageRandomAccessStreamAsync(ReactImageSource source) co
130132
response.ReasonPhrase(), response.StatusCode(), response.Headers());
131133
}
132134

135+
auto inputStream = co_await response.Content().ReadAsInputStreamAsync();
136+
auto contentLengthRef = response.Content().Headers().ContentLength();
137+
uint64_t total = contentLengthRef ? contentLengthRef.GetUInt64() : 0;
138+
uint64_t loaded = 0;
139+
133140
winrt::Windows::Storage::Streams::InMemoryRandomAccessStream memoryStream;
134-
co_await response.Content().WriteToStreamAsync(memoryStream);
141+
winrt::Windows::Storage::Streams::DataReader reader(inputStream);
142+
constexpr uint32_t bufferSize = 16 * 1024;
143+
144+
while (true) {
145+
uint32_t loadedBuffer = co_await reader.LoadAsync(bufferSize);
146+
if (loadedBuffer == 0)
147+
break;
148+
149+
auto buffer = reader.ReadBuffer(loadedBuffer);
150+
co_await memoryStream.WriteAsync(buffer);
151+
loaded += loadedBuffer;
152+
153+
if (progressCallback) {
154+
progressCallback(loaded, total);
155+
}
156+
}
157+
135158
memoryStream.Seek(0);
136159

137160
co_return winrt::Microsoft::ReactNative::Composition::StreamImageResponse(memoryStream.CloneStream());
@@ -160,7 +183,13 @@ facebook::react::ImageRequest WindowsImageManager::requestImage(
160183
source.width = imageSource.size.width;
161184
source.sourceType = ImageSourceType::Download;
162185

163-
imageResponseTask = GetImageRandomAccessStreamAsync(source);
186+
auto progressCallback = [weakObserverCoordinator](int64_t loaded, int64_t total) {
187+
if (auto observerCoordinator = weakObserverCoordinator.lock()) {
188+
float progress = total > 0 ? static_cast<float>(loaded) / static_cast<float>(total) : 1.0f;
189+
observerCoordinator->nativeImageResponseProgress(progress, loaded, total);
190+
}
191+
};
192+
imageResponseTask = GetImageRandomAccessStreamAsync(source, progressCallback);
164193
}
165194

166195
imageResponseTask.Completed([weakObserverCoordinator](auto asyncOp, auto status) {
@@ -195,11 +224,6 @@ facebook::react::ImageRequest WindowsImageManager::requestImage(
195224
observerCoordinator->nativeImageResponseFailed(facebook::react::ImageLoadError(errorInfo));
196225
break;
197226
}
198-
case winrt::Windows::Foundation::AsyncStatus::Started: {
199-
// TODO progress? - Can we register for progress off the download task?
200-
// observerCoordinator->nativeImageResponseProgress(0.0/*progress*/, 0/*completed*/, 0/*total*/);
201-
break;
202-
}
203227
}
204228
});
205229
return imageRequest;

vnext/Microsoft.ReactNative/Fabric/WindowsImageManager.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ struct WindowsImageManager {
2929

3030
private:
3131
winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::ImageResponse>
32-
GetImageRandomAccessStreamAsync(ReactImageSource source) const;
32+
GetImageRandomAccessStreamAsync(
33+
ReactImageSource source,
34+
std::function<void(uint64_t loaded, uint64_t total)> progressCallback) const;
3335

3436
winrt::Windows::Web::Http::HttpClient m_httpClient;
3537
winrt::Microsoft::ReactNative::ReactContext m_reactContext;

0 commit comments

Comments
 (0)