react-native-fast-image 8.10.0 → 8.12.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 +16 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageGif.java +37 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +10 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +74 -5
- package/dist/index.cjs.js.flow +2 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js.flow +2 -0
- package/ios/FastImage/FFFastImageView.h +5 -0
- package/ios/FastImage/FFFastImageView.m +38 -1
- package/ios/FastImage/FFFastImageViewManager.m +2 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -186,6 +186,22 @@ Changing it restarts the animation.
|
|
|
186
186
|
|
|
187
187
|
---
|
|
188
188
|
|
|
189
|
+
### `imageRendering?: 'auto' | 'smooth' | 'pixelated'`
|
|
190
|
+
|
|
191
|
+
How the image is filtered when it's drawn smaller or larger than its size (like CSS's `image-rendering`):
|
|
192
|
+
|
|
193
|
+
- `'auto'` **(Default)** - The platform's usual filtering.
|
|
194
|
+
- `'smooth'` - iOS only. Keeps a large image drawn much smaller than its size (e.g. a big photo as a thumbnail, or fine lines and text) from looking jagged or noisy. **It uses more memory:** the image is also kept at smaller sizes for drawing, about a third more than the decoded image. On Android it's the same as `'auto'` (images are already decoded at about the view's size there).
|
|
195
|
+
- `'pixelated'` - Sharp pixels, without smoothing, e.g. for pixel art drawn larger than its size. On Android, animated images are still smoothed.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### `paused?: boolean`
|
|
200
|
+
|
|
201
|
+
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.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
189
205
|
### `onLoadStart?: () => void`
|
|
190
206
|
|
|
191
207
|
Called when the image starts to load.
|
|
@@ -18,6 +18,7 @@ import com.bumptech.glide.load.resource.bitmap.BitmapResource;
|
|
|
18
18
|
import com.bumptech.glide.load.resource.gif.GifBitmapProvider;
|
|
19
19
|
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
|
20
20
|
|
|
21
|
+
import java.lang.reflect.Field;
|
|
21
22
|
import java.nio.ByteBuffer;
|
|
22
23
|
|
|
23
24
|
// Gives a view its own animation of a GIF. Glide gives every view that shows
|
|
@@ -64,6 +65,42 @@ final class FastImageGif {
|
|
|
64
65
|
return copy;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// GifDrawable.start() also starts counting plays again, so a paused GIF
|
|
69
|
+
// would play its whole loop count again when resumed; iOS continues it.
|
|
70
|
+
// The count isn't public: keep it across start() by reflection. A GIF
|
|
71
|
+
// that already finished its plays starts over, as on iOS. Without the
|
|
72
|
+
// fields (another Glide version), it counts again, as start() does.
|
|
73
|
+
@Nullable
|
|
74
|
+
private static final Field LOOP_COUNT = field("loopCount");
|
|
75
|
+
@Nullable
|
|
76
|
+
private static final Field MAX_LOOP_COUNT = field("maxLoopCount");
|
|
77
|
+
|
|
78
|
+
static void resume(GifDrawable gif) {
|
|
79
|
+
if (LOOP_COUNT == null || MAX_LOOP_COUNT == null) {
|
|
80
|
+
gif.start();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
int played = LOOP_COUNT.getInt(gif);
|
|
85
|
+
int max = MAX_LOOP_COUNT.getInt(gif);
|
|
86
|
+
gif.start();
|
|
87
|
+
if (max == GifDrawable.LOOP_FOREVER || played < max) LOOP_COUNT.setInt(gif, played);
|
|
88
|
+
} catch (IllegalAccessException e) {
|
|
89
|
+
gif.start();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Nullable
|
|
94
|
+
private static Field field(String name) {
|
|
95
|
+
try {
|
|
96
|
+
Field field = GifDrawable.class.getDeclaredField(name);
|
|
97
|
+
field.setAccessible(true);
|
|
98
|
+
return field.getType() == int.class ? field : null;
|
|
99
|
+
} catch (Exception e) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
67
104
|
// ByteBufferGifDecoder's.
|
|
68
105
|
private static int sampleSize(GifHeader header, int width, int height) {
|
|
69
106
|
int exact = Math.min(header.getHeight() / height, header.getWidth() / width);
|
|
@@ -94,6 +94,16 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
94
94
|
view.setLoopCount(loopCount);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
@ReactProp(name = "imageRendering")
|
|
98
|
+
public void setImageRendering(FastImageViewWithUrl view, @Nullable String imageRendering) {
|
|
99
|
+
view.setImageRendering(imageRendering);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@ReactProp(name = "paused")
|
|
103
|
+
public void setPaused(FastImageViewWithUrl view, boolean paused) {
|
|
104
|
+
view.setPaused(paused);
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
@ReactProp(name = "resizeMode")
|
|
98
108
|
public void setResizeMode(FastImageViewWithUrl view, String resizeMode) {
|
|
99
109
|
final FastImageViewWithUrl.ScaleType scaleType = FastImageViewConverter.getScaleType(resizeMode);
|
|
@@ -5,6 +5,7 @@ import static com.dylanvann.fastimage.FastImageRequestListener.REACT_ON_LOAD_END
|
|
|
5
5
|
|
|
6
6
|
import android.annotation.SuppressLint;
|
|
7
7
|
import android.content.Context;
|
|
8
|
+
import android.graphics.drawable.BitmapDrawable;
|
|
8
9
|
import android.graphics.drawable.Drawable;
|
|
9
10
|
|
|
10
11
|
import androidx.annotation.NonNull;
|
|
@@ -15,8 +16,10 @@ import androidx.core.view.ViewCompat;
|
|
|
15
16
|
import com.bumptech.glide.RequestBuilder;
|
|
16
17
|
import com.bumptech.glide.RequestManager;
|
|
17
18
|
import com.bumptech.glide.load.model.GlideUrl;
|
|
19
|
+
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
|
18
20
|
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
|
19
21
|
import com.bumptech.glide.request.Request;
|
|
22
|
+
import com.bumptech.glide.request.RequestOptions;
|
|
20
23
|
import com.bumptech.glide.request.target.DrawableImageViewTarget;
|
|
21
24
|
import com.bumptech.glide.request.target.SizeReadyCallback;
|
|
22
25
|
import com.bumptech.glide.request.transition.Transition;
|
|
@@ -76,8 +79,26 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
76
79
|
// another view plays it.
|
|
77
80
|
if (mOwnGif != null && getDrawable() == mOwnGif) {
|
|
78
81
|
applyLoopCount(mOwnGif);
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
if (!mPaused) {
|
|
83
|
+
mOwnGif.stop();
|
|
84
|
+
mOwnGif.startFromFirstFrame();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pauses GIFs on the frame they're showing (the view's own animation).
|
|
90
|
+
private boolean mPaused = false;
|
|
91
|
+
|
|
92
|
+
public void setPaused(boolean paused) {
|
|
93
|
+
if (paused == mPaused) return;
|
|
94
|
+
mPaused = paused;
|
|
95
|
+
if (mOwnGif != null && getDrawable() == mOwnGif) {
|
|
96
|
+
if (paused) {
|
|
97
|
+
mOwnGif.stop();
|
|
98
|
+
} else {
|
|
99
|
+
// Continues the loop count, as on iOS.
|
|
100
|
+
FastImageGif.resume(mOwnGif);
|
|
101
|
+
}
|
|
81
102
|
}
|
|
82
103
|
}
|
|
83
104
|
|
|
@@ -95,6 +116,13 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
95
116
|
|
|
96
117
|
@Override
|
|
97
118
|
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
|
119
|
+
if (resource instanceof BitmapDrawable) {
|
|
120
|
+
if (mPixelated) {
|
|
121
|
+
// Scaled by the view without filtering: sharp pixels.
|
|
122
|
+
resource = resource.mutate();
|
|
123
|
+
resource.setFilterBitmap(false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
98
126
|
if (resource instanceof GifDrawable) {
|
|
99
127
|
GifDrawable own = FastImageGif.copy(
|
|
100
128
|
getContext(), (GifDrawable) resource, mLoadingWidth, mLoadingHeight);
|
|
@@ -102,12 +130,27 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
102
130
|
applyLoopCount(own);
|
|
103
131
|
super.onResourceReady(own, transition);
|
|
104
132
|
mOwnGif = own;
|
|
133
|
+
// The target starts it; paused, it waits on its first frame.
|
|
134
|
+
if (mPaused) own.stop();
|
|
105
135
|
return;
|
|
106
136
|
}
|
|
107
137
|
}
|
|
108
138
|
super.onResourceReady(resource, transition);
|
|
109
139
|
}
|
|
110
140
|
|
|
141
|
+
// The target starts animations again when the Activity does: not a
|
|
142
|
+
// paused one, and the view's own GIF continues its loop count (start()
|
|
143
|
+
// would count again).
|
|
144
|
+
@Override
|
|
145
|
+
public void onStart() {
|
|
146
|
+
if (mPaused) return;
|
|
147
|
+
if (mOwnGif != null && getDrawable() == mOwnGif) {
|
|
148
|
+
FastImageGif.resume(mOwnGif);
|
|
149
|
+
} else {
|
|
150
|
+
super.onStart();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
111
154
|
@Override
|
|
112
155
|
protected void setResource(@Nullable Drawable resource) {
|
|
113
156
|
super.setResource(resource);
|
|
@@ -134,6 +177,33 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
134
177
|
mNeedsReload = true;
|
|
135
178
|
}
|
|
136
179
|
|
|
180
|
+
// imageRendering="pixelated": no resampling by Glide, and drawn without
|
|
181
|
+
// filtering (sharp pixels). "smooth" is iOS only; here it's the same as
|
|
182
|
+
// "auto": Glide already decodes a large image at about the view's size, and
|
|
183
|
+
// averaging it properly would need a full-size decode. Part of the
|
|
184
|
+
// request, so a change reloads.
|
|
185
|
+
private boolean mPixelated = false;
|
|
186
|
+
|
|
187
|
+
public void setImageRendering(@Nullable String imageRendering) {
|
|
188
|
+
boolean pixelated = "pixelated".equals(imageRendering);
|
|
189
|
+
if (pixelated == mPixelated) return;
|
|
190
|
+
mPixelated = pixelated;
|
|
191
|
+
mNeedsReload = true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The scale type's options (what into() would apply), or none when
|
|
195
|
+
// pixelated.
|
|
196
|
+
private RequestOptions renderingOptions(Object model) {
|
|
197
|
+
if (mPixelated) {
|
|
198
|
+
// Decoded as it is, then scaled by the view without filtering (see
|
|
199
|
+
// OwnGifTarget).
|
|
200
|
+
return new RequestOptions()
|
|
201
|
+
.downsample(new FastImageSourceSize.Capture(DownsampleStrategy.NONE, String.valueOf(model)))
|
|
202
|
+
.dontTransform();
|
|
203
|
+
}
|
|
204
|
+
return FastImageSourceSize.scaleTypeOptions(getScaleType(), FastImageSourceSize.capture(getScaleType(), model));
|
|
205
|
+
}
|
|
206
|
+
|
|
137
207
|
// The request for the image the view shows once it has loaded, and the one
|
|
138
208
|
// loading. A new source starts with the shown one as a thumbnail, from the
|
|
139
209
|
// cache only, so the image stays until the new one has loaded instead of
|
|
@@ -353,7 +423,6 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
353
423
|
if (requestManager != null) {
|
|
354
424
|
// Records the image's own size when Glide decodes it, for onLoad.
|
|
355
425
|
Object model = imageSource == null ? null : imageSource.getSourceForLoad();
|
|
356
|
-
FastImageSourceSize.Capture capture = FastImageSourceSize.capture(getScaleType(), model);
|
|
357
426
|
RequestBuilder<Drawable> builder =
|
|
358
427
|
requestManager
|
|
359
428
|
// This will make this work for remote and local images. e.g.
|
|
@@ -368,8 +437,8 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
368
437
|
.placeholder(mDefaultSource) // show until loaded
|
|
369
438
|
.fallback(mDefaultSource)) // null will not be treated as error
|
|
370
439
|
// What into() would apply for the scale type, with
|
|
371
|
-
// the size capture.
|
|
372
|
-
.apply(
|
|
440
|
+
// the size capture, for imageRendering.
|
|
441
|
+
.apply(renderingOptions(model));
|
|
373
442
|
RequestBuilder<Drawable> request = builder.clone();
|
|
374
443
|
|
|
375
444
|
boolean thumbnail = shownRequest != null && model != null;
|
package/dist/index.cjs.js.flow
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,20 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
80
80
|
* or a number of times. Changing it restarts the animation.
|
|
81
81
|
*/
|
|
82
82
|
loop?: boolean | number;
|
|
83
|
+
/**
|
|
84
|
+
* How the image is filtered when it's drawn smaller or larger than its
|
|
85
|
+
* size: `'auto'` (default, the platform's usual filtering), `'smooth'`
|
|
86
|
+
* (iOS only: keeps a large image drawn much smaller from looking jagged
|
|
87
|
+
* or noisy; uses more memory, see the README) or `'pixelated'` (sharp
|
|
88
|
+
* pixels, e.g. for pixel art).
|
|
89
|
+
*/
|
|
90
|
+
imageRendering?: 'auto' | 'smooth' | 'pixelated';
|
|
91
|
+
/**
|
|
92
|
+
* Pauses an animated image (GIF, and animated WebP on iOS) on the frame
|
|
93
|
+
* it's showing; `false` plays it again from there. Each image animates on
|
|
94
|
+
* its own, so pausing one doesn't pause others showing the same file.
|
|
95
|
+
*/
|
|
96
|
+
paused?: boolean;
|
|
83
97
|
onLoadStart?(): void;
|
|
84
98
|
onProgress?(event: OnProgressEvent): void;
|
|
85
99
|
onLoad?(event: OnLoadEvent): void;
|
package/dist/index.js.flow
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
// How many times animated images play: -1 for the file's own loop count (the
|
|
26
26
|
// `loop` prop not set), 0 for forever, or a number of times.
|
|
27
27
|
@property (nonatomic, assign) NSInteger loopCount;
|
|
28
|
+
// How the image is filtered when drawn at another size: auto, smooth or
|
|
29
|
+
// pixelated.
|
|
30
|
+
@property (nonatomic, copy) NSString *imageRendering;
|
|
31
|
+
// Pauses animated images on the frame they're showing.
|
|
32
|
+
@property (nonatomic, assign) BOOL paused;
|
|
28
33
|
|
|
29
34
|
@end
|
|
30
35
|
|
|
@@ -53,6 +53,24 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
53
53
|
return self;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
- (void) setImageRendering: (NSString*)imageRendering {
|
|
57
|
+
_imageRendering = [imageRendering copy];
|
|
58
|
+
if ([imageRendering isEqualToString: @"smooth"]) {
|
|
59
|
+
// Trilinear: drawn smaller from mipmaps, so a large image drawn much
|
|
60
|
+
// smaller is smoothed instead of aliased (#445). The mipmaps take
|
|
61
|
+
// about a third more memory than the decoded image.
|
|
62
|
+
self.layer.minificationFilter = kCAFilterTrilinear;
|
|
63
|
+
self.layer.magnificationFilter = kCAFilterLinear;
|
|
64
|
+
} else if ([imageRendering isEqualToString: @"pixelated"]) {
|
|
65
|
+
// Nearest neighbor: sharp pixels, e.g. for pixel art (#926).
|
|
66
|
+
self.layer.minificationFilter = kCAFilterNearest;
|
|
67
|
+
self.layer.magnificationFilter = kCAFilterNearest;
|
|
68
|
+
} else {
|
|
69
|
+
self.layer.minificationFilter = kCAFilterLinear;
|
|
70
|
+
self.layer.magnificationFilter = kCAFilterLinear;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
- (void) setLoopCount: (NSInteger)loopCount {
|
|
57
75
|
if (_loopCount == loopCount) {
|
|
58
76
|
return;
|
|
@@ -67,9 +85,28 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
67
85
|
} else if (self.player && [self.image conformsToProtocol: @protocol(SDAnimatedImage)]) {
|
|
68
86
|
self.player.totalLoopCount = [(id<SDAnimatedImage>) self.image animatedImageLoopCount];
|
|
69
87
|
}
|
|
70
|
-
// Apply it to the image that's showing, and play it again
|
|
88
|
+
// Apply it to the image that's showing, and play it again (unless it's
|
|
89
|
+
// paused: then it waits on the first frame).
|
|
71
90
|
if (self.player) {
|
|
72
91
|
[self.player seekToFrameAtIndex: 0 loopCount: 0];
|
|
92
|
+
if (!_paused) {
|
|
93
|
+
[self startAnimating];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
- (void) setPaused: (BOOL)paused {
|
|
99
|
+
if (_paused == paused) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
_paused = paused;
|
|
103
|
+
// SDAnimatedImageView starts an animated image when it's shown, and again
|
|
104
|
+
// when the view comes back on screen, unless autoPlayAnimatedImage is off.
|
|
105
|
+
// Stopping keeps the frame it's on (resetFrameIndexWhenStopped is off).
|
|
106
|
+
self.autoPlayAnimatedImage = !paused;
|
|
107
|
+
if (paused) {
|
|
108
|
+
[self stopAnimating];
|
|
109
|
+
} else {
|
|
73
110
|
[self startAnimating];
|
|
74
111
|
}
|
|
75
112
|
}
|
|
@@ -19,6 +19,8 @@ RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage)
|
|
|
19
19
|
RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
|
|
20
20
|
RCT_EXPORT_VIEW_PROPERTY(recyclingKey, NSString)
|
|
21
21
|
RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
|
|
22
|
+
RCT_EXPORT_VIEW_PROPERTY(imageRendering, NSString)
|
|
23
|
+
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
|
|
22
24
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
|
|
23
25
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock)
|
|
24
26
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-fast-image",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.12.0",
|
|
4
4
|
"description": "🚩 FastImage, performant React Native image component.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@types/react": "^17.0.14",
|
|
52
52
|
"@types/react-native": "^0.69.5",
|
|
53
53
|
"@types/react-test-renderer": "^17.0.1",
|
|
54
|
+
"conventional-changelog-angular": "8.3.1",
|
|
54
55
|
"maestro-runner": "1.1.27",
|
|
55
56
|
"odiff-bin": "^4.5.0",
|
|
56
57
|
"oxfmt": "0.70.0",
|