Merge branch 'fjcaetano-feature/prefetch'

This commit is contained in:
Dylan Vann 2017-06-20 03:11:28 -04:00
commit 4e69ddd099
10 changed files with 244 additions and 90 deletions

View File

@ -1,8 +1,15 @@
import React, { PropTypes, Component } from 'react'
import { requireNativeComponent, Image, View } from 'react-native'
import {
requireNativeComponent,
Image,
NativeModules,
View,
} from 'react-native'
const resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource')
const FastImageViewNativeModule = NativeModules.FastImageView
class FastImage extends Component {
setNativeProps(nativeProps) {
this._root.setNativeProps(nativeProps)
@ -50,6 +57,10 @@ FastImage.priority = {
high: 'high',
}
FastImage.preload = sources => {
FastImageViewNativeModule.preload(sources)
}
const FastImageSourcePropType = PropTypes.shape({
uri: PropTypes.string,
headers: PropTypes.objectOf(PropTypes.string),

View File

@ -36,6 +36,7 @@ and
- [x] Aggressively cache images.
- [x] Add authorization headers.
- [x] Prioritize images.
- [x] Preload images.
- [x] GIF support.
## Usage
@ -63,13 +64,13 @@ const YourImage = () =>
## Properties
`source?: object`
### `source?: object`
Source for the remote image to load.
---
`source.uri?: string`
### `source.uri?: string`
Remote url to load the image from. e.g. `'https://facebook.github.io/react/img/logo_og.png'`.
@ -81,7 +82,7 @@ Headers to load the image with. e.g. `{ Authorization: 'someAuthToken' }`.
---
`source.priority?: enum`
### `source.priority?: enum`
- `FastImage.priority.low` - Low Priority
- `FastImage.priority.normal` **(Default)** - Normal Priority
@ -89,7 +90,7 @@ Headers to load the image with. e.g. `{ Authorization: 'someAuthToken' }`.
---
`resizeMode?: enum`
### `resizeMode?: enum`
- `FastImage.resizeMode.contain` **(Default)** - 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).
- `FastImage.resizeMode.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).
@ -98,16 +99,44 @@ Headers to load the image with. e.g. `{ Authorization: 'someAuthToken' }`.
---
`onLoad?: () => void`
### `onLoad?: () => void`
Called on a successful image fetch.
---
`onError?: () => void`
### `onError?: () => void`
Called on an image fetching error.
---
### `children`
`FastImage` does not currently support children.
Absolute positioning can be used as an alternative.
(This is because `FastImage` supplies a `android.widget.imageview` and not a `android.view.viewgroup`.)
## Static Methods
### `FastImage.preload: (source[]) => void`
Preload images to display later. e.g.
```js
FastImage.preload([
{
uri: 'https://facebook.github.io/react/img/logo_og.png',
headers: { Authorization: 'someAuthToken' },
},
{
uri: 'https://facebook.github.io/react/img/logo_og.png',
headers: { Authorization: 'someAuthToken' },
},
])
```
## Development
```bash

View File

@ -0,0 +1,71 @@
package com.dylanvann.fastimage;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.LazyHeaders;
import com.facebook.react.bridge.NoSuchKeyException;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import java.util.HashMap;
import java.util.Map;
class FastImageViewConverter {
static GlideUrl glideUrl(ReadableMap source) {
final String uriProp = source.getString("uri");
// Get the headers prop and add to glideUrl.
GlideUrl glideUrl;
try {
final ReadableMap headersMap = source.getMap("headers");
ReadableMapKeySetIterator headersIterator = headersMap.keySetIterator();
LazyHeaders.Builder headersBuilder = new LazyHeaders.Builder();
while (headersIterator.hasNextKey()) {
String key = headersIterator.nextKey();
String value = headersMap.getString(key);
headersBuilder.addHeader(key, value);
}
LazyHeaders headers = headersBuilder.build();
glideUrl = new GlideUrl(uriProp, headers);
} catch (NoSuchKeyException e) {
// If there is no headers object.
glideUrl = new GlideUrl(uriProp);
}
return glideUrl;
}
private static Map<String, Priority> REACT_PRIORITY_MAP =
new HashMap<String, Priority>() {{
put("low", Priority.LOW);
put("normal", Priority.NORMAL);
put("high", Priority.HIGH);
}};
static Priority priority(ReadableMap source) {
// Get the priority prop.
String priorityProp = "normal";
try {
priorityProp = source.getString("priority");
} catch (Exception e) {
// Noop.
}
final Priority priority = REACT_PRIORITY_MAP.get(priorityProp);
return priority;
}
private static Map<String, ImageView.ScaleType> REACT_RESIZE_MODE_MAP =
new HashMap<String, ImageView.ScaleType>() {{
put("contain", ScaleType.FIT_CENTER);
put("cover", ScaleType.CENTER_CROP);
put("stretch", ScaleType.FIT_XY);
put("center", ScaleType.CENTER);
}};
public static ScaleType scaleType(String resizeMode) {
if (resizeMode == null) resizeMode = "contain";
final ImageView.ScaleType scaleType = REACT_RESIZE_MODE_MAP.get(resizeMode);
return scaleType;
}
}

View File

@ -4,25 +4,17 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import com.bumptech.glide.DrawableRequestBuilder;
import com.bumptech.glide.DrawableTypeRequest;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Priority;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.LazyHeaders;
import com.bumptech.glide.load.model.stream.StreamModelLoader;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.signature.StringSignature;
import com.facebook.react.bridge.NoSuchKeyException;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.common.MapBuilder;
@ -33,9 +25,7 @@ import com.facebook.react.uimanager.events.RCTEventEmitter;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
@ -49,21 +39,6 @@ class FastImageViewManager extends SimpleViewManager<ImageView> {
private static Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
private static Map<String, Priority> REACT_PRIORITY_MAP =
new HashMap<String, Priority>() {{
put("low", Priority.LOW);
put("normal", Priority.NORMAL);
put("high", Priority.HIGH);
}};
private static Map<String, ImageView.ScaleType> REACT_RESIZE_MODE_MAP =
new HashMap<String, ImageView.ScaleType>() {{
put("contain", ScaleType.FIT_CENTER);
put("cover", ScaleType.CENTER_CROP);
put("stretch", ScaleType.FIT_XY);
put("center", ScaleType.CENTER);
}};
@Override
public String getName() {
return REACT_CLASS;
@ -125,51 +100,27 @@ class FastImageViewManager extends SimpleViewManager<ImageView> {
return;
}
final String uriProp = source.getString("uri");
// Get the GlideUrl which contains header info.
final GlideUrl glideUrl = FastImageViewConverter.glideUrl(source);
// Get the headers prop and add to glideUrl.
GlideUrl glideUrl;
try {
final ReadableMap headersMap = source.getMap("headers");
ReadableMapKeySetIterator headersIterator = headersMap.keySetIterator();
LazyHeaders.Builder headersBuilder = new LazyHeaders.Builder();
while (headersIterator.hasNextKey()) {
String key = headersIterator.nextKey();
String value = headersMap.getString(key);
headersBuilder.addHeader(key, value);
}
LazyHeaders headers = headersBuilder.build();
glideUrl = new GlideUrl(uriProp, headers);
} catch (NoSuchKeyException e) {
// If there is no headers object.
glideUrl = new GlideUrl(uriProp);
}
// Get the priority prop.
String priorityProp = "normal";
try {
priorityProp = source.getString("priority");
} catch (Exception e) {
// Noop.
}
final Priority priority = REACT_PRIORITY_MAP.get(priorityProp);
// Get priority.
final Priority priority = FastImageViewConverter.priority(source);
// Cancel existing request.
Glide.clear(view);
Glide
.with(view.getContext())
.load(glideUrl)
.priority(priority)
.placeholder(TRANSPARENT_DRAWABLE)
.listener(LISTENER)
.into(view);
.with(view.getContext())
.load(glideUrl)
.priority(priority)
.placeholder(TRANSPARENT_DRAWABLE)
.listener(LISTENER)
.into(view);
}
@ReactProp(name = "resizeMode")
public void setResizeMode(ImageView view, String resizeMode) {
if (resizeMode == null) resizeMode = "contain";
final ImageView.ScaleType scaleType = REACT_RESIZE_MODE_MAP.get(resizeMode);
final ImageView.ScaleType scaleType = FastImageViewConverter.scaleType(resizeMode);
view.setScaleType(scaleType);
}

View File

@ -0,0 +1,54 @@
package com.dylanvann.fastimage;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.model.GlideUrl;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
class FastImageViewModule extends ReactContextBaseJavaModule {
private static final String REACT_CLASS = "FastImageView";
FastImageViewModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return REACT_CLASS;
}
private static Drawable TRANSPARENT_DRAWABLE = new ColorDrawable(Color.TRANSPARENT);
@ReactMethod
public void preload(final ReadableArray sources) {
final Activity activity = getCurrentActivity();
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < sources.size(); i++) {
final ReadableMap source = sources.getMap(i);
final GlideUrl glideUrl = FastImageViewConverter.glideUrl(source);
final Priority priority = FastImageViewConverter.priority(source);
Glide
.with(activity.getApplicationContext())
.load(glideUrl)
.priority(priority)
.placeholder(TRANSPARENT_DRAWABLE)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.preload();
}
}
});
}
}

