From b7547aec5297fe5bdee9c30d65eab6b188c53ef6 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Thu, 9 Feb 2017 17:58:08 -0500 Subject: [PATCH 1/2] fix(ImageLoader): Adds prefetch cancellation and cache query Recent update to the UWP Community Toolkit allows us to cancel prefetch operations. This changeset also adds the ability to query for images on disk, but not yet in-memory. --- Libraries/Image/Image.windows.js | 86 ++++++++++++++++--- .../Modules/Image/ImageLoaderModule.cs | 41 ++++++++- ReactWindows/ReactNative/project.json | 2 +- 3 files changed, 113 insertions(+), 16 deletions(-) diff --git a/Libraries/Image/Image.windows.js b/Libraries/Image/Image.windows.js index 4247a8f2397..2c31964288d 100644 --- a/Libraries/Image/Image.windows.js +++ b/Libraries/Image/Image.windows.js @@ -11,11 +11,11 @@ */ 'use strict'; -const NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeMethodsMixin = require('NativeMethodsMixin'); var NativeModules = require('NativeModules'); var ImageResizeMode = require('ImageResizeMode'); var ImageStylePropTypes = require('ImageStylePropTypes'); -var PropTypes = require('react/lib/ReactPropTypes'); +var ViewStylePropTypes = require('ViewStylePropTypes'); var React = require('React'); var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); var StyleSheet = require('StyleSheet'); @@ -26,11 +26,19 @@ var flattenStyle = require('flattenStyle'); var merge = require('merge'); var requireNativeComponent = require('requireNativeComponent'); var resolveAssetSource = require('resolveAssetSource'); +var Set = require('Set'); +var filterObject = require('fbjs/lib/filterObject'); +var PropTypes = React.PropTypes; var { ImageLoader, } = NativeModules; +let _requestId = 1; +function generateRequestId() { + return _requestId++; +} + /** * - A react component for displaying different types of images, * including network images, static resources, temporary local images, and @@ -63,6 +71,9 @@ var ImageViewAttributes = merge(ReactNativeViewAttributes.UIView, { shouldNotifyLoadEvents: true, }); +var ViewStyleKeys = new Set(Object.keys(ViewStylePropTypes)); +var ImageSpecificStyleKeys = new Set(Object.keys(ImageStylePropTypes).filter(x => !ViewStyleKeys.has(x))); + var Image = React.createClass({ propTypes: { ...View.propTypes, @@ -107,6 +118,10 @@ var Image = React.createClass({ * Invoked on load start */ onLoadStart: PropTypes.func, + /** + * Invoked on load error + */ + onError: PropTypes.func, /** * Invoked when load completes successfully */ @@ -119,6 +134,26 @@ var Image = React.createClass({ * Used to locate this view in end-to-end tests. */ testID: PropTypes.string, + /** + * Determines how to resize the image when the frame doesn't match the raw + * image dimensions. + * + * 'cover': Scale the image uniformly (maintain the image's aspect ratio) + * so that both dimensions (width and height) of the image will be equal + * to or larger than the corresponding dimension of the view (minus padding). + * + * 'contain': Scale the image uniformly (maintain the image's aspect ratio) + * so that both dimensions (width and height) of the image will be equal to + * or less than the corresponding dimension of the view (minus padding). + * + * 'stretch': Scale width and height independently, This may change the + * aspect ratio of the src. + * + * 'center': Scale the image down so that it is completely visible, + * if bigger than the area of the view. + * The image will not be scaled up. + */ + resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch', 'center']), }, statics: { @@ -142,9 +177,36 @@ var Image = React.createClass({ * Prefetches a remote image for later use by downloading it to the disk * cache */ - prefetch(url: string) { - return ImageLoader.prefetchImage(url); + prefetch(url: string, callback: ?Function) { + const requestId = generateRequestId(); + callback && callback(requestId); + return ImageLoader.prefetchImage(url, requestId); + }, + + /** + * Abort prefetch request + */ + abortPrefetch(requestId: number) { + ImageLoader.abortRequest(requestId); + }, + + /** + * Perform cache interrogation. + * + * @param urls the list of image URLs to check the cache for. + * @return a mapping from url to cache status, such as "disk" or "memory". If a requested URL is + * not in the mapping, it means it's not in the cache. + */ + async queryCache(urls: Array): Promise> { + return await ImageLoader.queryCache(urls); }, + + /** + * Resolves an asset reference into an object which has the properties `uri`, `width`, + * and `height`. The input may either be a number (opaque type returned by + * require('./foo.png')) or an `ImageSource` like { uri: '' } + */ + resolveAssetSource: resolveAssetSource, }, mixins: [NativeMethodsMixin], @@ -212,28 +274,31 @@ var Image = React.createClass({ sources = source; } - const {onLoadStart, onLoad, onLoadEnd} = this.props; + const {onLoadStart, onLoad, onLoadEnd, onError} = this.props; const nativeProps = merge(this.props, { style, - shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd), + shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError), src: sources, loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null, }); if (nativeProps.children) { // TODO(6033040): Consider implementing this as a separate native component + const containerStyle = filterObject(style, (val, key) => !ImageSpecificStyleKeys.has(key)); + const imageStyle = filterObject(style, (val, key) => ImageSpecificStyleKeys.has(key)); const imageProps = merge(nativeProps, { - style: styles.absoluteImage, + style: [imageStyle, styles.absoluteImage], children: undefined, }); + return ( - + {this.props.children} ); } else { - return ; + return ; } } return null; @@ -257,9 +322,6 @@ var cfg = { nativeOnly: { src: true, loadingIndicatorSrc: true, - defaultImageSrc: true, - imageTag: true, - progressHandlerRegistered: true, shouldNotifyLoadEvents: true, }, }; diff --git a/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs b/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs index f3f139aadf3..5f7ae43bd62 100644 --- a/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs +++ b/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs @@ -1,6 +1,7 @@ using Microsoft.Toolkit.Uwp.UI; using Newtonsoft.Json.Linq; using ReactNative.Bridge; +using ReactNative.Modules.Network; using System; using System.Reactive.Linq; using Windows.UI.Xaml.Media.Imaging; @@ -13,6 +14,8 @@ class ImageLoaderModule : NativeModuleBase private const string ErrorPrefetchFailure = "E_PREFETCH_FAILURE"; private const string ErrorGetSizeFailure = "E_GET_SIZE_FAILURE"; + private readonly TaskCancellationManager _prefetchRequests = new TaskCancellationManager(); + public override string Name { get @@ -22,7 +25,7 @@ public override string Name } [ReactMethod] - public void prefetchImage(string uriString, IPromise promise) + public void prefetchImage(string uriString, int requestId, IPromise promise) { if (string.IsNullOrEmpty(uriString)) { @@ -34,9 +37,13 @@ public void prefetchImage(string uriString, IPromise promise) { try { - // TODO: enable prefetch cancellation var uri = new Uri(uriString); - await ImageCache.Instance.PreCacheAsync(uri, true, true).ConfigureAwait(false); + + await _prefetchRequests.AddAndInvokeAsync( + requestId, + async token => await ImageCache.Instance.PreCacheAsync(uri, true, true, token).ConfigureAwait(false)) + .ConfigureAwait(false); + promise.Resolve(true); } catch (Exception ex) @@ -46,6 +53,12 @@ public void prefetchImage(string uriString, IPromise promise) }); } + [ReactMethod] + public void abortRequest(int requestId) + { + _prefetchRequests.Cancel(requestId); + } + [ReactMethod] public void getSize(string uriString, IPromise promise) { @@ -99,5 +112,27 @@ public void getSize(string uriString, IPromise promise) } }); } + + [ReactMethod] + public async void queryCache(string[] urls, IPromise promise) + { + // TODO: support query for items in memory + var result = new JObject(); + foreach (var url in urls) + { + var file = await ImageCache.Instance.GetFileFromCacheAsync(new Uri(url)); + if (file != null) + { + result.Add(url, "disk"); + } + } + + promise.Resolve(result); + } + + public override void OnReactInstanceDispose() + { + _prefetchRequests.CancelAllTasks(); + } } } diff --git a/ReactWindows/ReactNative/project.json b/ReactWindows/ReactNative/project.json index 7e77a05f570..1c5b5c50cda 100644 --- a/ReactWindows/ReactNative/project.json +++ b/ReactWindows/ReactNative/project.json @@ -2,7 +2,7 @@ "dependencies": { "Facebook.Yoga": "1.0.1-pre", "Microsoft.NETCore.UniversalWindowsPlatform": "5.2.2", - "Microsoft.Toolkit.Uwp.UI": "1.2.0", + "Microsoft.Toolkit.Uwp.UI": "1.3.1", "Newtonsoft.Json": "9.0.1", "PCLStorage": "1.0.2", "System.Reactive": "3.0.0", From 4e67dc60ab2dcb1aa9f4b3bb9075c8bc891d2f66 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Thu, 9 Feb 2017 18:18:05 -0500 Subject: [PATCH 2/2] Stubbing methods in .NET 4.6 --- .../Modules/Image/ImageLoaderModule.cs | 15 ++++++++++++++- .../Modules/Image/ImageLoaderModule.cs | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs b/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs index 44ecbf680f1..cfb9a2612b0 100644 --- a/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs +++ b/ReactWindows/ReactNative.Net46/Modules/Image/ImageLoaderModule.cs @@ -11,6 +11,7 @@ class ImageLoaderModule : NativeModuleBase private const string ErrorInvalidUri = "E_INVALID_URI"; private const string ErrorPrefetchFailure = "E_PREFETCH_FAILURE"; private const string ErrorGetSizeFailure = "E_GET_SIZE_FAILURE"; + private const string ErrorQueryCacheFailure = "E_QUERY_CACHE_FAILURE"; public override string Name { @@ -21,11 +22,17 @@ public override string Name } [ReactMethod] - public void prefetchImage(string uriString, IPromise promise) + public void prefetchImage(string uriString, int requestId, IPromise promise) { promise.Reject(ErrorPrefetchFailure, "Prefetch is not yet implemented."); } + [ReactMethod] + public void abortRequest(int requestId) + { + // No op as prefetch is not yet implemented. + } + [ReactMethod] public void getSize(string uriString, IPromise promise) { @@ -69,5 +76,11 @@ public void getSize(string uriString, IPromise promise) } }); } + + [ReactMethod] + public void queryCache(string[] urls, IPromise promise) + { + promise.Reject(ErrorQueryCacheFailure, "Prefetch is not yet implemented."); + } } } diff --git a/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs b/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs index 5f7ae43bd62..4f231f237f6 100644 --- a/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs +++ b/ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs @@ -120,7 +120,7 @@ public async void queryCache(string[] urls, IPromise promise) var result = new JObject(); foreach (var url in urls) { - var file = await ImageCache.Instance.GetFileFromCacheAsync(new Uri(url)); + var file = await ImageCache.Instance.GetFileFromCacheAsync(new Uri(url)).ConfigureAwait(false); if (file != null) { result.Add(url, "disk");