Skip to content
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

fix(ImageLoader): Adds prefetch cancellation and cache query #996

Merged
merged 3 commits into from
Feb 10, 2017
Merged
Show file tree
Hide file tree
Changes from 2 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
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();
}
}
}
2 changes: 1 addition & 1 deletion ReactWindows/ReactNative/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down