Skip to content

Commit

Permalink
fix(ImageLoader): Adds prefetch cancellation and cache query (#996)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rozele committed Feb 10, 2017
1 parent e18704d commit d07bd2d
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 16 deletions.
86 changes: 74 additions & 12 deletions Libraries/Image/Image.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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++;
}

/**
* <Image> - A react component for displaying different types of images,
* including network images, static resources, temporary local images, and
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
*/
Expand All @@ -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: {
Expand All @@ -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<string>): Promise<Map<string, 'memory' | 'disk'>> {
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: '<http location || file path>' }
*/
resolveAssetSource: resolveAssetSource,
},

mixins: [NativeMethodsMixin],
Expand Down Expand Up @@ -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 (
<View style={nativeProps.style}>
<View style={containerStyle}>
<RKImage {...imageProps}/>
{this.props.children}
</View>
);
} else {
return <RKImage {...nativeProps}/>;
return <RKImage {...nativeProps}/>;
}
}
return null;
Expand All @@ -257,9 +322,6 @@ var cfg = {
nativeOnly: {
src: true,
loadingIndicatorSrc: true,
defaultImageSrc: true,
imageTag: true,
progressHandlerRegistered: true,
shouldNotifyLoadEvents: true,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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)
{
Expand Down Expand Up @@ -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.");
}
}
}
41 changes: 38 additions & 3 deletions ReactWindows/ReactNative/Modules/Image/ImageLoaderModule.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<int> _prefetchRequests = new TaskCancellationManager<int>();

public override string Name
{
get
Expand All @@ -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))
{
Expand All @@ -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)
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)).ConfigureAwait(false);
if (file != null)
{
result.Add(url, "disk");
}
}

promise.Resolve(result);
}

public override void OnReactInstanceDispose()
{
_prefetchRequests.CancelAllTasks();
}
}
}

0 comments on commit d07bd2d

Please sign in to comment.