diff --git a/change/react-native-windows-2019-12-02-13-46-48-bitmap-image.json b/change/react-native-windows-2019-12-02-13-46-48-bitmap-image.json new file mode 100644 index 00000000000..cb1dcbc112d --- /dev/null +++ b/change/react-native-windows-2019-12-02-13-46-48-bitmap-image.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "Conditionally use BitmapImage", + "packageName": "react-native-windows", + "email": "email not defined", + "commit": "023ef6ee7d849d6fa1fc319040ead78d11328bf3", + "date": "2019-12-02T21:46:48.495Z" +} \ No newline at end of file diff --git a/packages/playground/Samples/image.tsx b/packages/playground/Samples/image.tsx index 96628fb6b42..1e0a8be5533 100644 --- a/packages/playground/Samples/image.tsx +++ b/packages/playground/Samples/image.tsx @@ -12,6 +12,9 @@ const largeImageUri = const smallImageUri = 'http://facebook.github.io/react-native/img/header_logo.png'; +const dataImageUri = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEXRFWHRTb2Z0d2FyZQBwbmdjcnVzaEB1SfMAAABQSURBVGje7dSxCQBACARB+2/ab8BEeQNhFi6WSYzYLYudDQYGBgYGBgYGBgYGBgYGBgZmcvDqYGBgmhivGQYGBgYGBgYGBgYGBgYGBgbmQw+P/eMrC5UTVAAAAABJRU5ErkJggg=='; + export default class Bootstrap extends React.Component< {}, { @@ -22,69 +25,78 @@ export default class Bootstrap extends React.Component< | 'contain' | 'repeat' | undefined; - useLargeImage: boolean; inlcudeBorder: boolean; - imageUrl: string; + selectedSource: string; + imageUri: string; } > { state = { selectedResizeMode: 'center' as 'center', - useLargeImage: false, + selectedSource: 'small', inlcudeBorder: false, - imageUrl: 'http://facebook.github.io/react-native/img/header_logo.png', + imageUri: 'http://facebook.github.io/react-native/img/header_logo.png', }; - switchImageUrl = () => { - const useLargeImage = !this.state.useLargeImage; - this.setState({useLargeImage}); + switchImageUri = (value: string) => { + this.setState({selectedSource: value}); + + let imageUri = ''; + + if (value === 'small') { + imageUri = smallImageUri; + } else if (value === 'large') { + imageUri = largeImageUri; + } else if (value === 'data') { + imageUri = dataImageUri; + } - const imageUrl = useLargeImage ? largeImageUri : smallImageUri; - this.setState({imageUrl}); + this.setState({imageUri}); }; render() { return ( - - ResizeMode - - this.setState({selectedResizeMode: value}) - }> - - - - - - - - - Image Size - Small - - Large - - - No Border - this.setState({inlcudeBorder: value})} - /> - Round Border - + ResizeMode + this.setState({selectedResizeMode: value})}> + + + + + + + + + Image Source + this.switchImageUri(value)}> + + + + + + + No Border + + this.setState({inlcudeBorder: value}) + } + /> + Round Border @@ -103,6 +115,7 @@ const styles = StyleSheet.create({ rowContainer: { flexDirection: 'row', alignItems: 'center', + marginBottom: 5, }, imageContainer: { marginTop: 5, @@ -125,7 +138,7 @@ const styles = StyleSheet.create({ title: { fontWeight: 'bold', margin: 5, - width: 80, + width: 100, }, }); diff --git a/vnext/ReactUWP/Modules/ImageViewManagerModule.cpp b/vnext/ReactUWP/Modules/ImageViewManagerModule.cpp index 2b1730cc5f5..a018641f7f5 100644 --- a/vnext/ReactUWP/Modules/ImageViewManagerModule.cpp +++ b/vnext/ReactUWP/Modules/ImageViewManagerModule.cpp @@ -61,7 +61,7 @@ winrt::fire_and_forget GetImageSizeAsync( bool succeeded{false}; try { - ImageSource source; + ReactImageSource source; source.uri = uriString; winrt::Uri uri{Microsoft::Common::Unicode::Utf8ToUtf16(uriString)}; diff --git a/vnext/ReactUWP/Views/Image/ImageViewManager.cpp b/vnext/ReactUWP/Views/Image/ImageViewManager.cpp index 5e2a9222a5a..9b0214528ec 100644 --- a/vnext/ReactUWP/Views/Image/ImageViewManager.cpp +++ b/vnext/ReactUWP/Views/Image/ImageViewManager.cpp @@ -24,9 +24,9 @@ using namespace Windows::UI::Xaml::Controls; // Such code is better to move to a seperate parser layer template <> -struct json_type_traits { - static react::uwp::ImageSource parseJson(const folly::dynamic &json) { - react::uwp::ImageSource source; +struct json_type_traits { + static react::uwp::ReactImageSource parseJson(const folly::dynamic &json) { + react::uwp::ReactImageSource source; for (auto &item : json.items()) { if (item.first == "uri") source.uri = item.second.asString(); @@ -81,7 +81,7 @@ class ImageShadowNode : public ShadowNodeBase { m_onLoadEndToken = reactImage->OnLoadEnd([imageViewManager{static_cast(GetViewManager())}, reactImage](const auto &, const bool &succeeded) { - ImageSource source{reactImage->Source()}; + ReactImageSource source{reactImage->Source()}; imageViewManager->EmitImageEvent(reactImage.as(), succeeded ? "topLoad" : "topError", source); imageViewManager->EmitImageEvent(reactImage.as(), "topLoadEnd", source); @@ -143,7 +143,7 @@ void ImageViewManager::UpdateProperties(ShadowNodeBase *nodeToUpdate, const foll UpdateCornerRadiusOnElement(nodeToUpdate, grid); } -void ImageViewManager::EmitImageEvent(winrt::Grid grid, const char *eventName, ImageSource &source) { +void ImageViewManager::EmitImageEvent(winrt::Grid grid, const char *eventName, ReactImageSource &source) { auto reactInstance{m_wkReactInstance.lock()}; if (reactInstance == nullptr) return; @@ -161,9 +161,13 @@ void ImageViewManager::setSource(winrt::Grid grid, const folly::dynamic &data) { if (instance == nullptr) return; - auto sources{json_type_traits>::parseJson(data)}; + auto sources{json_type_traits>::parseJson(data)}; sources[0].bundleRootPath = instance->GetBundleRootPath(); + if (sources[0].packagerAsset && sources[0].uri.find("file://") == 0) { + sources[0].uri.replace(0, 7, sources[0].bundleRootPath); + } + auto reactImage{grid.as()}; EmitImageEvent(grid, "topLoadStart", sources[0]); diff --git a/vnext/ReactUWP/Views/Image/ImageViewManager.h b/vnext/ReactUWP/Views/Image/ImageViewManager.h index c22697ab611..a27d18c7eab 100644 --- a/vnext/ReactUWP/Views/Image/ImageViewManager.h +++ b/vnext/ReactUWP/Views/Image/ImageViewManager.h @@ -19,7 +19,7 @@ class ImageViewManager : public FrameworkElementViewManager { folly::dynamic GetExportedCustomDirectEventTypeConstants() const override; folly::dynamic GetNativeProps() const override; facebook::react::ShadowNode *createShadow() const override; - void EmitImageEvent(winrt::Windows::UI::Xaml::Controls::Grid grid, const char *eventName, ImageSource &source); + void EmitImageEvent(winrt::Windows::UI::Xaml::Controls::Grid grid, const char *eventName, ReactImageSource &source); protected: XamlView CreateViewCore(int64_t tag) override; diff --git a/vnext/ReactUWP/Views/Image/ReactImage.cpp b/vnext/ReactUWP/Views/Image/ReactImage.cpp index bd91a2ff37d..d00530b5dbe 100644 --- a/vnext/ReactUWP/Views/Image/ReactImage.cpp +++ b/vnext/ReactUWP/Views/Image/ReactImage.cpp @@ -15,7 +15,9 @@ namespace winrt { using namespace Windows::Foundation; using namespace Windows::Storage::Streams; using namespace Windows::UI; +using namespace Windows::UI::Xaml; using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Media::Imaging; using namespace Windows::Web::Http; } // namespace winrt @@ -29,18 +31,16 @@ using Microsoft::Common::Unicode::Utf8ToUtf16; namespace react { namespace uwp { -ReactImage::ReactImage() { - m_brush = ReactImageBrush::Create(); - this->Background(m_brush.as()); -} - /*static*/ winrt::com_ptr ReactImage::Create() { return winrt::make_self(); } winrt::Size ReactImage::ArrangeOverride(winrt::Size finalSize) { - auto brush{Background().as()}; - brush->AvailableSize(finalSize); + if (m_useCompositionBrush) { + if (auto brush{Background().try_as()}) { + brush->AvailableSize(finalSize); + } + } return finalSize; } @@ -53,66 +53,207 @@ void ReactImage::OnLoadEnd(winrt::event_token const &token) noexcept { m_onLoadEndEvent.remove(token); } -winrt::fire_and_forget ReactImage::Source(ImageSource source) { - std::string uriString{source.uri}; - if (uriString.length() == 0) { +void ReactImage::ResizeMode(react::uwp::ResizeMode value) { + if (m_resizeMode != value) { + m_resizeMode = value; + + bool shouldUseCompositionBrush{m_resizeMode == ResizeMode::Repeat}; + bool switchBrushes{m_useCompositionBrush != shouldUseCompositionBrush}; + + if (switchBrushes) { + m_useCompositionBrush = shouldUseCompositionBrush; + SetBackground(false); + } else if (auto bitmapBrush{Background().as()}) { + bitmapBrush.Stretch(ResizeModeToStretch(m_resizeMode)); + } + } +} + +winrt::Stretch ReactImage::ResizeModeToStretch(react::uwp::ResizeMode value) { + switch (value) { + case ResizeMode::Cover: + return winrt::Stretch::UniformToFill; + case ResizeMode::Stretch: + return winrt::Stretch::Fill; + case ResizeMode::Contain: + return winrt::Stretch::Uniform; + default: // ResizeMode::Center + // This function should never be called for the 'repeat' resizeMode case. + // That is handled by the shouldUseCompositionBrush/switchBrushes code path. + assert(value != ResizeMode::Repeat); + + if (m_imageSource.height < ActualHeight() && m_imageSource.width < ActualWidth()) { + return winrt::Stretch::None; + } else { + return winrt::Stretch::Uniform; + } + } +} + +void ReactImage::Source(ReactImageSource source) { + if (source.uri.length() == 0) { m_onLoadEndEvent(*this, false); return; } - if (source.packagerAsset && uriString.find("file://") == 0) { - uriString.replace(0, 7, source.bundleRootPath); - } + m_imageSource = source; - winrt::Uri uri{Utf8ToUtf16(uriString)}; + winrt::Uri uri{Utf8ToUtf16(m_imageSource.uri)}; winrt::hstring scheme{uri.SchemeName()}; - bool needsDownload = (scheme == L"http") || (scheme == L"https"); - bool inlineData = scheme == L"data"; + + if (((scheme == L"http") || (scheme == L"https")) && !m_imageSource.headers.empty()) { + m_imageSource.sourceType = ImageSourceType::Download; + } else if (scheme == L"data") { + m_imageSource.sourceType = ImageSourceType::InlineData; + } + + SetBackground(true); +} + +winrt::IAsyncOperation ReactImage::GetImageMemoryStreamAsync( + ReactImageSource source) { + switch (source.sourceType) { + case ImageSourceType::Download: + return co_await GetImageStreamAsync(source); + case ImageSourceType::InlineData: + return co_await GetImageInlineDataAsync(source); + default: // ImageSourceType::Uri + return nullptr; + } +} + +winrt::fire_and_forget ReactImage::SetBackground(bool fireLoadEndEvent) { + const ReactImageSource source{m_imageSource}; + const winrt::Uri uri{Utf8ToUtf16(source.uri)}; + const bool fromStream{source.sourceType != ImageSourceType::Uri}; + + winrt::InMemoryRandomAccessStream memoryStream{nullptr}; + // get weak reference before any co_await calls auto weak_this{get_weak()}; try { - m_imageSource = source; + memoryStream = co_await GetImageMemoryStreamAsync(source); - winrt::InMemoryRandomAccessStream memoryStream; - if (needsDownload) { - memoryStream = co_await GetImageStreamAsync(source); - } else if (inlineData) { - memoryStream = co_await GetImageInlineDataAsync(source); - } - - if (auto strong_this{weak_this.get()}) { - if ((needsDownload || inlineData) && !memoryStream) { + // Fire failed load event if we're not loading from URI and the memory stream is null. + if (fromStream && !memoryStream) { + if (auto strong_this{weak_this.get()}) { strong_this->m_onLoadEndEvent(*strong_this, false); } + return; + } + } catch (winrt::hresult_error const &) { + const auto strong_this{weak_this.get()}; + if (strong_this && fireLoadEndEvent) { + strong_this->m_onLoadEndEvent(*strong_this, false); + } + } + + if (auto strong_this{weak_this.get()}) { + if (strong_this->m_useCompositionBrush) { + const auto compositionBrush{ReactImageBrush::Create()}; + compositionBrush->ResizeMode(strong_this->m_resizeMode); + + const auto surface = fromStream ? winrt::LoadedImageSurface::StartLoadFromStream(memoryStream) + : winrt::LoadedImageSurface::StartLoadFromUri(uri); + + m_sizeChangedRevoker = strong_this->SizeChanged( + winrt::auto_revoke, [compositionBrush](const auto &, const winrt::SizeChangedEventArgs &args) { + compositionBrush->AvailableSize(args.NewSize()); + }); + + strong_this->m_surfaceLoadedRevoker = surface.LoadCompleted( + winrt::auto_revoke, + [weak_this, compositionBrush, surface, fireLoadEndEvent]( + winrt::LoadedImageSurface const & /*sender*/, + winrt::LoadedImageSourceLoadCompletedEventArgs const &args) { + if (auto strong_this{weak_this.get()}) { + bool succeeded{false}; + if (args.Status() == winrt::LoadedImageSourceLoadStatus::Success) { + winrt::Size size{surface.DecodedPhysicalSize()}; + strong_this->m_imageSource.height = size.Height; + strong_this->m_imageSource.width = size.Width; + + // If we are dynamically switching the resizeMode to 'repeat', then + // the SizeChanged event has already fired and the ReactImageBrush's + // size has not been set. Use ActualSize in that case. + if (compositionBrush->AvailableSize() == winrt::Size{0, 0}) { + compositionBrush->AvailableSize(strong_this->ActualSize()); + } - if (!needsDownload || memoryStream) { - auto surface = needsDownload || inlineData ? winrt::LoadedImageSurface::StartLoadFromStream(memoryStream) - : winrt::LoadedImageSurface::StartLoadFromUri(uri); + compositionBrush->Source(surface); + strong_this->Background(compositionBrush.as()); + succeeded = true; + } - strong_this->m_surfaceLoadedRevoker = surface.LoadCompleted( - winrt::auto_revoke, - [weak_this, surface]( - winrt::LoadedImageSurface const & /*sender*/, - winrt::LoadedImageSourceLoadCompletedEventArgs const &args) { + if (fireLoadEndEvent) { + strong_this->m_onLoadEndEvent(*strong_this, succeeded); + } + + strong_this->m_sizeChangedRevoker.revoke(); + } + }); + } else { + winrt::ImageBrush imageBrush{strong_this->Background().try_as()}; + bool createImageBrush{!imageBrush}; + if (createImageBrush) { + imageBrush = winrt::ImageBrush{}; + + // ImageOpened and ImageFailed are mutually exclusive. One event of the other will + // always fire whenever an ImageBrush has the ImageSource value set or reset. + strong_this->m_imageBrushOpenedRevoker = imageBrush.ImageOpened( + winrt::auto_revoke, [weak_this, imageBrush, fireLoadEndEvent](const auto &, const auto &) { if (auto strong_this{weak_this.get()}) { - bool succeeded{false}; - if (args.Status() == winrt::LoadedImageSourceLoadStatus::Success) { - strong_this->m_brush->Source(surface); - succeeded = true; + const auto bitmap{imageBrush.ImageSource().as()}; + strong_this->m_imageSource.height = bitmap.PixelHeight(); + strong_this->m_imageSource.width = bitmap.PixelWidth(); + + imageBrush.Stretch(strong_this->ResizeModeToStretch(strong_this->m_resizeMode)); + + if (fireLoadEndEvent) { + strong_this->m_onLoadEndEvent(*strong_this, true); } - strong_this->m_onLoadEndEvent(*strong_this, succeeded); + } + }); + + strong_this->m_imageBrushFailedRevoker = + imageBrush.ImageFailed(winrt::auto_revoke, [weak_this, fireLoadEndEvent](const auto &, const auto &) { + const auto strong_this{weak_this.get()}; + if (strong_this && fireLoadEndEvent) { + strong_this->m_onLoadEndEvent(*strong_this, false); } }); } + + winrt::BitmapImage bitmapImage{imageBrush.ImageSource().try_as()}; + + if (!bitmapImage) { + bitmapImage = winrt::BitmapImage{}; + + strong_this->m_bitmapImageOpened = bitmapImage.ImageOpened( + winrt::auto_revoke, [imageBrush](const auto &, const auto &) { imageBrush.Opacity(1); }); + + imageBrush.ImageSource(bitmapImage); + } + + if (createImageBrush) { + strong_this->Background(imageBrush); + } + + if (fromStream) { + co_await bitmapImage.SetSourceAsync(memoryStream); + } else { + bitmapImage.UriSource(uri); + + // TODO: When we change the source of a BitmapImage, we're getting a flicker of the old image + // being resized to the size of the new image. This is a temporary workaround. + imageBrush.Opacity(0); + } } - } catch (winrt::hresult_error const &) { - if (auto strong_this{weak_this.get()}) - strong_this->m_onLoadEndEvent(*strong_this, false); } -} // namespace uwp +} -winrt::IAsyncOperation GetImageStreamAsync(ImageSource source) { +winrt::IAsyncOperation GetImageStreamAsync(ReactImageSource source) { try { co_await winrt::resume_background(); @@ -151,7 +292,7 @@ winrt::IAsyncOperation GetImageStreamAsync(Im return nullptr; } -winrt::IAsyncOperation GetImageInlineDataAsync(ImageSource source) { +winrt::IAsyncOperation GetImageInlineDataAsync(ReactImageSource source) { size_t start = source.uri.find(','); if (start == std::string::npos || start + 1 > source.uri.length()) return nullptr; diff --git a/vnext/ReactUWP/Views/Image/ReactImage.h b/vnext/ReactUWP/Views/Image/ReactImage.h index 612920e10c9..53e517c234a 100644 --- a/vnext/ReactUWP/Views/Image/ReactImage.h +++ b/vnext/ReactUWP/Views/Image/ReactImage.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -16,7 +17,9 @@ namespace react { namespace uwp { -struct ImageSource { +enum class ImageSourceType { Uri = 0, Download = 1, InlineData = 2 }; + +struct ReactImageSource { std::string uri; std::string method; std::string bundleRootPath; @@ -25,12 +28,13 @@ struct ImageSource { double height = 0; double scale = 1.0; bool packagerAsset = false; + ImageSourceType sourceType = ImageSourceType::Uri; }; struct ReactImage : winrt::Windows::UI::Xaml::Controls::GridT { using Super = winrt::Windows::UI::Xaml::Controls::GridT; - ReactImage(); + ReactImage() = default; public: static winrt::com_ptr Create(); @@ -43,29 +47,38 @@ struct ReactImage : winrt::Windows::UI::Xaml::Controls::GridT { void OnLoadEnd(winrt::event_token const &token) noexcept; // Public Properties - ImageSource Source() { + ReactImageSource Source() { return m_imageSource; } - winrt::fire_and_forget Source(ImageSource source); + void Source(ReactImageSource source); react::uwp::ResizeMode ResizeMode() { - return m_brush->ResizeMode(); - } - void ResizeMode(react::uwp::ResizeMode value) { - m_brush->ResizeMode(value); + return m_resizeMode; } + void ResizeMode(react::uwp::ResizeMode value); private: - ImageSource m_imageSource; - winrt::com_ptr m_brush; + winrt::Windows::UI::Xaml::Media::Stretch ResizeModeToStretch(react::uwp::ResizeMode value); + winrt::Windows::Foundation::IAsyncOperation + GetImageMemoryStreamAsync(ReactImageSource source); + winrt::fire_and_forget SetBackground(bool fireLoadEndEvent); + + bool m_useCompositionBrush{false}; + ReactImageSource m_imageSource; + react::uwp::ResizeMode m_resizeMode{ResizeMode::Contain}; + winrt::event> m_onLoadEndEvent; + winrt::Windows::UI::Xaml::FrameworkElement::SizeChanged_revoker m_sizeChangedRevoker; winrt::Windows::UI::Xaml::Media::LoadedImageSurface::LoadCompleted_revoker m_surfaceLoadedRevoker; + winrt::Windows::UI::Xaml::Media::Imaging::BitmapImage::ImageOpened_revoker m_bitmapImageOpened; + winrt::Windows::UI::Xaml::Media::ImageBrush::ImageOpened_revoker m_imageBrushOpenedRevoker; + winrt::Windows::UI::Xaml::Media::ImageBrush::ImageFailed_revoker m_imageBrushFailedRevoker; }; // Helper functions winrt::Windows::Foundation::IAsyncOperation -GetImageStreamAsync(ImageSource source); +GetImageStreamAsync(ReactImageSource source); winrt::Windows::Foundation::IAsyncOperation -GetImageInlineDataAsync(ImageSource source); +GetImageInlineDataAsync(ReactImageSource source); } // namespace uwp } // namespace react diff --git a/vnext/ReactUWP/Views/Image/ReactImageBrush.cpp b/vnext/ReactUWP/Views/Image/ReactImageBrush.cpp index ae030f63f3e..4a59db48ac1 100644 --- a/vnext/ReactUWP/Views/Image/ReactImageBrush.cpp +++ b/vnext/ReactUWP/Views/Image/ReactImageBrush.cpp @@ -71,15 +71,13 @@ void ReactImageBrush::UpdateCompositionBrush() { auto compositionBrush{surfaceBrush.as()}; if (ResizeMode() == ResizeMode::Repeat) { - // If ResizeMode is set to Repeat, then we need to use a - // CompositionEffectBrush. The CompositionSurfaceBrush holding the image - // is used as its source. + // If ResizeMode is set to Repeat, then we need to use a CompositionEffectBrush. + // The CompositionSurfaceBrush holding the image is used as its source. compositionBrush = GetOrCreateEffectBrush(surfaceBrush); } - // The CompositionBrush is only set after the image is first loaded, - // and anytime we switch between Surface and Effect brushes (to/from - // ResizeMode::Repeat) + // The CompositionBrush is only set after the image is first loaded and anytime + // we switch between Surface and Effect brushes (to/from ResizeMode::Repeat) if (CompositionBrush() != compositionBrush) { if (ResizeMode() == ResizeMode::Repeat) { surfaceBrush.HorizontalAlignmentRatio(0.0f); diff --git a/yarn.lock b/yarn.lock index 222def14e8d..a3549b7051f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10054,9 +10054,10 @@ react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== -"react-native@https://github.com/microsoft/react-native/archive/v0.60.0-microsoft.28.tar.gz": - version "0.60.0-microsoft.28" - resolved "https://github.com/microsoft/react-native/archive/v0.60.0-microsoft.28.tar.gz#fad0f343cf2ee7c277f89b86f770efa71872839e" +"react-native@https://github.com/microsoft/react-native/archive/v0.60.0-microsoft.31.tar.gz": + version "0.60.0-microsoft.31" + uid "26b4041e78b54517e3494beb6478bc7ee0a3a726" + resolved "https://github.com/microsoft/react-native/archive/v0.60.0-microsoft.31.tar.gz#26b4041e78b54517e3494beb6478bc7ee0a3a726" dependencies: "@babel/core" "^7.4.0" "@babel/generator" "^7.4.0"