react-native-fast-image 8.29.0 → 8.30.0
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 +12 -19
- package/RNFastImage.podspec +3 -0
- package/android/build.gradle +23 -3
- package/android/consumer-rules.pro +9 -5
- package/android/src/glide-4.15/java/com/dylanvann/fastimage/FastImageAnimated.java +244 -0
- package/android/src/glide-older/java/com/dylanvann/fastimage/FastImageAnimated.java +36 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageAnimatable.java +13 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageApng.java +141 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageApngRenderer.java +141 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageOkHttpProgressGlideModule.java +6 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSharedDownloads.java +292 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewModule.java +27 -5
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +93 -0
- package/dist/index.d.ts +9 -6
- package/ios/FastImage/FFFastImageSource.m +6 -6
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -115,22 +115,9 @@ Remote url to load the image from. e.g. `'https://example.com/image.jpg'`.
|
|
|
115
115
|
|
|
116
116
|
#### Photo library images (iOS)
|
|
117
117
|
|
|
118
|
-
A photo library url (`ph://<localIdentifier>`, as camera roll libraries give) loads
|
|
118
|
+
A photo library url (`ph://<localIdentifier>`, as camera roll libraries give) loads with SDWebImagePhotosPlugin, which FastImage includes on iOS.
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
pod 'SDWebImagePhotosPlugin'
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
With Expo, add it with [expo-build-properties](https://docs.expo.dev/versions/latest/sdk/build-properties/):
|
|
125
|
-
|
|
126
|
-
```json
|
|
127
|
-
[
|
|
128
|
-
"expo-build-properties",
|
|
129
|
-
{ "ios": { "extraPods": [{ "name": "SDWebImagePhotosPlugin" }] } }
|
|
130
|
-
]
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
The app needs access to the photo library, which it has if it got the url from there. A photo library image is decoded at about the view's size, since photos are large and usually shown small; `onLoad` still reports the photo's own size. `FastImage.preload` of a `ph://` source loads the full-size photo (there's no view to size it for), which doesn't make a view's smaller copy load faster, so preloading photo library images usually isn't worth it. Without the plugin, a `ph://` source fails with `onError`, saying so. `assets-library://` urls aren't supported. On Android, photo pickers give `content://` urls, which load as they are.
|
|
120
|
+
The app needs access to the photo library, which it has if it got the url from there. A photo library image is decoded at about the view's size, since photos are large and usually shown small; `onLoad` still reports the photo's own size. `FastImage.preload` of a `ph://` source loads the full-size photo (there's no view to size it for), which doesn't make a view's smaller copy load faster, so preloading photo library images usually isn't worth it. `assets-library://` urls aren't supported. On Android, photo pickers give `content://` urls, which load as they are.
|
|
134
121
|
|
|
135
122
|
#### SVG images
|
|
136
123
|
|
|
@@ -154,6 +141,12 @@ An SVG is drawn at the size it's shown at, so it's sharp at any size, and then w
|
|
|
154
141
|
|
|
155
142
|
---
|
|
156
143
|
|
|
144
|
+
#### Animated PNG (Android)
|
|
145
|
+
|
|
146
|
+
APNG (animated PNG) images animate on Android with [APNG4Android](https://github.com/penfeizhou/APNG4Android), which FastImage includes when the app's `minSdkVersion` is 21 or later (React Native 0.64 and later by default). With an older `minSdkVersion` they show their first frame.
|
|
147
|
+
|
|
148
|
+
APNGs work like other animated images: `loop`, `paused` (which continues from the frame it paused on), and a still first frame with `blurRadius` or `resizeMode="repeat"`. Each frame is decoded as it plays, at about the size it's shown at, off the main thread.
|
|
149
|
+
|
|
157
150
|
### `source.headers?: object`
|
|
158
151
|
|
|
159
152
|
Headers to load the image with. e.g. `{ Authorization: 'someAuthToken' }`.
|
|
@@ -260,7 +253,7 @@ For views that get reused for other content, such as rows in FlashList or recycl
|
|
|
260
253
|
|
|
261
254
|
### `loop?: boolean | number`
|
|
262
255
|
|
|
263
|
-
How many times an animated image (GIF, animated WebP) plays:
|
|
256
|
+
How many times an animated image (GIF, animated WebP, APNG, and animated AVIF on Android) plays:
|
|
264
257
|
|
|
265
258
|
- Not set **(Default)** - As many times as the file says (like a browser).
|
|
266
259
|
- `true` - Loop forever.
|
|
@@ -283,7 +276,7 @@ How the image is filtered when it's drawn smaller or larger than its size (like
|
|
|
283
276
|
|
|
284
277
|
### `paused?: boolean`
|
|
285
278
|
|
|
286
|
-
Pauses an animated image (GIF, and animated
|
|
279
|
+
Pauses an animated image (GIF, animated WebP, APNG, and animated AVIF on Android) on the frame it's showing; `false` plays it again from there (on Android, an animated WebP or AVIF plays again from its first frame: Android can't resume one). Each image animates on its own, so pausing one doesn't pause others showing the same file.
|
|
287
280
|
|
|
288
281
|
---
|
|
289
282
|
|
|
@@ -315,7 +308,7 @@ iOS only. Decodes a large image at about the size it's shown at, instead of at f
|
|
|
315
308
|
- `true` **(Default)** - An image at least twice the size its view needs is decoded at about the view's size. If the view grows, the image is decoded again for its new size (from the disk cache).
|
|
316
309
|
- `false` - Images are decoded at full size, e.g. for an image that's zoomed in on with a transform (a pinch-to-zoom viewer), which would otherwise show the smaller copy enlarged.
|
|
317
310
|
|
|
318
|
-
It doesn't change `onLoad`'s width and height (the image's own size) or the cached file. Needs SDWebImage 5.19.7 or later: before 5.19 images are decoded at full size, and 5.19.0 to 5.19.6 show photos stored sideways with an EXIF orientation (most phone photos) sideways. Photo library images are always decoded this way, and on Android images are always decoded at about the view's size.
|
|
311
|
+
It doesn't change `onLoad`'s width and height (the image's own size) or the cached file. Needs SDWebImage 5.19.7 or later: before 5.19 images are decoded at full size, and 5.19.0 to 5.19.6 show photos stored sideways with an EXIF orientation (most phone photos) sideways. Photo library images are always decoded this way, and on Android images are always decoded at about the view's size (but Android 16 and later decode animated WebP at full size, and scale it as they draw it).
|
|
319
312
|
|
|
320
313
|
If you control the images, serve them at the size they're shown (resized on your server or by an image CDN), which also saves bandwidth.
|
|
321
314
|
|
|
@@ -328,7 +321,7 @@ Blurs the image by this radius, in points (the same radius looks about the same
|
|
|
328
321
|
It's for still images, or a radius that changes now and then (e.g. blurring a photo behind a sheet). Each change blurs the image again on the CPU, so don't animate it.
|
|
329
322
|
|
|
330
323
|
- Only the loaded image is blurred, not `defaultSource`.
|
|
331
|
-
- An animated image (GIF, animated WebP) shows its first frame, blurred, and doesn't animate.
|
|
324
|
+
- An animated image (GIF, animated WebP, APNG or AVIF) shows its first frame, blurred, and doesn't animate.
|
|
332
325
|
- With `tintColor`, the blurred image is tinted.
|
|
333
326
|
- The image is blurred at about the size it's shown at, off the main thread. The cached file stays the original image, so `getCachePath` and other views of it aren't affected.
|
|
334
327
|
- Changing it blurs the image that's showing again, without sending the load events again.
|
package/RNFastImage.podspec
CHANGED
|
@@ -21,4 +21,7 @@ Pod::Spec.new do |s|
|
|
|
21
21
|
# manifest Apple requires.
|
|
22
22
|
s.dependency 'SDWebImage', '>= 5.11.1', '< 6.0'
|
|
23
23
|
s.dependency 'SDWebImageWebPCoder', '>= 0.8.4', '< 1.0'
|
|
24
|
+
# Photo library images (ph://). iOS only: it needs tvOS 10, and FastImage
|
|
25
|
+
# finds it at runtime, so a tvOS app can still add it.
|
|
26
|
+
s.ios.dependency 'SDWebImagePhotosPlugin', '>= 1.2.0', '< 2.0'
|
|
24
27
|
end
|
package/android/build.gradle
CHANGED
|
@@ -19,6 +19,17 @@ buildscript {
|
|
|
19
19
|
|
|
20
20
|
apply plugin: 'com.android.library'
|
|
21
21
|
|
|
22
|
+
// When changing the default, revisit the Glide rules in consumer-rules.pro.
|
|
23
|
+
// 4.15+ animates animated WebP (Android 9+) and AVIF (Android 12+).
|
|
24
|
+
def glideVersion = safeExtGet('glideVersion', '4.16.0')
|
|
25
|
+
// Whether it's 4.15 or later, which FastImageAnimated (src/glide-4.15) builds
|
|
26
|
+
// on; an older one gets a stand-in (src/glide-older). A version that isn't
|
|
27
|
+
// numbered (e.g. "4.+") is taken to be recent.
|
|
28
|
+
def glideVersionParts = glideVersion.toString() =~ /^(\d+)\.(\d+)/
|
|
29
|
+
def glideHasAnimatedDecoder = !glideVersionParts.find() ||
|
|
30
|
+
glideVersionParts.group(1).toInteger() > 4 ||
|
|
31
|
+
(glideVersionParts.group(1).toInteger() == 4 && glideVersionParts.group(2).toInteger() >= 15)
|
|
32
|
+
|
|
22
33
|
// Newer versions of the Android Gradle plugin (7.3+) take the namespace from
|
|
23
34
|
// build.gradle and warn about the package attribute in AndroidManifest.xml;
|
|
24
35
|
// older versions need that attribute.
|
|
@@ -44,9 +55,12 @@ android {
|
|
|
44
55
|
manifest.srcFile "src/main/AndroidManifestNew.xml"
|
|
45
56
|
}
|
|
46
57
|
java {
|
|
58
|
+
srcDir glideHasAnimatedDecoder ? "src/glide-4.15/java" : "src/glide-older/java"
|
|
47
59
|
if (safeExtGet('excludeAppGlideModule', false)) {
|
|
48
60
|
srcDir "src"
|
|
49
61
|
exclude "**/FastImageGlideModule.java"
|
|
62
|
+
// Only the one chosen above.
|
|
63
|
+
exclude "glide-*/**"
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
66
|
}
|
|
@@ -70,9 +84,6 @@ repositories {
|
|
|
70
84
|
mavenCentral()
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
// When changing the default, revisit the Glide rules in consumer-rules.pro.
|
|
74
|
-
def glideVersion = safeExtGet('glideVersion', '4.12.0')
|
|
75
|
-
|
|
76
87
|
dependencies {
|
|
77
88
|
//noinspection GradleDynamicVersion
|
|
78
89
|
implementation 'com.facebook.react:react-native:+' // From node_modules
|
|
@@ -84,4 +95,13 @@ dependencies {
|
|
|
84
95
|
// SVG images, when the app has AndroidSVG (either package): not shipped,
|
|
85
96
|
// so there's never a second copy of it (see FastImageSvg).
|
|
86
97
|
compileOnly 'com.caverock:androidsvg-aar:1.4'
|
|
98
|
+
// Animated PNGs, with APNG4Android (see FastImageApng), which needs
|
|
99
|
+
// minSdkVersion 21: shipped when the app's is 21 or later. With an older
|
|
100
|
+
// one, FastImage only compiles against it, and APNGs show their first
|
|
101
|
+
// frame.
|
|
102
|
+
if (safeExtGet('minSdkVersion', 16).toString().toInteger() >= 21) {
|
|
103
|
+
implementation 'com.github.penfeizhou.android.animation:apng:3.0.5'
|
|
104
|
+
} else {
|
|
105
|
+
compileOnly 'com.github.penfeizhou.android.animation:apng:3.0.5'
|
|
106
|
+
}
|
|
87
107
|
}
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
# The Glide rules below cover what Glide's own rules (its library's
|
|
11
11
|
# proguard-rules.txt) didn't at the version FastImage uses by default
|
|
12
|
-
# (glideVersion in build.gradle
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# old-style modules in their
|
|
16
|
-
# OkHttpGlideModule still did in 5.0.9).
|
|
12
|
+
# (glideVersion in build.gradle) and older ones apps may pick. When updating
|
|
13
|
+
# Glide, revisit which are still needed: whether Glide's rules now keep
|
|
14
|
+
# modules' constructors (4.16.0's still keep only the AppGlideModule's), and
|
|
15
|
+
# whether its integrations still register old-style modules in their
|
|
16
|
+
# manifests (the OkHttp integration's OkHttpGlideModule still did in 5.0.9).
|
|
17
17
|
|
|
18
18
|
# Glide's modules and their constructors: FastImage's, the one Glide
|
|
19
19
|
# generates from them, and old-style modules Glide finds in the app's manifest
|
|
@@ -46,3 +46,7 @@
|
|
|
46
46
|
# (FastImageSvgRenderer), and doesn't ship. Without it, R8 would stop at the
|
|
47
47
|
# missing classes.
|
|
48
48
|
-dontwarn com.caverock.androidsvg.**
|
|
49
|
+
|
|
50
|
+
# APNG4Android, which FastImage uses for animated PNGs (FastImageApngRenderer),
|
|
51
|
+
# and doesn't ship to apps with a minSdkVersion below 21 (build.gradle).
|
|
52
|
+
-dontwarn com.github.penfeizhou.animation.**
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.res.Resources;
|
|
5
|
+
import android.graphics.drawable.AnimatedImageDrawable;
|
|
6
|
+
import android.graphics.drawable.BitmapDrawable;
|
|
7
|
+
import android.graphics.drawable.Drawable;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
import android.os.Handler;
|
|
10
|
+
import android.os.Looper;
|
|
11
|
+
|
|
12
|
+
import androidx.annotation.NonNull;
|
|
13
|
+
import androidx.annotation.Nullable;
|
|
14
|
+
import androidx.annotation.RequiresApi;
|
|
15
|
+
|
|
16
|
+
import com.bumptech.glide.Glide;
|
|
17
|
+
import com.bumptech.glide.Registry;
|
|
18
|
+
import com.bumptech.glide.load.ImageHeaderParser;
|
|
19
|
+
import com.bumptech.glide.load.Options;
|
|
20
|
+
import com.bumptech.glide.load.ResourceDecoder;
|
|
21
|
+
import com.bumptech.glide.load.engine.Resource;
|
|
22
|
+
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
|
|
23
|
+
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder;
|
|
24
|
+
import com.bumptech.glide.load.resource.bitmap.ByteBufferBitmapDecoder;
|
|
25
|
+
import com.bumptech.glide.load.resource.bitmap.Downsampler;
|
|
26
|
+
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
|
|
27
|
+
import com.bumptech.glide.load.resource.drawable.AnimatedImageDecoder;
|
|
28
|
+
import com.bumptech.glide.load.resource.gif.GifOptions;
|
|
29
|
+
import com.bumptech.glide.util.ByteBufferUtil;
|
|
30
|
+
|
|
31
|
+
import java.io.IOException;
|
|
32
|
+
import java.io.InputStream;
|
|
33
|
+
import java.nio.ByteBuffer;
|
|
34
|
+
import java.util.List;
|
|
35
|
+
import java.util.Map;
|
|
36
|
+
import java.util.WeakHashMap;
|
|
37
|
+
import java.util.concurrent.Executor;
|
|
38
|
+
import java.util.concurrent.Executors;
|
|
39
|
+
|
|
40
|
+
// Animated WebP and AVIF images, as views' own animations. Glide (4.15+)
|
|
41
|
+
// decodes them to an AnimatedImageDrawable and keeps it in its memory cache,
|
|
42
|
+
// giving the same drawable to every view that shows the image, so the views
|
|
43
|
+
// would share its playback: one view's paused or loop would apply to all of
|
|
44
|
+
// them. A view shows the drawable Glide gives it when no other view is showing
|
|
45
|
+
// it (shownElsewhere), and otherwise one of its own (copy), decoded again from
|
|
46
|
+
// the image's data (which the drawable keeps anyway, to decode its frames
|
|
47
|
+
// from), off the main thread.
|
|
48
|
+
//
|
|
49
|
+
// Not animated (dontAnimate: blurRadius, resizeMode repeat), they're decoded
|
|
50
|
+
// as their first frame, a bitmap, as GIFs are, and as Glide did before 4.15.
|
|
51
|
+
// Glide's own decoder for them doesn't check that option.
|
|
52
|
+
//
|
|
53
|
+
// Built only with Glide 4.15 or later, which has the decoder this wraps; with
|
|
54
|
+
// an older Glide, src/glide-older's stand-in does nothing (build.gradle).
|
|
55
|
+
final class FastImageAnimated {
|
|
56
|
+
private FastImageAnimated() {}
|
|
57
|
+
|
|
58
|
+
interface CopyCallback {
|
|
59
|
+
// On the main thread: the copy, or null if it couldn't be decoded.
|
|
60
|
+
void onCopy(@Nullable Drawable copy);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// What each drawable Glide decoded was decoded from, to decode copies
|
|
64
|
+
// (kept as long as the drawable is).
|
|
65
|
+
private static final Map<Drawable, Source> SOURCES = new WeakHashMap<>();
|
|
66
|
+
// Decodes copies, one at a time.
|
|
67
|
+
private static final Executor COPIES = Executors.newSingleThreadExecutor(runnable -> {
|
|
68
|
+
Thread thread = new Thread(runnable, "FastImageAnimated");
|
|
69
|
+
thread.setDaemon(true);
|
|
70
|
+
return thread;
|
|
71
|
+
});
|
|
72
|
+
private static final Handler MAIN = new Handler(Looper.getMainLooper());
|
|
73
|
+
// The repeat count each drawable decoded here had: the file's, which a
|
|
74
|
+
// view goes back to when loop isn't set (a view may have changed it with
|
|
75
|
+
// loop, this one or one that showed the drawable before).
|
|
76
|
+
private static final Map<Drawable, Integer> REPEAT_COUNTS = new WeakHashMap<>();
|
|
77
|
+
|
|
78
|
+
private static final class Source {
|
|
79
|
+
final ResourceDecoder<ByteBuffer, Drawable> decoder;
|
|
80
|
+
final ByteBuffer data;
|
|
81
|
+
final int width;
|
|
82
|
+
final int height;
|
|
83
|
+
final Options options;
|
|
84
|
+
|
|
85
|
+
Source(ResourceDecoder<ByteBuffer, Drawable> decoder, ByteBuffer data, int width, int height, Options options) {
|
|
86
|
+
this.decoder = decoder;
|
|
87
|
+
this.data = data;
|
|
88
|
+
this.width = width;
|
|
89
|
+
this.height = height;
|
|
90
|
+
this.options = options;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Ahead of Glide's own decoder for them, which this uses.
|
|
95
|
+
static void register(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
|
96
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return;
|
|
97
|
+
List<ImageHeaderParser> parsers = registry.getImageHeaderParsers();
|
|
98
|
+
ArrayPool arrayPool = glide.getArrayPool();
|
|
99
|
+
Resources resources = context.getResources();
|
|
100
|
+
Downsampler downsampler = new Downsampler(
|
|
101
|
+
parsers, resources.getDisplayMetrics(), glide.getBitmapPool(), arrayPool);
|
|
102
|
+
ByteBufferDecoder byteBufferDecoder = new ByteBufferDecoder(
|
|
103
|
+
AnimatedImageDecoder.byteBufferDecoder(parsers, arrayPool),
|
|
104
|
+
new BitmapDrawableDecoder<>(resources, new ByteBufferBitmapDecoder(downsampler)));
|
|
105
|
+
registry.prepend(ByteBuffer.class, Drawable.class, byteBufferDecoder);
|
|
106
|
+
registry.prepend(InputStream.class, Drawable.class, new StreamDecoder(
|
|
107
|
+
AnimatedImageDecoder.streamDecoder(parsers, arrayPool),
|
|
108
|
+
new BitmapDrawableDecoder<>(resources, new StreamBitmapDecoder(downsampler, arrayPool)),
|
|
109
|
+
byteBufferDecoder));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Whether the drawable is one decoded here that a view other than this
|
|
113
|
+
// one is showing (its callback is that view, or a drawable that view
|
|
114
|
+
// shows). A view that stops showing it clears its callback, so the next
|
|
115
|
+
// view can show it instead of a copy.
|
|
116
|
+
static boolean shownElsewhere(@NonNull Drawable drawable, @NonNull Drawable.Callback view) {
|
|
117
|
+
synchronized (SOURCES) {
|
|
118
|
+
if (!SOURCES.containsKey(drawable)) return false;
|
|
119
|
+
}
|
|
120
|
+
Drawable.Callback callback = drawable.getCallback();
|
|
121
|
+
return callback != null && callback != view;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The file's repeat count for a drawable decoded here, or null.
|
|
125
|
+
@Nullable
|
|
126
|
+
static Integer repeatCount(@NonNull Drawable drawable) {
|
|
127
|
+
synchronized (REPEAT_COUNTS) {
|
|
128
|
+
return REPEAT_COUNTS.get(drawable);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@RequiresApi(Build.VERSION_CODES.P)
|
|
133
|
+
@Nullable
|
|
134
|
+
private static Resource<Drawable> remember(@Nullable Resource<Drawable> decoded) {
|
|
135
|
+
if (decoded == null) return null;
|
|
136
|
+
Drawable drawable = decoded.get();
|
|
137
|
+
if (drawable instanceof AnimatedImageDrawable) {
|
|
138
|
+
synchronized (REPEAT_COUNTS) {
|
|
139
|
+
REPEAT_COUNTS.put(drawable, ((AnimatedImageDrawable) drawable).getRepeatCount());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return decoded;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Decodes a drawable of its own for an image decoded here, off the main
|
|
146
|
+
// thread (its first frame: the drawable decodes the rest as it plays).
|
|
147
|
+
// The callback gets null if it isn't one, or it couldn't be decoded.
|
|
148
|
+
static void copy(@NonNull Drawable drawable, @NonNull CopyCallback callback) {
|
|
149
|
+
Source source;
|
|
150
|
+
synchronized (SOURCES) {
|
|
151
|
+
source = SOURCES.get(drawable);
|
|
152
|
+
}
|
|
153
|
+
if (source == null) {
|
|
154
|
+
callback.onCopy(null);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
COPIES.execute(() -> {
|
|
158
|
+
Drawable copy = null;
|
|
159
|
+
try {
|
|
160
|
+
Resource<Drawable> resource = remember(source.decoder.decode(
|
|
161
|
+
source.data.duplicate(), source.width, source.height, source.options));
|
|
162
|
+
if (resource != null) copy = resource.get();
|
|
163
|
+
} catch (IOException | RuntimeException ignored) {
|
|
164
|
+
// It decoded the first time; if it doesn't now, there's no copy.
|
|
165
|
+
}
|
|
166
|
+
Drawable result = copy;
|
|
167
|
+
MAIN.post(() -> callback.onCopy(result));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static boolean animates(Options options) {
|
|
172
|
+
return !Boolean.TRUE.equals(options.get(GifOptions.DISABLE_ANIMATION));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The first frame, as a still image.
|
|
176
|
+
@SuppressWarnings("unchecked")
|
|
177
|
+
@Nullable
|
|
178
|
+
private static <T> Resource<Drawable> still(
|
|
179
|
+
ResourceDecoder<T, BitmapDrawable> decoder, T source, int width, int height, Options options)
|
|
180
|
+
throws IOException {
|
|
181
|
+
return (Resource<Drawable>) (Resource<?>) decoder.decode(source, width, height, options);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@RequiresApi(Build.VERSION_CODES.P)
|
|
185
|
+
private static final class ByteBufferDecoder implements ResourceDecoder<ByteBuffer, Drawable> {
|
|
186
|
+
private final ResourceDecoder<ByteBuffer, Drawable> glide;
|
|
187
|
+
private final ResourceDecoder<ByteBuffer, BitmapDrawable> stillDecoder;
|
|
188
|
+
|
|
189
|
+
ByteBufferDecoder(ResourceDecoder<ByteBuffer, Drawable> glide,
|
|
190
|
+
ResourceDecoder<ByteBuffer, BitmapDrawable> stillDecoder) {
|
|
191
|
+
this.glide = glide;
|
|
192
|
+
this.stillDecoder = stillDecoder;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Override
|
|
196
|
+
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
|
|
197
|
+
return glide.handles(source, options);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@Nullable
|
|
201
|
+
@Override
|
|
202
|
+
public Resource<Drawable> decode(@NonNull ByteBuffer source, int width, int height, @NonNull Options options)
|
|
203
|
+
throws IOException {
|
|
204
|
+
if (!animates(options)) return still(stillDecoder, source, width, height, options);
|
|
205
|
+
// Its own position, for decoding it again.
|
|
206
|
+
ByteBuffer data = source.duplicate();
|
|
207
|
+
Resource<Drawable> decoded = remember(glide.decode(source, width, height, options));
|
|
208
|
+
if (decoded != null) {
|
|
209
|
+
synchronized (SOURCES) {
|
|
210
|
+
SOURCES.put(decoded.get(), new Source(glide, data, width, height, options));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return decoded;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@RequiresApi(Build.VERSION_CODES.P)
|
|
218
|
+
private static final class StreamDecoder implements ResourceDecoder<InputStream, Drawable> {
|
|
219
|
+
private final ResourceDecoder<InputStream, Drawable> glide;
|
|
220
|
+
private final ResourceDecoder<InputStream, BitmapDrawable> stillDecoder;
|
|
221
|
+
private final ByteBufferDecoder byteBufferDecoder;
|
|
222
|
+
|
|
223
|
+
StreamDecoder(ResourceDecoder<InputStream, Drawable> glide,
|
|
224
|
+
ResourceDecoder<InputStream, BitmapDrawable> stillDecoder, ByteBufferDecoder byteBufferDecoder) {
|
|
225
|
+
this.glide = glide;
|
|
226
|
+
this.stillDecoder = stillDecoder;
|
|
227
|
+
this.byteBufferDecoder = byteBufferDecoder;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@Override
|
|
231
|
+
public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException {
|
|
232
|
+
return glide.handles(source, options);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Read whole, as Glide's own does.
|
|
236
|
+
@Nullable
|
|
237
|
+
@Override
|
|
238
|
+
public Resource<Drawable> decode(@NonNull InputStream source, int width, int height, @NonNull Options options)
|
|
239
|
+
throws IOException {
|
|
240
|
+
if (!animates(options)) return still(stillDecoder, source, width, height, options);
|
|
241
|
+
return byteBufferDecoder.decode(ByteBufferUtil.fromStream(source), width, height, options);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.drawable.Drawable;
|
|
5
|
+
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
import androidx.annotation.Nullable;
|
|
8
|
+
|
|
9
|
+
import com.bumptech.glide.Glide;
|
|
10
|
+
import com.bumptech.glide.Registry;
|
|
11
|
+
|
|
12
|
+
// Built instead of src/glide-4.15's FastImageAnimated with a Glide before
|
|
13
|
+
// 4.15 (build.gradle), which has no decoder for animated WebP or AVIF: they
|
|
14
|
+
// show their first frame, so there's nothing to register, remember or copy.
|
|
15
|
+
final class FastImageAnimated {
|
|
16
|
+
private FastImageAnimated() {}
|
|
17
|
+
|
|
18
|
+
interface CopyCallback {
|
|
19
|
+
void onCopy(@Nullable Drawable copy);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static void register(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {}
|
|
23
|
+
|
|
24
|
+
@Nullable
|
|
25
|
+
static Integer repeatCount(@NonNull Drawable drawable) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static boolean shownElsewhere(@NonNull Drawable drawable, @NonNull Drawable.Callback view) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static void copy(@NonNull Drawable drawable, @NonNull CopyCallback callback) {
|
|
34
|
+
callback.onCopy(null);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
// An animated image that applies FastImage's loop and paused itself (an APNG,
|
|
4
|
+
// FastImageApngRenderer). The view's target starts it when it's shown.
|
|
5
|
+
interface FastImageAnimatable {
|
|
6
|
+
// loopCount as FastImageViewWithUrl has it: -1, as many times as the file
|
|
7
|
+
// says; 0, forever; n, n times. restart: play it again from its first
|
|
8
|
+
// frame.
|
|
9
|
+
void setLoopCount(int loopCount, boolean restart);
|
|
10
|
+
|
|
11
|
+
// Stays on the frame it's showing; false continues from there.
|
|
12
|
+
void setPaused(boolean paused);
|
|
13
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.graphics.drawable.Drawable;
|
|
4
|
+
import android.os.Build;
|
|
5
|
+
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
import androidx.annotation.Nullable;
|
|
8
|
+
|
|
9
|
+
import com.bumptech.glide.Registry;
|
|
10
|
+
import com.bumptech.glide.load.Options;
|
|
11
|
+
import com.bumptech.glide.load.ResourceDecoder;
|
|
12
|
+
import com.bumptech.glide.load.engine.Resource;
|
|
13
|
+
import com.bumptech.glide.load.resource.gif.GifOptions;
|
|
14
|
+
import com.bumptech.glide.util.ByteBufferUtil;
|
|
15
|
+
|
|
16
|
+
import java.io.IOException;
|
|
17
|
+
import java.io.InputStream;
|
|
18
|
+
import java.nio.ByteBuffer;
|
|
19
|
+
import java.nio.charset.StandardCharsets;
|
|
20
|
+
|
|
21
|
+
// Animated PNGs (APNG), animated with APNG4Android
|
|
22
|
+
// (com.github.penfeizhou.android.animation:apng), which FastImage ships when
|
|
23
|
+
// the app's minSdkVersion is 21 or later, as APNG4Android needs (build.gradle).
|
|
24
|
+
// Without it, an APNG shows its first frame, as Android shows it. Android and
|
|
25
|
+
// Glide don't animate APNG themselves.
|
|
26
|
+
//
|
|
27
|
+
// The decoders come before Glide's own, and only take PNGs with an animation
|
|
28
|
+
// control chunk (acTL), when the request animates (not with dontAnimate:
|
|
29
|
+
// blurRadius, resizeMode repeat, which get the first frame from Glide's
|
|
30
|
+
// decoder). This class doesn't use APNG4Android itself
|
|
31
|
+
// (FastImageApngRenderer does), so it loads without it.
|
|
32
|
+
final class FastImageApng {
|
|
33
|
+
// As much of the start of the file as is read to find acTL, which comes
|
|
34
|
+
// before the image data.
|
|
35
|
+
private static final int HEAD_LENGTH = 4096;
|
|
36
|
+
private static final byte[] SIGNATURE = {(byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'};
|
|
37
|
+
@Nullable
|
|
38
|
+
private static Boolean available;
|
|
39
|
+
|
|
40
|
+
private FastImageApng() {
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static synchronized boolean available() {
|
|
44
|
+
if (available == null) {
|
|
45
|
+
try {
|
|
46
|
+
// Android 5+, as APNG4Android needs.
|
|
47
|
+
available = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && FastImageApngRenderer.available();
|
|
48
|
+
} catch (Throwable e) {
|
|
49
|
+
available = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return available;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static void register(@NonNull Registry registry) {
|
|
56
|
+
if (!available()) return;
|
|
57
|
+
registry.prepend(InputStream.class, Drawable.class, new StreamDecoder())
|
|
58
|
+
.prepend(ByteBuffer.class, Drawable.class, new BufferDecoder());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// How many times the APNG says it plays (0: forever), or -1 if the data
|
|
62
|
+
// doesn't start like an APNG: a PNG with an acTL chunk before its image
|
|
63
|
+
// data.
|
|
64
|
+
static int plays(@NonNull byte[] head, int length) {
|
|
65
|
+
if (length < SIGNATURE.length) return -1;
|
|
66
|
+
for (int i = 0; i < SIGNATURE.length; i++) {
|
|
67
|
+
if (head[i] != SIGNATURE[i]) return -1;
|
|
68
|
+
}
|
|
69
|
+
int offset = SIGNATURE.length;
|
|
70
|
+
// Each chunk: a length, a type, the data, a CRC.
|
|
71
|
+
while (offset + 8 <= length) {
|
|
72
|
+
int chunkLength = ((head[offset] & 0xff) << 24) | ((head[offset + 1] & 0xff) << 16)
|
|
73
|
+
| ((head[offset + 2] & 0xff) << 8) | (head[offset + 3] & 0xff);
|
|
74
|
+
String type = new String(head, offset + 4, 4, StandardCharsets.US_ASCII);
|
|
75
|
+
if (type.equals("acTL")) {
|
|
76
|
+
// num_frames, then num_plays.
|
|
77
|
+
int plays = offset + 12;
|
|
78
|
+
if (plays + 4 > length) return -1;
|
|
79
|
+
return ((head[plays] & 0x7f) << 24) | ((head[plays + 1] & 0xff) << 16)
|
|
80
|
+
| ((head[plays + 2] & 0xff) << 8) | (head[plays + 3] & 0xff);
|
|
81
|
+
}
|
|
82
|
+
if (type.equals("IDAT") || chunkLength < 0) return -1;
|
|
83
|
+
offset += 12 + chunkLength;
|
|
84
|
+
}
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private static boolean animates(Options options) {
|
|
89
|
+
return !Boolean.TRUE.equals(options.get(GifOptions.DISABLE_ANIMATION));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private static int plays(@NonNull ByteBuffer source) {
|
|
93
|
+
ByteBuffer copy = source.duplicate();
|
|
94
|
+
byte[] head = new byte[Math.min(HEAD_LENGTH, copy.remaining())];
|
|
95
|
+
copy.get(head);
|
|
96
|
+
return plays(head, head.length);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private static final class BufferDecoder implements ResourceDecoder<ByteBuffer, Drawable> {
|
|
100
|
+
@Override
|
|
101
|
+
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
|
|
102
|
+
return animates(options) && plays(source) >= 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Nullable
|
|
106
|
+
@Override
|
|
107
|
+
public Resource<Drawable> decode(@NonNull ByteBuffer source, int width, int height, @NonNull Options options)
|
|
108
|
+
throws IOException {
|
|
109
|
+
return FastImageApngRenderer.decode(source.duplicate(), plays(source));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private static final class StreamDecoder implements ResourceDecoder<InputStream, Drawable> {
|
|
114
|
+
@Override
|
|
115
|
+
public boolean handles(@NonNull InputStream source, @NonNull Options options) throws IOException {
|
|
116
|
+
if (!animates(options)) return false;
|
|
117
|
+
byte[] head = new byte[HEAD_LENGTH];
|
|
118
|
+
source.mark(HEAD_LENGTH);
|
|
119
|
+
int read = 0;
|
|
120
|
+
try {
|
|
121
|
+
while (read < head.length) {
|
|
122
|
+
int count = source.read(head, read, head.length - read);
|
|
123
|
+
if (count < 0) break;
|
|
124
|
+
read += count;
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
source.reset();
|
|
128
|
+
}
|
|
129
|
+
return plays(head, read) >= 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Read whole: APNG4Android reads frames from it as it plays.
|
|
133
|
+
@Nullable
|
|
134
|
+
@Override
|
|
135
|
+
public Resource<Drawable> decode(@NonNull InputStream source, int width, int height, @NonNull Options options)
|
|
136
|
+
throws IOException {
|
|
137
|
+
ByteBuffer data = ByteBufferUtil.fromStream(source);
|
|
138
|
+
return FastImageApngRenderer.decode(data, plays(data));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|