Skip to content

[Fabric] Implement onProgress for Image #14493

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

Merged
merged 6 commits into from
Apr 10, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Implement onProgress for Image",
"packageName": "react-native-windows",
"email": "[email protected]",
"dependentChangeType": "patch"
}
12 changes: 11 additions & 1 deletion packages/playground/Samples/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const loadingImageUri =
const largeImageUri =
'https://cdn.freebiesupply.com/logos/large/2x/react-logo-png-transparent.png';

const smallImageUri =
const smallImageUri = 'https://reactnative.dev/img/tiny_logo.png';

const flowerImageUri =
'https://cdn.pixabay.com/photo/2021/08/02/00/10/flowers-6515538_1280.jpg';

const reactLogoUri = 'https://reactjs.org/logo-og.png';
Expand Down Expand Up @@ -74,6 +76,8 @@ export default class Bootstrap extends React.Component<
let imageUri = '';
if (value === 'small') {
imageUri = smallImageUri;
} else if (value === 'flower') {
imageUri = flowerImageUri;
} else if (value === 'large') {
imageUri = largeImageUri;
} else if (value === 'data-svg') {
Expand Down Expand Up @@ -138,6 +142,10 @@ export default class Bootstrap extends React.Component<
this.setSelection(value);
};

handleOnProgress = (event: any) => {
const {progress, loaded, total} = event.nativeEvent;
console.log(`Progress: ${progress}, Loaded = ${loaded} , Total = ${total}`);
};
render() {
const resizeModes = [
{label: 'center', value: 'center'},
Expand All @@ -149,6 +157,7 @@ export default class Bootstrap extends React.Component<

const imageSources = [
{label: 'small', value: 'small'},
{label: 'flower', value: 'flower'},
{label: 'large', value: 'large'},
{label: 'data', value: 'data'},
{label: 'data-svg', value: 'data-svg'},
Expand Down Expand Up @@ -258,6 +267,7 @@ export default class Bootstrap extends React.Component<
onLoad={() => console.log('onLoad')}
onLoadStart={() => console.log('onLoadStart')}
onLoadEnd={() => console.log('onLoadEnd')}
onProgress={this.handleOnProgress}
/>
)}
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,29 @@ extern "C" HRESULT WINAPI WICCreateImagingFactory_Proxy(UINT SDKVersion, IWICIma
namespace winrt::Microsoft::ReactNative::Composition::implementation {

ImageComponentView::WindowsImageResponseObserver::WindowsImageResponseObserver(
winrt::Microsoft::ReactNative::ReactContext const &reactContext,
winrt::weak_ref<winrt::Microsoft::ReactNative::Composition::implementation::ImageComponentView> wkImage)
: m_wkImage(std::move(wkImage)) {}
: m_reactContext(reactContext), m_wkImage(std::move(wkImage)) {}

void ImageComponentView::WindowsImageResponseObserver::didReceiveProgress(float progress, int64_t loaded, int64_t total)
const {
// TODO progress?
int loadedInt = static_cast<int>(loaded);
int totalInt = static_cast<int>(total);
m_reactContext.UIDispatcher().Post([progress, wkImage = m_wkImage, loadedInt, totalInt]() {
if (auto image = wkImage.get()) {
image->didReceiveProgress(progress, loadedInt, totalInt);
}
});
}

void ImageComponentView::WindowsImageResponseObserver::didReceiveImage(
facebook::react::ImageResponse const &imageResponse) const {
if (auto imgComponentView{m_wkImage.get()}) {
auto imageResponseImage = std::static_pointer_cast<ImageResponseImage>(imageResponse.getImage());
imgComponentView->m_reactContext.UIDispatcher().Post([imageResponseImage, wkImage = m_wkImage]() {
if (auto image{wkImage.get()}) {
image->didReceiveImage(imageResponseImage);
}
});
}
auto imageResponseImage = std::static_pointer_cast<ImageResponseImage>(imageResponse.getImage());
m_reactContext.UIDispatcher().Post([imageResponseImage, wkImage = m_wkImage]() {
if (auto image{wkImage.get()}) {
image->didReceiveImage(imageResponseImage);
}
});
}

void ImageComponentView::WindowsImageResponseObserver::didReceiveFailure(
Expand Down Expand Up @@ -95,6 +100,21 @@ void ImageComponentView::ImageLoadStart() noexcept {
}
}

void ImageComponentView::ImageLoaded() noexcept {
auto imageEventEmitter = std::static_pointer_cast<facebook::react::ImageEventEmitter const>(m_eventEmitter);
if (imageEventEmitter) {
imageEventEmitter->onLoadEnd();
}
}

void ImageComponentView::didReceiveProgress(float progress, int loaded, int total) noexcept {
auto imageEventEmitter = std::static_pointer_cast<facebook::react::ImageEventEmitter const>(m_eventEmitter);
if (imageEventEmitter) {
imageEventEmitter->onProgress(progress, loaded, total);
}
ensureDrawingSurface();
}

void ImageComponentView::didReceiveImage(const std::shared_ptr<ImageResponseImage> &imageResponseImage) noexcept {
// TODO check for recycled?

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

if (!m_imageResponseObserver) {
m_imageResponseObserver = std::make_shared<WindowsImageResponseObserver>(get_weak());
m_imageResponseObserver = std::make_shared<WindowsImageResponseObserver>(this->m_reactContext, get_weak());
}

setStateAndResubscribeImageResponseObserver(newImageState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,23 @@ struct ImageComponentView : ImageComponentViewT<ImageComponentView, ViewComponen
struct WindowsImageResponseObserver : facebook::react::ImageResponseObserver {
public:
WindowsImageResponseObserver(
winrt::Microsoft::ReactNative::ReactContext const &reactContext,
winrt::weak_ref<winrt::Microsoft::ReactNative::Composition::implementation::ImageComponentView> wkImage);
void didReceiveProgress(float progress, int64_t loaded, int64_t total) const override;
void didReceiveImage(const facebook::react::ImageResponse &imageResponse) const override;
void didReceiveFailure(const facebook::react::ImageLoadError &error) const override;

private:
winrt::weak_ref<winrt::Microsoft::ReactNative::Composition::implementation::ImageComponentView> m_wkImage;
winrt::Microsoft::ReactNative::ReactContext m_reactContext;
};

void ensureDrawingSurface() noexcept;
void DrawImage() noexcept;

void ImageLoadStart() noexcept;
void ImageLoaded() noexcept;
void didReceiveProgress(float progress, int loaded, int total) noexcept;
void didReceiveImage(const std::shared_ptr<ImageResponseImage> &wicbmp) noexcept;
void didReceiveFailureFromObserver(const facebook::react::ImageLoadError &error) noexcept;
void setStateAndResubscribeImageResponseObserver(
Expand Down
40 changes: 32 additions & 8 deletions vnext/Microsoft.ReactNative/Fabric/WindowsImageManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ wicBitmapSourceFromStream(const winrt::Windows::Storage::Streams::IRandomAccessS
}

winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::ImageResponse>
WindowsImageManager::GetImageRandomAccessStreamAsync(ReactImageSource source) const {
WindowsImageManager::GetImageRandomAccessStreamAsync(
ReactImageSource source,
std::function<void(uint64_t loaded, uint64_t total)> progressCallback) const {
co_await winrt::resume_background();

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

auto inputStream = co_await response.Content().ReadAsInputStreamAsync();
auto contentLengthRef = response.Content().Headers().ContentLength();
uint64_t total = contentLengthRef ? contentLengthRef.GetUInt64() : 0;
uint64_t loaded = 0;

winrt::Windows::Storage::Streams::InMemoryRandomAccessStream memoryStream;
co_await response.Content().WriteToStreamAsync(memoryStream);
winrt::Windows::Storage::Streams::DataReader reader(inputStream);
constexpr uint32_t bufferSize = 16 * 1024;

while (true) {
uint32_t loadedBuffer = co_await reader.LoadAsync(bufferSize);
if (loadedBuffer == 0)
break;

auto buffer = reader.ReadBuffer(loadedBuffer);
co_await memoryStream.WriteAsync(buffer);
loaded += loadedBuffer;

if (progressCallback) {
progressCallback(loaded, total);
}
}

memoryStream.Seek(0);

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

imageResponseTask = GetImageRandomAccessStreamAsync(source);
auto progressCallback = [weakObserverCoordinator](int64_t loaded, int64_t total) {
if (auto observerCoordinator = weakObserverCoordinator.lock()) {
float progress = total > 0 ? static_cast<float>(loaded) / static_cast<float>(total) : 1.0f;
observerCoordinator->nativeImageResponseProgress(progress, loaded, total);
}
};
imageResponseTask = GetImageRandomAccessStreamAsync(source, progressCallback);
}

imageResponseTask.Completed([weakObserverCoordinator](auto asyncOp, auto status) {
Expand Down Expand Up @@ -195,11 +224,6 @@ facebook::react::ImageRequest WindowsImageManager::requestImage(
observerCoordinator->nativeImageResponseFailed(facebook::react::ImageLoadError(errorInfo));
break;
}
case winrt::Windows::Foundation::AsyncStatus::Started: {
// TODO progress? - Can we register for progress off the download task?
// observerCoordinator->nativeImageResponseProgress(0.0/*progress*/, 0/*completed*/, 0/*total*/);
break;
}
}
});
return imageRequest;
Expand Down
4 changes: 3 additions & 1 deletion vnext/Microsoft.ReactNative/Fabric/WindowsImageManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ struct WindowsImageManager {

private:
winrt::Windows::Foundation::IAsyncOperation<winrt::Microsoft::ReactNative::Composition::ImageResponse>
GetImageRandomAccessStreamAsync(ReactImageSource source) const;
GetImageRandomAccessStreamAsync(
ReactImageSource source,
std::function<void(uint64_t loaded, uint64_t total)> progressCallback) const;

winrt::Windows::Web::Http::HttpClient m_httpClient;
winrt::Microsoft::ReactNative::ReactContext m_reactContext;
Expand Down