Merge branch 'fjcaetano-feature/prefetch'
This commit is contained in:
commit
4e69ddd099
13
FastImage.js
13
FastImage.js
|
@ -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),
|
||||
|
|
41
README.md
41
README.md
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -40,4 +40,6 @@ RCT_ENUM_CONVERTER(FFFPriority, (@{
|
|||
return imageSource;
|
||||
}
|
||||
|
||||
RCT_ARRAY_CONVERTER(FFFastImageSource);
|
||||
|
||||
@end
|
||||
|
|
Loading…
Reference in New Issue