View File

@ -1,5 +1,7 @@
package com.dylanvann.fastimage;
import com.dylanvann.fastimage.FastImageViewModule;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.JavaScriptModule;
@ -10,10 +12,9 @@ import java.util.Collections;
import java.util.List;
public class FastImageViewPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
return Collections.<NativeModule>singletonList(new FastImageViewModule(reactContext));
}
@Override

View File

@ -8,24 +8,30 @@ import uuid from 'uuid/v4'
const getImageUrl = (id, width, height) =>
`https://source.unsplash.com/${id}/${width}x${height}`
let IMAGE_1
let IMAGE_2
let IMAGE_3
const IMAGE_SIZE = 150
// The server is used to test that sending headers is working correctly.
const USE_SERVER = false
const token = 'someToken'
if (USE_SERVER) {
const baseUrl = '192.168.2.11'
IMAGE_1 = `http://${baseUrl}:8080/pictures/ahmed-saffu-235616.jpg`
IMAGE_2 = `http://${baseUrl}:8080/pictures/alex-bertha-236361.jpg`
IMAGE_3 = `http://${baseUrl}:8080/pictures/jaromir-kavan-233699.jpg`
} else {
IMAGE_1 = getImageUrl('x58soEovG_M', IMAGE_SIZE, IMAGE_SIZE)
IMAGE_2 = getImageUrl('yPI7myL5eWY', IMAGE_SIZE, IMAGE_SIZE)
IMAGE_3 =
'https://cdn-images-1.medium.com/max/1600/1*-CY5bU4OqiJRox7G00sftw.gif'
const TOKEN = 'someToken'
const getImages = () => {
if (USE_SERVER) {
const baseUrl = '192.168.2.11'
return [
`http://${baseUrl}:8080/pictures/ahmed-saffu-235616.jpg`,
`http://${baseUrl}:8080/pictures/alex-bertha-236361.jpg`,
`http://${baseUrl}:8080/pictures/jaromir-kavan-233699.jpg`,
]
}
return [
getImageUrl('x58soEovG_M', IMAGE_SIZE, IMAGE_SIZE),
getImageUrl('yPI7myL5eWY', IMAGE_SIZE, IMAGE_SIZE),
'https://cdn-images-1.medium.com/max/1600/1*-CY5bU4OqiJRox7G00sftw.gif',
]
}
const images = getImages()
class FastImageExample extends Component {
componentDidMount() {
// Forcing an update every 5s to demonstrate loading.
@ -39,6 +45,17 @@ class FastImageExample extends Component {
const key = uuid()
// Busting image cache.
const bust = `?bust=${key}`
// Preload images.
FastImage.preload([
{
uri: 'https://facebook.github.io/react/img/logo_og.png',
headers: { Authorization: 'someAuthToken' },
},
{
uri: 'https://facebook.github.io/react/img/logo_og.png',
headers: { Authorization: 'someAuthToken' },
},
])
return (
<View style={styles.container} key={key}>
<StatusBar
@ -58,9 +75,9 @@ class FastImageExample extends Component {
<FastImage
style={styles.image}
source={{
uri: IMAGE_1 + bust,
uri: images[0] + bust,
headers: {
token,
token: TOKEN,
},
priority: FastImage.priority.low,
}}
@ -68,9 +85,9 @@ class FastImageExample extends Component {
<FastImage
style={styles.image}
source={{
uri: IMAGE_2 + bust,
uri: images[1] + bust,
headers: {
token,
token: TOKEN,
},
priority: FastImage.priority.normal,
}}
@ -78,9 +95,9 @@ class FastImageExample extends Component {
<FastImage
style={styles.image}
source={{
uri: IMAGE_3 + bust,
uri: images[2] + bust,
headers: {
token,
token: TOKEN,
},
priority: FastImage.priority.high,
}}

View File

@ -2,7 +2,7 @@
"name": "FastImage",
"version": "0.0.1",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"start": "react-native start",
"test": "jest"
},
"dependencies": {
@ -18,6 +18,7 @@
"babel-jest": "19.0.0",
"babel-preset-react-native": "1.9.1",
"jest": "19.0.2",
"react-native-cli": "^2.0.1",
"react-test-renderer": "16.0.0-alpha.6"
},
"private": true,

View File

@ -1,6 +1,8 @@
#import "FFFastImageViewManager.h"
#import "FFFastImageView.h"
#import <SDWebImage/SDWebImagePrefetcher.h>
@implementation FFFastImageViewManager
RCT_EXPORT_MODULE(FastImageView)
@ -17,4 +19,19 @@ RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode);
RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoad, RCTDirectEventBlock);
RCT_EXPORT_METHOD(preload:(nonnull NSArray<FFFastImageSource *> *)sources)
{
NSMutableArray *urls = [NSMutableArray arrayWithCapacity:sources.count];
[sources enumerateObjectsUsingBlock:^(FFFastImageSource * _Nonnull source, NSUInteger idx, BOOL * _Nonnull stop) {
[source.headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString* header, BOOL *stop) {
[[SDWebImageDownloader sharedDownloader] setValue:header forHTTPHeaderField:key];
}];
[urls setObject:source.uri atIndexedSubscript:idx];
}];
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:urls];
}
@end

View File

@ -40,4 +40,6 @@ RCT_ENUM_CONVERTER(FFFPriority, (@{
return imageSource;
}
RCT_ARRAY_CONVERTER(FFFastImageSource);
@end