react-native-fast-image 8.28.1 → 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 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 when the app has SDWebImagePhotosPlugin. Add it to the app's `ios/Podfile` and run `pod install`:
118
+ A photo library url (`ph://<localIdentifier>`, as camera roll libraries give) loads with SDWebImagePhotosPlugin, which FastImage includes on iOS.
119
119
 
120
- ```ruby
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 WebP on iOS) on the frame it's showing; `false` plays it again from there. Each image animates on its own, so pausing one doesn't pause others showing the same file.
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.
@@ -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
@@ -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, 4.12.0) and older ones apps may pick. When
13
- # updating Glide, revisit which are still needed: whether Glide's rules now
14
- # keep modules' constructors, and whether its integrations still register
15
- # old-style modules in their manifests (the OkHttp integration's
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
+ }