react-native-fast-image 8.18.2 → 8.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -2
- package/android/src/main/java/com/dylanvann/fastimage/FastImageCacheLimits.java +101 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageCacheWrite.java +111 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageGlideModule.java +14 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageKeyedGlideUrl.java +7 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +4 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java +10 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +207 -81
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewPackage.java +2 -0
- package/app.plugin.js +62 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.flow +18 -0
- package/dist/index.d.ts +31 -1
- package/dist/index.js +2 -0
- package/dist/index.js.flow +18 -0
- package/ios/FastImage/FFFastImageSource.h +6 -3
- package/ios/FastImage/FFFastImageSource.m +30 -0
- package/ios/FastImage/FFFastImageView.m +37 -39
- package/ios/FastImage/FFFastImageViewManager.m +294 -79
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -125,8 +125,10 @@ Indicates the load order priority of an image. Images with `FastImage.priority.h
|
|
|
125
125
|
|
|
126
126
|
### `source.cache?: enum`
|
|
127
127
|
|
|
128
|
+
How fresh the image must be. See [how caching is handled](docs/how-is-caching-handled.md) for how the options fit together.
|
|
129
|
+
|
|
128
130
|
- `FastImage.cacheControl.immutable` - **(Default)** - Only updates if url changes.
|
|
129
|
-
- `FastImage.cacheControl.web` - Use headers and follow normal caching procedures.
|
|
131
|
+
- `FastImage.cacheControl.web` - Use headers and follow normal caching procedures. These responses are kept in their own HTTP cache (50 MB on each platform), which `clearDiskCache` also clears.
|
|
130
132
|
- `FastImage.cacheControl.cacheOnly` - Only show images from cache, do not make any network requests.
|
|
131
133
|
|
|
132
134
|
---
|
|
@@ -367,7 +369,7 @@ if (result.ok) {
|
|
|
367
369
|
}
|
|
368
370
|
```
|
|
369
371
|
|
|
370
|
-
With `cache: 'cacheOnly'` it doesn't download: use it to check whether an image is cached.
|
|
372
|
+
With `cache: 'cacheOnly'` it doesn't download: use it to check whether an image is cached. A source with a `cacheKey` and no `uri` is only looked up too (there's no url to download from), e.g. for an image whose signed url has expired.
|
|
371
373
|
|
|
372
374
|
```js
|
|
373
375
|
const { ok } = await FastImage.getCachePath({
|
|
@@ -384,6 +386,69 @@ const { ok } = await FastImage.getCachePath({
|
|
|
384
386
|
|
|
385
387
|
There's no way to remove a single image from the cache. To load an image again after it changed on the server, change its [`cacheKey`](#sourcecachekey-string).
|
|
386
388
|
|
|
389
|
+
### `FastImage.writeToCache: (source: Source, file: string) => Promise<CachePathResult>`
|
|
390
|
+
|
|
391
|
+
Stores a local image file as the source's image in the disk cache, so views and preloads of the source show it without downloading it. For example, after a user uploads a new avatar, store the photo they picked under the avatar's new url or [`cacheKey`](#sourcecachekey-string), and it shows at once. `file` is a `file://` uri or a path (or on Android a `content://` uri, as image pickers often return). A source with a `cacheKey` doesn't need a `uri`, so the image can be stored before its url is known. Resolves with `{ ok: true, path }` (the cached file) or `{ ok: false, error }`. Never rejects.
|
|
392
|
+
|
|
393
|
+
```js
|
|
394
|
+
const result = await FastImage.writeToCache(
|
|
395
|
+
{
|
|
396
|
+
uri: user.avatarUrl,
|
|
397
|
+
cacheKey: `avatar-${user.id}-${user.avatarUpdatedAt}`,
|
|
398
|
+
},
|
|
399
|
+
pickedPhoto.uri,
|
|
400
|
+
)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
- The file must be an image.
|
|
404
|
+
- It doesn't replace an image that's already cached under the source's key: give a new image a new `cacheKey` (Glide can't replace one on Android).
|
|
405
|
+
- Not for `cache: 'web'` sources, which are kept in an HTTP cache.
|
|
406
|
+
- Like any cached image, it can be removed from the cache later, and then it's downloaded from the source's url.
|
|
407
|
+
|
|
408
|
+
### `FastImage.configureCache: (limits?: CacheLimits) => Promise<CacheState>`
|
|
409
|
+
|
|
410
|
+
How much the image cache keeps. Set the limits your app starts with in its native config, so they're in effect from the first image, and change them while the app runs with `configureCache`, e.g. from a storage setting. Runtime changes are saved, and used on the next launches too.
|
|
411
|
+
|
|
412
|
+
| Limit | iOS | Android |
|
|
413
|
+
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
|
414
|
+
| `maxDiskSize`: the most bytes of images kept on disk. When it's over, the least recently used images are removed. | Changes apply at once (the cache is trimmed to half the limit). Default: no limit. | Changes apply from the next launch (Glide's disk cache size is set when it starts). Default: 250 MB. |
|
|
415
|
+
| `maxDiskAge`: seconds an image is kept on disk after it was last used. | Default: 1 week (counted from when it was stored before SDWebImage 5.21, unless it's set). | No age limit. |
|
|
416
|
+
| `maxMemorySize`: the most bytes of decoded images kept in memory. | Default: no limit (they're removed when the system is low on memory). | Sized from the screen by Glide. |
|
|
417
|
+
|
|
418
|
+
`0` means no limit.
|
|
419
|
+
|
|
420
|
+
**Starting limits.** With Expo, in `app.json`:
|
|
421
|
+
|
|
422
|
+
```json
|
|
423
|
+
"plugins": [["react-native-fast-image", { "maxDiskSize": 209715200, "maxDiskAge": 2592000 }]]
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Without Expo, in `ios/<App>/Info.plist`:
|
|
427
|
+
|
|
428
|
+
```xml
|
|
429
|
+
<key>FastImageMaxDiskSize</key>
|
|
430
|
+
<integer>209715200</integer>
|
|
431
|
+
<key>FastImageMaxDiskAge</key>
|
|
432
|
+
<integer>2592000</integer>
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
and in `android/app/src/main/AndroidManifest.xml`, inside `<application>`:
|
|
436
|
+
|
|
437
|
+
```xml
|
|
438
|
+
<meta-data android:name="fastimage.MAX_DISK_SIZE" android:value="209715200" />
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Changing them at runtime.** Pass the limits to change; `null` goes back to the native config's (or the default). It resolves with the limits in effect and `diskSize`, the bytes the image disk cache uses now. Call it without limits to see them.
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
await FastImage.configureCache({ maxDiskSize: 500 * 1024 * 1024 })
|
|
445
|
+
const { maxDiskSize, diskSize } = await FastImage.configureCache()
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
On Android, if your app has its own `AppGlideModule` (see [Are you using Glide already](docs/app-glide-module.md)), set the disk cache size there instead: `maxDiskSize` isn't applied or reported.
|
|
449
|
+
|
|
450
|
+
Images with `cache: 'web'` are kept in their own HTTP cache instead, up to 50 MB on each platform, which these limits don't change.
|
|
451
|
+
|
|
387
452
|
## Troubleshooting
|
|
388
453
|
|
|
389
454
|
If you have any problems using this library try the steps in [troubleshooting](docs/troubleshooting.md) and see if they fix it.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.SharedPreferences;
|
|
5
|
+
import android.content.pm.ApplicationInfo;
|
|
6
|
+
import android.content.pm.PackageManager;
|
|
7
|
+
import android.os.Bundle;
|
|
8
|
+
|
|
9
|
+
import androidx.annotation.Nullable;
|
|
10
|
+
|
|
11
|
+
import com.bumptech.glide.load.engine.cache.DiskCache;
|
|
12
|
+
|
|
13
|
+
import java.io.File;
|
|
14
|
+
|
|
15
|
+
// configureCache's maxDiskSize on Android. Glide's disk cache size can only be
|
|
16
|
+
// set when Glide starts, so FastImageGlideModule applies it then: the one
|
|
17
|
+
// saved by configureCache at runtime, or the app's manifest's
|
|
18
|
+
// (fastimage.MAX_DISK_SIZE meta-data), or Glide's default.
|
|
19
|
+
final class FastImageCacheLimits {
|
|
20
|
+
private static final String PREFERENCES = "fast-image-cache-limits";
|
|
21
|
+
private static final String MAX_DISK_SIZE = "maxDiskSize";
|
|
22
|
+
private static final String MANIFEST_MAX_DISK_SIZE = "fastimage.MAX_DISK_SIZE";
|
|
23
|
+
|
|
24
|
+
// The disk cache size Glide started with, in bytes (0 for no limit), or
|
|
25
|
+
// -1 if FastImageGlideModule hasn't started it (not yet, or the app has
|
|
26
|
+
// its own AppGlideModule instead).
|
|
27
|
+
static volatile long startedMaxDiskSize = -1;
|
|
28
|
+
|
|
29
|
+
private FastImageCacheLimits() {
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Android loads the file on its own thread the first time; reading it
|
|
33
|
+
// waits for that. FastImageViewModule gets it early, off the main thread,
|
|
34
|
+
// so Glide doesn't wait on the disk when it starts.
|
|
35
|
+
static SharedPreferences preferences(Context context) {
|
|
36
|
+
return context.getApplicationContext().getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Starts loading the preferences file on a background thread, when
|
|
40
|
+
// FastImage's package creates its module and view manager (before any
|
|
41
|
+
// image loads).
|
|
42
|
+
static void loadInBackground(final Context context) {
|
|
43
|
+
new Thread(new Runnable() {
|
|
44
|
+
@Override
|
|
45
|
+
public void run() {
|
|
46
|
+
preferences(context);
|
|
47
|
+
}
|
|
48
|
+
}, "FastImageCacheLimits").start();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Saves a runtime change (bytes, 0 for no limit), or with null removes it.
|
|
52
|
+
static void saveMaxDiskSize(Context context, @Nullable Long maxDiskSize) {
|
|
53
|
+
SharedPreferences.Editor editor = preferences(context).edit();
|
|
54
|
+
if (maxDiskSize == null) {
|
|
55
|
+
editor.remove(MAX_DISK_SIZE);
|
|
56
|
+
} else {
|
|
57
|
+
editor.putLong(MAX_DISK_SIZE, maxDiskSize);
|
|
58
|
+
}
|
|
59
|
+
editor.commit();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The size to start Glide with: saved, or the manifest's, or Glide's
|
|
63
|
+
// default. 0 for no limit.
|
|
64
|
+
static long maxDiskSize(Context context) {
|
|
65
|
+
SharedPreferences preferences = preferences(context);
|
|
66
|
+
if (preferences.contains(MAX_DISK_SIZE)) {
|
|
67
|
+
return Math.max(preferences.getLong(MAX_DISK_SIZE, 0), 0);
|
|
68
|
+
}
|
|
69
|
+
Long manifest = manifestMaxDiskSize(context);
|
|
70
|
+
return manifest != null ? Math.max(manifest, 0) : DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Nullable
|
|
74
|
+
private static Long manifestMaxDiskSize(Context context) {
|
|
75
|
+
try {
|
|
76
|
+
ApplicationInfo info = context.getPackageManager()
|
|
77
|
+
.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
|
|
78
|
+
Bundle metaData = info.metaData;
|
|
79
|
+
Object value = metaData == null ? null : metaData.get(MANIFEST_MAX_DISK_SIZE);
|
|
80
|
+
if (value instanceof Number) return ((Number) value).longValue();
|
|
81
|
+
if (value instanceof String) return Long.parseLong(((String) value).trim());
|
|
82
|
+
} catch (PackageManager.NameNotFoundException | NumberFormatException e) {
|
|
83
|
+
// None.
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The bytes the disk cache (in Glide's default folder, where
|
|
89
|
+
// FastImageGlideModule puts it) uses now.
|
|
90
|
+
static long diskSize(Context context) {
|
|
91
|
+
return size(new File(context.getCacheDir(), DiskCache.Factory.DEFAULT_DISK_CACHE_DIR));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private static long size(File file) {
|
|
95
|
+
File[] files = file.listFiles();
|
|
96
|
+
if (files == null) return file.isFile() ? file.length() : 0;
|
|
97
|
+
long size = 0;
|
|
98
|
+
for (File child : files) size += size(child);
|
|
99
|
+
return size;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.net.Uri;
|
|
5
|
+
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
|
|
8
|
+
import com.bumptech.glide.Priority;
|
|
9
|
+
import com.bumptech.glide.load.DataSource;
|
|
10
|
+
import com.bumptech.glide.load.Options;
|
|
11
|
+
import com.bumptech.glide.load.data.DataFetcher;
|
|
12
|
+
import com.bumptech.glide.load.model.GlideUrl;
|
|
13
|
+
import com.bumptech.glide.load.model.ModelLoader;
|
|
14
|
+
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
|
15
|
+
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
|
16
|
+
|
|
17
|
+
import java.io.IOException;
|
|
18
|
+
import java.io.InputStream;
|
|
19
|
+
|
|
20
|
+
// What writeToCache loads: a local file's bytes (file:// or content://), with
|
|
21
|
+
// a source's GlideUrl as their key, so Glide stores them in its disk cache
|
|
22
|
+
// where loads of the source find them (the disk key is the url's cache key:
|
|
23
|
+
// the source's cacheKey or url).
|
|
24
|
+
final class FastImageCacheWrite {
|
|
25
|
+
final GlideUrl key;
|
|
26
|
+
final Uri file;
|
|
27
|
+
|
|
28
|
+
FastImageCacheWrite(GlideUrl key, Uri file) {
|
|
29
|
+
this.key = key;
|
|
30
|
+
this.file = file;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static final class LoaderFactory implements ModelLoaderFactory<FastImageCacheWrite, InputStream> {
|
|
34
|
+
private final Context context;
|
|
35
|
+
|
|
36
|
+
LoaderFactory(Context context) {
|
|
37
|
+
this.context = context;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@NonNull
|
|
41
|
+
@Override
|
|
42
|
+
public ModelLoader<FastImageCacheWrite, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
|
43
|
+
return new ModelLoader<FastImageCacheWrite, InputStream>() {
|
|
44
|
+
@Override
|
|
45
|
+
public LoadData<InputStream> buildLoadData(@NonNull FastImageCacheWrite model, int width, int height, @NonNull Options options) {
|
|
46
|
+
return new LoadData<>(model.key, new Fetcher(context, model.file));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Override
|
|
50
|
+
public boolean handles(@NonNull FastImageCacheWrite model) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Override
|
|
57
|
+
public void teardown() {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private static final class Fetcher implements DataFetcher<InputStream> {
|
|
62
|
+
private final Context context;
|
|
63
|
+
private final Uri file;
|
|
64
|
+
private InputStream stream;
|
|
65
|
+
|
|
66
|
+
Fetcher(Context context, Uri file) {
|
|
67
|
+
this.context = context;
|
|
68
|
+
this.file = file;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Override
|
|
72
|
+
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
|
73
|
+
try {
|
|
74
|
+
stream = context.getContentResolver().openInputStream(file);
|
|
75
|
+
if (stream == null) throw new IOException("Can't read " + file);
|
|
76
|
+
callback.onDataReady(stream);
|
|
77
|
+
} catch (IOException | SecurityException e) {
|
|
78
|
+
callback.onLoadFailed(e);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Override
|
|
83
|
+
public void cleanup() {
|
|
84
|
+
if (stream == null) return;
|
|
85
|
+
try {
|
|
86
|
+
stream.close();
|
|
87
|
+
} catch (IOException e) {
|
|
88
|
+
// Closed as far as it could.
|
|
89
|
+
}
|
|
90
|
+
stream = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Override
|
|
94
|
+
public void cancel() {
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@NonNull
|
|
98
|
+
@Override
|
|
99
|
+
public Class<InputStream> getDataClass() {
|
|
100
|
+
return InputStream.class;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Not DATA_DISK_CACHE or MEMORY_CACHE, which DiskCacheStrategy.DATA
|
|
104
|
+
// doesn't store.
|
|
105
|
+
@NonNull
|
|
106
|
+
@Override
|
|
107
|
+
public DataSource getDataSource() {
|
|
108
|
+
return DataSource.LOCAL;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
package com.dylanvann.fastimage;
|
|
2
2
|
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
|
|
5
|
+
import androidx.annotation.NonNull;
|
|
6
|
+
|
|
7
|
+
import com.bumptech.glide.GlideBuilder;
|
|
3
8
|
import com.bumptech.glide.annotation.GlideModule;
|
|
9
|
+
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
|
4
10
|
import com.bumptech.glide.module.AppGlideModule;
|
|
5
11
|
|
|
6
12
|
// We need an AppGlideModule to be present for progress events to work.
|
|
7
13
|
@GlideModule
|
|
8
14
|
public final class FastImageGlideModule extends AppGlideModule {
|
|
15
|
+
// The app's disk cache size (saved by configureCache, or in its manifest;
|
|
16
|
+
// FastImageCacheLimits), in Glide's default folder.
|
|
17
|
+
@Override
|
|
18
|
+
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
|
|
19
|
+
long maxDiskSize = FastImageCacheLimits.maxDiskSize(context);
|
|
20
|
+
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, maxDiskSize == 0 ? Long.MAX_VALUE : maxDiskSize));
|
|
21
|
+
FastImageCacheLimits.startedMaxDiskSize = maxDiskSize;
|
|
22
|
+
}
|
|
9
23
|
}
|
|
@@ -19,6 +19,13 @@ class FastImageKeyedGlideUrl extends GlideUrl {
|
|
|
19
19
|
mCacheKey = cacheKey;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// For a source with a cacheKey and no uri (getCachePath, writeToCache):
|
|
23
|
+
// GlideUrl needs a url, but only the key is used for the caches, and it's
|
|
24
|
+
// never loaded from the network.
|
|
25
|
+
static FastImageKeyedGlideUrl forKey(@NonNull String cacheKey) {
|
|
26
|
+
return new FastImageKeyedGlideUrl("fast-image-cache-key:" + cacheKey, Headers.DEFAULT, cacheKey);
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
@Override
|
|
23
30
|
public String getCacheKey() {
|
|
24
31
|
return mCacheKey;
|
package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java
CHANGED
|
@@ -130,6 +130,10 @@ public class FastImageOkHttpProgressGlideModule extends LibraryGlideModule {
|
|
|
130
130
|
webCache = new Cache(new File(context.getCacheDir(), "fast-image-http-cache"), WEB_CACHE_SIZE);
|
|
131
131
|
webClient = client.newBuilder().cache(webCache).build();
|
|
132
132
|
registry.prepend(FastImageWebGlideUrl.class, InputStream.class, new WebUrlLoaderFactory(webClient));
|
|
133
|
+
|
|
134
|
+
// writeToCache: local files stored under a source's key.
|
|
135
|
+
registry.prepend(FastImageCacheWrite.class, InputStream.class,
|
|
136
|
+
new FastImageCacheWrite.LoaderFactory(context.getApplicationContext()));
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
// Loads GlideUrls with the given client, except `web` ones
|
|
@@ -138,6 +138,16 @@ class FastImageViewConverter {
|
|
|
138
138
|
return options;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// The source's cacheKey, or null without one.
|
|
142
|
+
@Nullable
|
|
143
|
+
static String getCacheKey(@Nullable ReadableMap source) {
|
|
144
|
+
if (source == null || !source.hasKey("cacheKey") || source.getType("cacheKey") != ReadableType.String) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
String cacheKey = source.getString("cacheKey");
|
|
148
|
+
return cacheKey == null || cacheKey.isEmpty() ? null : cacheKey;
|
|
149
|
+
}
|
|
150
|
+
|
|
141
151
|
static FastImageCacheControl getCacheControl(ReadableMap source) {
|
|
142
152
|
return getValueFromSource("cache", "immutable", FAST_IMAGE_CACHE_CONTROL_MAP, source);
|
|
143
153
|
}
|