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 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
- mOwnGif.stop();
80
- mOwnGif.startFromFirstFrame();
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(FastImageSourceSize.scaleTypeOptions(getScaleType(), capture));
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;
@@ -90,6 +90,8 @@ export type FastImageProps = $ReadOnly<{|
90
90
  fallback?: ?boolean,
91
91
  recyclingKey?: ?string,
92
92
  loop?: ?(boolean | number),
93
+ imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
94
+ paused?: ?boolean,
93
95
  testID?: ?string,
94
96
  |}>
95
97
 
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;
@@ -90,6 +90,8 @@ export type FastImageProps = $ReadOnly<{|
90
90
  fallback?: ?boolean,
91
91
  recyclingKey?: ?string,
92
92
  loop?: ?(boolean | number),
93
+ imageRendering?: ?('auto' | 'smooth' | 'pixelated'),
94
+ paused?: ?boolean,
93
95
  testID?: ?string,
94
96
  |}>
95
97
 
@@ -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.10.0",
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",