react-native-fast-image 8.20.0 → 8.21.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/FastImageBlur.java +213 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageRequestListener.java +6 -1
- package/android/src/main/java/com/dylanvann/fastimage/FastImageSourceSize.java +17 -2
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java +6 -0
- package/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java +88 -9
- package/dist/index.cjs.js.flow +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js.flow +1 -0
- package/ios/FastImage/FFFastImageView.h +2 -0
- package/ios/FastImage/FFFastImageView.m +219 -12
- package/ios/FastImage/FFFastImageViewManager.m +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -222,6 +222,22 @@ Decoding a smaller copy can take a little longer, so use it where the memory mat
|
|
|
222
222
|
|
|
223
223
|
---
|
|
224
224
|
|
|
225
|
+
### `blurRadius?: number`
|
|
226
|
+
|
|
227
|
+
Blurs the image by this radius, in points, like React Native's `Image` (the same radius looks about the same on iOS and Android). `0` **(Default)** is no blur.
|
|
228
|
+
|
|
229
|
+
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.
|
|
230
|
+
|
|
231
|
+
- Only the loaded image is blurred, not `defaultSource`.
|
|
232
|
+
- An animated image (GIF, animated WebP) shows its first frame, blurred, and doesn't animate.
|
|
233
|
+
- With `tintColor`, the blurred image is tinted.
|
|
234
|
+
- 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.
|
|
235
|
+
- Changing it blurs the image that's showing again, without sending the load events again.
|
|
236
|
+
|
|
237
|
+
To animate a blur, or to blur an animated image, use React Native's `filter` style instead, which the GPU draws: `style={{ filter: [{ blur: 6 }] }}`. It needs the New Architecture. React Native's docs list `blur` for Android 12+ only; on iOS it's behind an experimental React Native feature flag (`enableSwiftUIBasedFilters`, SwiftUI-based filters).
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
225
241
|
### `onLoadStart?: () => void`
|
|
226
242
|
|
|
227
243
|
Called when the image starts to load.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
package com.dylanvann.fastimage;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.Canvas;
|
|
5
|
+
import android.graphics.Paint;
|
|
6
|
+
import android.graphics.PorterDuff;
|
|
7
|
+
import android.graphics.PorterDuffXfermode;
|
|
8
|
+
import android.widget.ImageView;
|
|
9
|
+
|
|
10
|
+
import androidx.annotation.NonNull;
|
|
11
|
+
|
|
12
|
+
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
|
|
13
|
+
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
|
14
|
+
import com.bumptech.glide.request.target.Target;
|
|
15
|
+
|
|
16
|
+
import java.nio.IntBuffer;
|
|
17
|
+
import java.nio.charset.Charset;
|
|
18
|
+
import java.security.MessageDigest;
|
|
19
|
+
|
|
20
|
+
// Blurs the image (blurRadius), as React Native's Image does on iOS: three box
|
|
21
|
+
// blurs, close to a Gaussian blur, with the box size worked out from the radius
|
|
22
|
+
// (in pixels) as the SVG spec describes and halved, as React Native does. So a
|
|
23
|
+
// radius looks the same on both platforms. It runs after the scale type's crop
|
|
24
|
+
// or fit, on the image at about the size the view shows it (a blurred image
|
|
25
|
+
// needs no more detail), on Glide's threads. Part of the request's key, so
|
|
26
|
+
// blurred and unblurred views of one image don't share a cached image; the
|
|
27
|
+
// cached file stays the original.
|
|
28
|
+
final class FastImageBlur extends BitmapTransformation {
|
|
29
|
+
private static final String ID = "com.dylanvann.fastimage.FastImageBlur";
|
|
30
|
+
private static final byte[] ID_BYTES = ID.getBytes(Charset.forName("UTF-8"));
|
|
31
|
+
|
|
32
|
+
// In pixels on screen.
|
|
33
|
+
private final float radius;
|
|
34
|
+
private final ImageView.ScaleType scaleType;
|
|
35
|
+
|
|
36
|
+
FastImageBlur(float radius, ImageView.ScaleType scaleType) {
|
|
37
|
+
this.radius = radius;
|
|
38
|
+
this.scaleType = scaleType;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Override
|
|
42
|
+
protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
|
|
43
|
+
int width = toTransform.getWidth();
|
|
44
|
+
int height = toTransform.getHeight();
|
|
45
|
+
if (outWidth == Target.SIZE_ORIGINAL) outWidth = width;
|
|
46
|
+
if (outHeight == Target.SIZE_ORIGINAL) outHeight = height;
|
|
47
|
+
// How many pixels on screen a pixel of the bitmap takes.
|
|
48
|
+
float shownScale = shownScale(width, height, outWidth, outHeight);
|
|
49
|
+
// Blurred at the size it's shown at, or at its own size if it's shown
|
|
50
|
+
// larger.
|
|
51
|
+
float scale = Math.min(shownScale, 1);
|
|
52
|
+
int blurredWidth = Math.max(1, Math.round(width * scale));
|
|
53
|
+
int blurredHeight = Math.max(1, Math.round(height * scale));
|
|
54
|
+
int boxSize = boxSize(radius * scale / shownScale, Math.max(blurredWidth, blurredHeight));
|
|
55
|
+
if (boxSize <= 1 && scale == 1) return toTransform;
|
|
56
|
+
|
|
57
|
+
Bitmap result = pool.get(blurredWidth, blurredHeight, Bitmap.Config.ARGB_8888);
|
|
58
|
+
result.setDensity(toTransform.getDensity());
|
|
59
|
+
Canvas canvas = new Canvas(result);
|
|
60
|
+
canvas.scale(blurredWidth / (float) width, blurredHeight / (float) height);
|
|
61
|
+
canvas.drawBitmap(toTransform, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
|
|
62
|
+
canvas.setBitmap(null);
|
|
63
|
+
if (boxSize > 1) blur(pool, result, boxSize);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Pixels on screen per pixel of the bitmap, for the view's scale type.
|
|
68
|
+
private float shownScale(int width, int height, int outWidth, int outHeight) {
|
|
69
|
+
if (width <= 0 || height <= 0 || outWidth <= 0 || outHeight <= 0) return 1;
|
|
70
|
+
float x = outWidth / (float) width;
|
|
71
|
+
float y = outHeight / (float) height;
|
|
72
|
+
if (scaleType == null) return 1;
|
|
73
|
+
switch (scaleType) {
|
|
74
|
+
case CENTER_CROP:
|
|
75
|
+
return Math.max(x, y);
|
|
76
|
+
case FIT_CENTER:
|
|
77
|
+
case FIT_START:
|
|
78
|
+
case FIT_END:
|
|
79
|
+
return Math.min(x, y);
|
|
80
|
+
case FIT_XY:
|
|
81
|
+
return (float) Math.sqrt(x * y);
|
|
82
|
+
case CENTER_INSIDE:
|
|
83
|
+
return Math.min(Math.min(x, y), 1);
|
|
84
|
+
default:
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// React Native's (RCTImageBlurUtils) for a radius in pixels: odd, so it's
|
|
90
|
+
// centered. At most the image's larger side (a larger box looks the same,
|
|
91
|
+
// and an absurd radius would overflow).
|
|
92
|
+
private static int boxSize(float radius, int max) {
|
|
93
|
+
double size = Math.floor((radius * 3 * Math.sqrt(2 * Math.PI) / 4 + 0.5) / 2);
|
|
94
|
+
return (int) Math.min(size, max) | 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Blurs the bitmap, on its premultiplied pixels (as stored), each byte a
|
|
98
|
+
// channel. A large box blurs a copy 2, 4 or 8 times smaller instead (each
|
|
99
|
+
// pixel the average of a block), with a box that much smaller, of at least
|
|
100
|
+
// 11 pixels, drawn back at full size: away from the edges that matches
|
|
101
|
+
// blurring at full size within a few levels of 255 (about as much as a
|
|
102
|
+
// slightly different radius changes it), for a quarter to a sixty-fourth of
|
|
103
|
+
// the work.
|
|
104
|
+
private static void blur(BitmapPool pool, Bitmap bitmap, int boxSize) {
|
|
105
|
+
int width = bitmap.getWidth();
|
|
106
|
+
int height = bitmap.getHeight();
|
|
107
|
+
int[] pixels = new int[width * height];
|
|
108
|
+
IntBuffer buffer = IntBuffer.wrap(pixels);
|
|
109
|
+
bitmap.copyPixelsToBuffer(buffer);
|
|
110
|
+
int factor = boxSize >= 88 ? 8 : boxSize >= 44 ? 4 : boxSize >= 22 ? 2 : 1;
|
|
111
|
+
if (factor == 1) {
|
|
112
|
+
blur(pixels, width, height, boxSize);
|
|
113
|
+
buffer.rewind();
|
|
114
|
+
bitmap.copyPixelsFromBuffer(buffer);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
int smallWidth = (width + factor - 1) / factor;
|
|
118
|
+
int smallHeight = (height + factor - 1) / factor;
|
|
119
|
+
int[] small = shrink(pixels, width, height, factor, smallWidth, smallHeight);
|
|
120
|
+
blur(small, smallWidth, smallHeight, Math.round((boxSize / (float) factor - 1) / 2) * 2 + 1);
|
|
121
|
+
Bitmap copy = pool.get(smallWidth, smallHeight, Bitmap.Config.ARGB_8888);
|
|
122
|
+
copy.copyPixelsFromBuffer(IntBuffer.wrap(small));
|
|
123
|
+
Canvas canvas = new Canvas(bitmap);
|
|
124
|
+
canvas.scale(factor, factor);
|
|
125
|
+
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
|
126
|
+
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
|
|
127
|
+
canvas.drawBitmap(copy, 0, 0, paint);
|
|
128
|
+
canvas.setBitmap(null);
|
|
129
|
+
pool.put(copy);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Three box blurs, each across then down, with the edges extended.
|
|
133
|
+
private static void blur(int[] pixels, int width, int height, int boxSize) {
|
|
134
|
+
int[] other = new int[width * height];
|
|
135
|
+
int radius = boxSize / 2;
|
|
136
|
+
for (int i = 0; i < 3; i++) {
|
|
137
|
+
for (int y = 0; y < height; y++) boxBlur(pixels, other, y * width, 1, width, radius);
|
|
138
|
+
for (int x = 0; x < width; x++) boxBlur(other, pixels, x, width, height, radius);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// A copy factor times smaller: each pixel the average of a factor × factor
|
|
143
|
+
// block (smaller at the right and bottom edges).
|
|
144
|
+
private static int[] shrink(int[] pixels, int width, int height, int factor, int smallWidth, int smallHeight) {
|
|
145
|
+
int[] small = new int[smallWidth * smallHeight];
|
|
146
|
+
for (int sy = 0; sy < smallHeight; sy++) {
|
|
147
|
+
for (int sx = 0; sx < smallWidth; sx++) {
|
|
148
|
+
int a = 0, b = 0, c = 0, d = 0, n = 0;
|
|
149
|
+
for (int y = sy * factor; y < Math.min(height, (sy + 1) * factor); y++) {
|
|
150
|
+
for (int x = sx * factor; x < Math.min(width, (sx + 1) * factor); x++) {
|
|
151
|
+
int pixel = pixels[y * width + x];
|
|
152
|
+
a += pixel >>> 24;
|
|
153
|
+
b += (pixel >> 16) & 0xff;
|
|
154
|
+
c += (pixel >> 8) & 0xff;
|
|
155
|
+
d += pixel & 0xff;
|
|
156
|
+
n++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
int half = n / 2;
|
|
160
|
+
small[sy * smallWidth + sx] = ((a + half) / n) << 24 | ((b + half) / n) << 16
|
|
161
|
+
| ((c + half) / n) << 8 | ((d + half) / n);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return small;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Averages each pixel of a row or column (count pixels from start, step
|
|
168
|
+
// apart) with the radius pixels on each side.
|
|
169
|
+
private static void boxBlur(int[] from, int[] to, int start, int step, int count, int radius) {
|
|
170
|
+
int size = 2 * radius + 1;
|
|
171
|
+
int half = size / 2;
|
|
172
|
+
int a = 0, b = 0, c = 0, d = 0;
|
|
173
|
+
for (int i = -radius; i <= radius; i++) {
|
|
174
|
+
int pixel = from[start + clamp(i, count) * step];
|
|
175
|
+
a += pixel >>> 24;
|
|
176
|
+
b += (pixel >> 16) & 0xff;
|
|
177
|
+
c += (pixel >> 8) & 0xff;
|
|
178
|
+
d += pixel & 0xff;
|
|
179
|
+
}
|
|
180
|
+
for (int i = 0; i < count; i++) {
|
|
181
|
+
to[start + i * step] = ((a + half) / size) << 24 | ((b + half) / size) << 16
|
|
182
|
+
| ((c + half) / size) << 8 | ((d + half) / size);
|
|
183
|
+
int added = from[start + clamp(i + radius + 1, count) * step];
|
|
184
|
+
int removed = from[start + clamp(i - radius, count) * step];
|
|
185
|
+
a += (added >>> 24) - (removed >>> 24);
|
|
186
|
+
b += ((added >> 16) & 0xff) - ((removed >> 16) & 0xff);
|
|
187
|
+
c += ((added >> 8) & 0xff) - ((removed >> 8) & 0xff);
|
|
188
|
+
d += (added & 0xff) - (removed & 0xff);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private static int clamp(int i, int count) {
|
|
193
|
+
return i < 0 ? 0 : i >= count ? count - 1 : i;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Override
|
|
197
|
+
public boolean equals(Object o) {
|
|
198
|
+
return o instanceof FastImageBlur
|
|
199
|
+
&& ((FastImageBlur) o).radius == radius
|
|
200
|
+
&& ((FastImageBlur) o).scaleType == scaleType;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@Override
|
|
204
|
+
public int hashCode() {
|
|
205
|
+
return ID.hashCode() * 31 + Float.floatToIntBits(radius) * 31 + (scaleType == null ? 0 : scaleType.ordinal());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@Override
|
|
209
|
+
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
|
210
|
+
messageDigest.update(ID_BYTES);
|
|
211
|
+
messageDigest.update((radius + "," + scaleType).getBytes(Charset.forName("UTF-8")));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -34,7 +34,12 @@ public class FastImageRequestListener implements RequestListener<Drawable> {
|
|
|
34
34
|
|
|
35
35
|
@Override
|
|
36
36
|
public boolean onLoadFailed(@androidx.annotation.Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
|
|
37
|
-
if (!events)
|
|
37
|
+
if (!events) {
|
|
38
|
+
if (target instanceof ImageViewTarget) {
|
|
39
|
+
((FastImageViewWithUrl) ((ImageViewTarget) target).getView()).onQuietLoadFailed();
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
38
43
|
FastImageOkHttpProgressGlideModule.forget(key);
|
|
39
44
|
if (!(target instanceof ImageViewTarget)) {
|
|
40
45
|
return false;
|
|
@@ -14,7 +14,12 @@ import androidx.annotation.Nullable;
|
|
|
14
14
|
|
|
15
15
|
import com.bumptech.glide.Glide;
|
|
16
16
|
import com.bumptech.glide.load.ImageHeaderParserUtils;
|
|
17
|
+
import com.bumptech.glide.load.MultiTransformation;
|
|
18
|
+
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
|
19
|
+
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
|
20
|
+
import com.bumptech.glide.load.resource.bitmap.CenterInside;
|
|
17
21
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
|
22
|
+
import com.bumptech.glide.load.resource.bitmap.FitCenter;
|
|
18
23
|
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
|
19
24
|
import com.bumptech.glide.request.RequestOptions;
|
|
20
25
|
|
|
@@ -82,27 +87,37 @@ final class FastImageSourceSize {
|
|
|
82
87
|
|
|
83
88
|
// What RequestBuilder.into(ImageView) applies for the view's scale type
|
|
84
89
|
// (it leaves a transformation that's already set alone), with the
|
|
85
|
-
// downsample strategy it implies wrapped by capture
|
|
86
|
-
|
|
90
|
+
// downsample strategy it implies wrapped by capture, and then blur if it
|
|
91
|
+
// isn't null.
|
|
92
|
+
static RequestOptions scaleTypeOptions(@Nullable ImageView.ScaleType scaleType, Capture capture, @Nullable FastImageBlur blur) {
|
|
87
93
|
RequestOptions options = new RequestOptions();
|
|
94
|
+
BitmapTransformation transformation = null;
|
|
88
95
|
if (scaleType != null) {
|
|
89
96
|
switch (scaleType) {
|
|
90
97
|
case CENTER_CROP:
|
|
91
98
|
options = options.optionalCenterCrop();
|
|
99
|
+
transformation = new CenterCrop();
|
|
92
100
|
break;
|
|
93
101
|
case CENTER_INSIDE:
|
|
94
102
|
case FIT_XY:
|
|
95
103
|
options = options.optionalCenterInside();
|
|
104
|
+
transformation = new CenterInside();
|
|
96
105
|
break;
|
|
97
106
|
case FIT_CENTER:
|
|
98
107
|
case FIT_START:
|
|
99
108
|
case FIT_END:
|
|
100
109
|
options = options.optionalFitCenter();
|
|
110
|
+
transformation = new FitCenter();
|
|
101
111
|
break;
|
|
102
112
|
default:
|
|
103
113
|
break;
|
|
104
114
|
}
|
|
105
115
|
}
|
|
116
|
+
if (blur != null) {
|
|
117
|
+
options = transformation == null
|
|
118
|
+
? options.optionalTransform(blur)
|
|
119
|
+
: options.optionalTransform(new MultiTransformation<>(transformation, blur));
|
|
120
|
+
}
|
|
106
121
|
return options.downsample(capture);
|
|
107
122
|
}
|
|
108
123
|
|
|
@@ -22,6 +22,7 @@ import com.facebook.react.uimanager.LayoutShadowNode;
|
|
|
22
22
|
import com.facebook.react.uimanager.SimpleViewManager;
|
|
23
23
|
import com.facebook.react.bridge.ReactContext;
|
|
24
24
|
import com.facebook.react.uimanager.ThemedReactContext;
|
|
25
|
+
import com.facebook.react.uimanager.PixelUtil;
|
|
25
26
|
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
26
27
|
import com.facebook.react.views.imagehelper.ResourceDrawableIdHelper;
|
|
27
28
|
|
|
@@ -99,6 +100,11 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
|
|
|
99
100
|
view.setImageRendering(imageRendering);
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
@ReactProp(name = "blurRadius")
|
|
104
|
+
public void setBlurRadius(FastImageViewWithUrl view, float blurRadius) {
|
|
105
|
+
view.setBlurRadius(PixelUtil.toPixelFromDIP(blurRadius));
|
|
106
|
+
}
|
|
107
|
+
|
|
102
108
|
@ReactProp(name = "paused")
|
|
103
109
|
public void setPaused(FastImageViewWithUrl view, boolean paused) {
|
|
104
110
|
view.setPaused(paused);
|
|
@@ -202,19 +202,50 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
202
202
|
mNeedsReload = true;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// blurRadius, in pixels (0 is no blur). Part of the request (see
|
|
206
|
+
// reblur for a change).
|
|
207
|
+
private float mBlurRadius = 0;
|
|
208
|
+
private boolean mBlurChanged = false;
|
|
209
|
+
|
|
210
|
+
public void setBlurRadius(float blurRadius) {
|
|
211
|
+
if (blurRadius == mBlurRadius) return;
|
|
212
|
+
mBlurRadius = blurRadius;
|
|
213
|
+
mBlurChanged = true;
|
|
214
|
+
}
|
|
215
|
+
|
|
205
216
|
// The scale type's options (what into() would apply), or none when
|
|
206
|
-
// pixelated.
|
|
217
|
+
// pixelated, and the blur. A blurred animated image shows its first frame,
|
|
218
|
+
// as on iOS.
|
|
207
219
|
private RequestOptions renderingOptions(Object model) {
|
|
220
|
+
FastImageBlur blur = mBlurRadius > 0 ? new FastImageBlur(mBlurRadius, getScaleType()) : null;
|
|
221
|
+
RequestOptions options;
|
|
208
222
|
if (mPixelated) {
|
|
209
223
|
// Decoded as it is, then scaled by the view without filtering (see
|
|
210
224
|
// OwnGifTarget).
|
|
211
|
-
|
|
225
|
+
options = new RequestOptions()
|
|
212
226
|
.downsample(new FastImageSourceSize.Capture(DownsampleStrategy.NONE, String.valueOf(model)))
|
|
213
227
|
.dontTransform();
|
|
228
|
+
if (blur != null) options = options.optionalTransform(blur);
|
|
229
|
+
} else {
|
|
230
|
+
options = FastImageSourceSize.scaleTypeOptions(
|
|
231
|
+
getScaleType(), FastImageSourceSize.capture(getScaleType(), model), blur);
|
|
214
232
|
}
|
|
215
|
-
return
|
|
233
|
+
return blur != null ? options.dontAnimate() : options;
|
|
216
234
|
}
|
|
217
235
|
|
|
236
|
+
// The request the loading one was made from, before renderingOptions, and
|
|
237
|
+
// its model, to make it again with another blur (see reblur).
|
|
238
|
+
@Nullable
|
|
239
|
+
private RequestBuilder<Drawable> mBaseRequest;
|
|
240
|
+
@Nullable
|
|
241
|
+
private Object mModel;
|
|
242
|
+
// Whether the loading image has loaded or failed.
|
|
243
|
+
private boolean mLoadEnded = false;
|
|
244
|
+
// Set by reblur: the next load is the same one again, so it doesn't send
|
|
245
|
+
// onLoadStart (it sends its other events, as the load it replaces would
|
|
246
|
+
// have).
|
|
247
|
+
private boolean mRestarting = false;
|
|
248
|
+
|
|
218
249
|
// The request for the image the view shows once it has loaded, and the one
|
|
219
250
|
// loading. A new source starts with the shown one as a thumbnail, from the
|
|
220
251
|
// cache only, so the image stays until the new one has loaded instead of
|
|
@@ -255,6 +286,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
255
286
|
|
|
256
287
|
// The loading image loaded (FastImageRequestListener).
|
|
257
288
|
void onImageLoaded() {
|
|
289
|
+
mLoadEnded = true;
|
|
258
290
|
mShownRequest = mLoadingRequest;
|
|
259
291
|
mShownWidth = mLoadingWidth;
|
|
260
292
|
mShownHeight = mLoadingHeight;
|
|
@@ -310,6 +342,34 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
310
342
|
into(shown, builder, meanwhile);
|
|
311
343
|
}
|
|
312
344
|
|
|
345
|
+
// blurRadius changed, and nothing else that needs a reload: the image
|
|
346
|
+
// that's showing, blurred again, with no events, as reloadForSize does: it's
|
|
347
|
+
// decoded again from the cache and blurred on Glide's threads, and the view
|
|
348
|
+
// keeps the current image until then (or if that fails). A load that
|
|
349
|
+
// hasn't finished starts again with the new blur, without a second
|
|
350
|
+
// onLoadStart. Nothing loaded (defaultSource, which isn't blurred, or
|
|
351
|
+
// nothing): the next load has the new blur.
|
|
352
|
+
@SuppressLint("CheckResult")
|
|
353
|
+
private void reblur() {
|
|
354
|
+
if (requestManager == null || mBaseRequest == null) return;
|
|
355
|
+
RequestBuilder<Drawable> shown = mShownRequest;
|
|
356
|
+
if (shown != null && shown == mLoadingRequest) {
|
|
357
|
+
RequestBuilder<Drawable> request = mBaseRequest.clone().apply(renderingOptions(mModel));
|
|
358
|
+
RequestBuilder<Drawable> current = fromCache(shown);
|
|
359
|
+
Drawable meanwhile = shownCopy(shown);
|
|
360
|
+
mLoadCount++;
|
|
361
|
+
clearView(requestManager);
|
|
362
|
+
RequestBuilder<Drawable> builder = request.clone()
|
|
363
|
+
.error(current.clone())
|
|
364
|
+
.listener(new FastImageRequestListener(null, null, true, false));
|
|
365
|
+
if (meanwhile == null) builder = builder.thumbnail(current);
|
|
366
|
+
into(request, builder, meanwhile);
|
|
367
|
+
} else if (mLoadingRequest != null && !mLoadEnded) {
|
|
368
|
+
mRestarting = true;
|
|
369
|
+
mNeedsReload = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
313
373
|
// A copy of the image showing, for the view to show while the next one
|
|
314
374
|
// loads, when it isn't in Glide's memory cache (source.memoryCache false):
|
|
315
375
|
// a thumbnail of it would come from the disk, after the view has cleared,
|
|
@@ -346,6 +406,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
346
406
|
private void into(RequestBuilder<Drawable> request, RequestBuilder<Drawable> builder, @Nullable Drawable meanwhile) {
|
|
347
407
|
final int load = mLoadCount;
|
|
348
408
|
mLoadingRequest = request;
|
|
409
|
+
mLoadEnded = false;
|
|
349
410
|
mLoadingWidth = 0;
|
|
350
411
|
mLoadingHeight = 0;
|
|
351
412
|
OwnGifTarget target = new OwnGifTarget(meanwhile);
|
|
@@ -360,9 +421,16 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
360
421
|
builder.into(target);
|
|
361
422
|
}
|
|
362
423
|
|
|
424
|
+
// Loading the image that's showing again (at a new size, or with another
|
|
425
|
+
// blur) failed: the view keeps showing it (FastImageRequestListener).
|
|
426
|
+
void onQuietLoadFailed() {
|
|
427
|
+
mLoadEnded = true;
|
|
428
|
+
}
|
|
429
|
+
|
|
363
430
|
// The loading image failed (FastImageRequestListener). Glide shows
|
|
364
431
|
// defaultSource then, but not over a thumbnail: show it here, as iOS does.
|
|
365
432
|
void onImageFailed(boolean hadThumbnail) {
|
|
433
|
+
mLoadEnded = true;
|
|
366
434
|
mShownRequest = null;
|
|
367
435
|
if (!hadThumbnail) return;
|
|
368
436
|
final int load = mLoadCount;
|
|
@@ -380,15 +448,24 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
380
448
|
@Nonnull FastImageViewManager manager,
|
|
381
449
|
@Nullable RequestManager requestManager,
|
|
382
450
|
@Nonnull Map<String, List<FastImageViewWithUrl>> viewsForUrlsMap) {
|
|
451
|
+
if (mBlurChanged && !mNeedsReload) {
|
|
452
|
+
mBlurChanged = false;
|
|
453
|
+
reblur();
|
|
454
|
+
}
|
|
383
455
|
if (!mNeedsReload)
|
|
384
456
|
return;
|
|
385
457
|
// Only reload for changes that affect the request (source,
|
|
386
458
|
// defaultSource, resizeMode), not for every prop update.
|
|
387
459
|
mNeedsReload = false;
|
|
460
|
+
mBlurChanged = false;
|
|
461
|
+
boolean restarting = mRestarting;
|
|
462
|
+
mRestarting = false;
|
|
388
463
|
mLoadCount++;
|
|
389
464
|
RequestBuilder<Drawable> shownRequest = mShownRequest;
|
|
390
465
|
mShownRequest = null;
|
|
391
466
|
mLoadingRequest = null;
|
|
467
|
+
mBaseRequest = null;
|
|
468
|
+
mModel = null;
|
|
392
469
|
|
|
393
470
|
// Nothing to show.
|
|
394
471
|
if (mSource == null && mDefaultSource == null) {
|
|
@@ -453,7 +530,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
453
530
|
}
|
|
454
531
|
}
|
|
455
532
|
|
|
456
|
-
if (imageSource != null) {
|
|
533
|
+
if (imageSource != null && !restarting) {
|
|
457
534
|
// This is an orphan even without a load/loadend when only loading a placeholder
|
|
458
535
|
FastImageEvents.send(this, FastImageViewManager.REACT_ON_LOAD_START_EVENT);
|
|
459
536
|
}
|
|
@@ -461,7 +538,7 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
461
538
|
if (requestManager != null) {
|
|
462
539
|
// Records the image's own size when Glide decodes it, for onLoad.
|
|
463
540
|
Object model = imageSource == null ? null : imageSource.getSourceForLoad();
|
|
464
|
-
RequestBuilder<Drawable>
|
|
541
|
+
RequestBuilder<Drawable> base =
|
|
465
542
|
requestManager
|
|
466
543
|
// This will make this work for remote and local images. e.g.
|
|
467
544
|
// - file:///
|
|
@@ -473,10 +550,12 @@ class FastImageViewWithUrl extends AppCompatImageView {
|
|
|
473
550
|
.apply(FastImageViewConverter
|
|
474
551
|
.getOptions(getContext(), imageSource, mSource)
|
|
475
552
|
.placeholder(mDefaultSource) // show until loaded
|
|
476
|
-
.fallback(mDefaultSource)) // null will not be treated as error
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
553
|
+
.fallback(mDefaultSource)); // null will not be treated as error
|
|
554
|
+
mBaseRequest = model == null ? null : base;
|
|
555
|
+
mModel = model;
|
|
556
|
+
// What into() would apply for the scale type, with the size
|
|
557
|
+
// capture, for imageRendering, and the blur.
|
|
558
|
+
RequestBuilder<Drawable> builder = base.clone().apply(renderingOptions(model));
|
|
480
559
|
RequestBuilder<Drawable> request = builder.clone();
|
|
481
560
|
|
|
482
561
|
if (model == null) meanwhile = null;
|
package/dist/index.cjs.js.flow
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -142,6 +142,14 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
|
|
|
142
142
|
* or with an image CDN instead). See the README.
|
|
143
143
|
*/
|
|
144
144
|
downsample?: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Blurs the image by this radius, in points, like React Native's Image
|
|
147
|
+
* (the same radius looks about the same on both platforms). `0` (default)
|
|
148
|
+
* is no blur. For still images or occasional changes, not for animating.
|
|
149
|
+
* Only the loaded image is blurred, not `defaultSource`. An animated image
|
|
150
|
+
* shows its first frame, blurred. See the README.
|
|
151
|
+
*/
|
|
152
|
+
blurRadius?: number;
|
|
145
153
|
onLoadStart?(): void;
|
|
146
154
|
onProgress?(event: OnProgressEvent): void;
|
|
147
155
|
onLoad?(event: OnLoadEvent): void;
|
package/dist/index.js.flow
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
@property (nonatomic, assign) BOOL paused;
|
|
33
33
|
// Decodes images at about the view's size instead of at full size.
|
|
34
34
|
@property (nonatomic, assign) BOOL downsample;
|
|
35
|
+
// Blurs the loaded image by this radius, in points (0 is no blur).
|
|
36
|
+
@property (nonatomic, assign) CGFloat blurRadius;
|
|
35
37
|
|
|
36
38
|
@end
|
|
37
39
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#import <SDWebImage/SDWebImageError.h>
|
|
6
6
|
#import <SDWebImage/SDImageTransformer.h>
|
|
7
7
|
#import "FFFDownsampledImage.h"
|
|
8
|
+
#import <Accelerate/Accelerate.h>
|
|
8
9
|
|
|
9
10
|
@interface FFFastImageView ()
|
|
10
11
|
|
|
@@ -16,9 +17,15 @@
|
|
|
16
17
|
|
|
17
18
|
@property(nonatomic, strong) NSDictionary* onLoadEvent;
|
|
18
19
|
@property(nonatomic, strong) NSDictionary* onErrorEvent;
|
|
19
|
-
// The image before tinting, kept while
|
|
20
|
-
// changed or removed. nil when
|
|
21
|
-
@property(nonatomic, strong) UIImage*
|
|
20
|
+
// The image before tinting and blurring, kept while either is applied so they
|
|
21
|
+
// can be changed or removed. nil when neither is (super.image is the image).
|
|
22
|
+
@property(nonatomic, strong) UIImage* originalImage;
|
|
23
|
+
// Counts images set, so a blur that finishes can tell if it's still current.
|
|
24
|
+
@property(nonatomic, assign) NSUInteger imageCount;
|
|
25
|
+
// The view's size when the image showing was blurred.
|
|
26
|
+
@property(nonatomic, assign) CGSize blurredForSize;
|
|
27
|
+
// The blurred image showing (untinted), so a tint change can re-show it.
|
|
28
|
+
@property(nonatomic, strong) UIImage* blurredImage;
|
|
22
29
|
// Whether the current load was already restarted after the app came back
|
|
23
30
|
// from the background (see downloadImage:).
|
|
24
31
|
@property(nonatomic, assign) BOOL retriedAfterBackground;
|
|
@@ -41,6 +48,8 @@
|
|
|
41
48
|
// When the app last went to the background (CACurrentMediaTime), or 0.
|
|
42
49
|
static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
43
50
|
|
|
51
|
+
static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, BOOL downscale);
|
|
52
|
+
|
|
44
53
|
@implementation FFFastImageView
|
|
45
54
|
|
|
46
55
|
+ (void) initialize {
|
|
@@ -128,6 +137,10 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
128
137
|
if (_resizeMode != resizeMode) {
|
|
129
138
|
_resizeMode = resizeMode;
|
|
130
139
|
[self updateContentMode];
|
|
140
|
+
// The blur is made for the size the image is shown at.
|
|
141
|
+
if ([self blurs: self.originalImage]) {
|
|
142
|
+
[self setImage: self.originalImage];
|
|
143
|
+
}
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
146
|
|
|
@@ -153,6 +166,7 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
153
166
|
- (void) layoutSubviews {
|
|
154
167
|
[super layoutSubviews];
|
|
155
168
|
[self updateContentMode];
|
|
169
|
+
[self blurAgainIfResized];
|
|
156
170
|
if (self.waitsForSize) {
|
|
157
171
|
if ([self hasSize]) {
|
|
158
172
|
[self reloadImage];
|
|
@@ -198,8 +212,16 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
198
212
|
|
|
199
213
|
- (void) setImageColor: (UIColor*)imageColor {
|
|
200
214
|
_imageColor = imageColor;
|
|
201
|
-
//
|
|
202
|
-
|
|
215
|
+
// A blurred image is a still image: show it with the new tint, without
|
|
216
|
+
// blurring it again.
|
|
217
|
+
if ([self blurs: self.originalImage]) {
|
|
218
|
+
if (self.blurredImage) {
|
|
219
|
+
[self showImage: self.blurredImage];
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Re-apply to the original image, so the tint can change or be removed.
|
|
224
|
+
UIImage* image = self.originalImage ?: super.image;
|
|
203
225
|
if (image) {
|
|
204
226
|
// SDAnimatedImageView ignores the image it already shows, which would
|
|
205
227
|
// keep an animated image's frames as they were tinted.
|
|
@@ -215,14 +237,26 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
215
237
|
}
|
|
216
238
|
|
|
217
239
|
- (void) setImage: (UIImage*)image {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
self.imageCount++;
|
|
241
|
+
if ([self blurs: image]) {
|
|
242
|
+
self.originalImage = image;
|
|
243
|
+
self.blurredImage = nil;
|
|
244
|
+
[self blurImage: image];
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
self.originalImage = image && self.imageColor ? image : nil;
|
|
248
|
+
self.blurredImage = nil;
|
|
249
|
+
[self showImage: image];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Shows the image, tinted as React Native's Image does it: UIKit draws a
|
|
253
|
+
// template image in the view's tintColor, so no tinted copy of the image is
|
|
254
|
+
// made. SDAnimatedImageView draws the frames of an animated image itself,
|
|
255
|
+
// without the tint, so each frame is tinted as it's decoded (source-in, as the
|
|
256
|
+
// template is), or, before SDWebImage 5.20, the first frame shows.
|
|
257
|
+
- (void) showImage: (UIImage*)image {
|
|
223
258
|
UIColor* tint = image ? self.imageColor : nil;
|
|
224
259
|
BOOL animated = [image conformsToProtocol: @protocol(SDAnimatedImage)] && [(id<SDAnimatedImage>)image animatedImageFrameCount] > 1;
|
|
225
|
-
self.untintedImage = tint ? image : nil;
|
|
226
260
|
self.tintColor = tint;
|
|
227
261
|
if ([self tintsFrames]) {
|
|
228
262
|
[self setValue: tint && animated ? [SDImageTintTransformer transformerWithColor: tint] : nil forKey: @"animationTransformer"];
|
|
@@ -247,6 +281,179 @@ static CFTimeInterval FFFEnteredBackgroundAt = 0;
|
|
|
247
281
|
[self updateContentMode];
|
|
248
282
|
}
|
|
249
283
|
|
|
284
|
+
- (void) setBlurRadius: (CGFloat)blurRadius {
|
|
285
|
+
if (_blurRadius == blurRadius) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
_blurRadius = blurRadius;
|
|
289
|
+
// Re-apply to the original image, so the blur can change or be removed.
|
|
290
|
+
UIImage* image = self.originalImage ?: super.image;
|
|
291
|
+
if (image) {
|
|
292
|
+
[self setImage: image];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Whether the image is blurred: a loaded image, not defaultSource (as with
|
|
297
|
+
// React Native's Image, and on Android).
|
|
298
|
+
- (BOOL) blurs: (UIImage*)image {
|
|
299
|
+
return _blurRadius > 0 && image && image != _defaultSource;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Blurs the image off the main thread, and shows it (tinted as any image) unless
|
|
303
|
+
// another image was set meanwhile. Until then the view keeps showing what it
|
|
304
|
+
// showed: never the image without the blur. An animated image shows its first
|
|
305
|
+
// frame.
|
|
306
|
+
- (void) blurImage: (UIImage*)image {
|
|
307
|
+
// Not laid out yet (an image from the memory cache is set before the
|
|
308
|
+
// view's first layout): it's blurred for its size once it has one
|
|
309
|
+
// (blurAgainIfResized), instead of twice.
|
|
310
|
+
if (![self hasSize]) {
|
|
311
|
+
self.blurredForSize = CGSizeZero;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
CGFloat screenScale = self.window.screen.scale ?: [UIScreen mainScreen].scale;
|
|
315
|
+
// How many pixels on screen a pixel of the image takes.
|
|
316
|
+
CGFloat shownScale = [self shownScale: image] * screenScale / image.scale;
|
|
317
|
+
// Blurred at the size it's shown at (a blurred image needs no more
|
|
318
|
+
// detail), or at its own size if it's shown larger.
|
|
319
|
+
CGFloat scale = MIN(shownScale, 1);
|
|
320
|
+
// The radius in pixels of the image that's blurred.
|
|
321
|
+
CGFloat radius = _blurRadius * screenScale * scale / shownScale;
|
|
322
|
+
// Pixelated images are drawn larger without smoothing, so a smaller copy
|
|
323
|
+
// would show its pixels.
|
|
324
|
+
BOOL downscale = ![_imageRendering isEqualToString: @"pixelated"];
|
|
325
|
+
NSUInteger count = self.imageCount;
|
|
326
|
+
self.blurredForSize = self.bounds.size;
|
|
327
|
+
__weak typeof(self) weakSelf = self;
|
|
328
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
329
|
+
UIImage* blurred = FFFBlurredImage(image, scale, radius, downscale);
|
|
330
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
331
|
+
[weakSelf showBlurredImage: blurred count: count];
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
- (void) showBlurredImage: (UIImage*)image count: (NSUInteger)count {
|
|
337
|
+
if (self.imageCount == count) {
|
|
338
|
+
self.blurredImage = image;
|
|
339
|
+
[self showImage: image];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Points on screen per point of the image, for the view's size and
|
|
344
|
+
// resizeMode, or 1 before the view has a size.
|
|
345
|
+
- (CGFloat) shownScale: (UIImage*)image {
|
|
346
|
+
CGSize size = image.size;
|
|
347
|
+
CGSize view = self.bounds.size;
|
|
348
|
+
if (size.width <= 0 || size.height <= 0 || view.width <= 0 || view.height <= 0) {
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
CGFloat x = view.width / size.width;
|
|
352
|
+
CGFloat y = view.height / size.height;
|
|
353
|
+
switch (_resizeMode) {
|
|
354
|
+
case RCTResizeModeCover:
|
|
355
|
+
return MAX(x, y);
|
|
356
|
+
case RCTResizeModeContain:
|
|
357
|
+
return MIN(x, y);
|
|
358
|
+
case RCTResizeModeStretch:
|
|
359
|
+
return sqrt(x * y);
|
|
360
|
+
case RCTResizeModeCenter:
|
|
361
|
+
return MIN(MIN(x, y), 1);
|
|
362
|
+
default:
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The view's size changed a lot (by more than a fifth either way) since the
|
|
368
|
+
// image was blurred, which was for the size it was shown at then: blur it
|
|
369
|
+
// again for the new size.
|
|
370
|
+
- (void) blurAgainIfResized {
|
|
371
|
+
if (![self blurs: self.originalImage] || ![self hasSize]) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
CGSize view = self.bounds.size;
|
|
375
|
+
CGSize blurred = self.blurredForSize;
|
|
376
|
+
if (blurred.width > 0 && blurred.height > 0 &&
|
|
377
|
+
view.width <= blurred.width * 1.2 && blurred.width <= view.width * 1.2 &&
|
|
378
|
+
view.height <= blurred.height * 1.2 && blurred.height <= view.height * 1.2) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
[self setImage: self.originalImage];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// The image (its first frame, for an animated image) drawn at scale times its
|
|
385
|
+
// size in pixels, and blurred as React Native's Image blurs on iOS: three box
|
|
386
|
+
// blurs, close to a Gaussian blur, with the box size worked out from the
|
|
387
|
+
// radius (in pixels) as the SVG spec describes and halved, as React Native
|
|
388
|
+
// does. A large box blurs a copy 2, 4 or 8 times smaller instead, with a box
|
|
389
|
+
// that much smaller, of at least 11 pixels: away from the edges that matches
|
|
390
|
+
// blurring at full size within a few levels of 255 (about as much as a
|
|
391
|
+
// slightly different radius changes it), for a quarter to a sixty-fourth of
|
|
392
|
+
// the work. It keeps the image's size in points.
|
|
393
|
+
static UIImage* FFFBlurredImage(UIImage* image, CGFloat scale, CGFloat radius, BOOL downscale)
|
|
394
|
+
{
|
|
395
|
+
if (image.size.width <= 0 || image.size.height <= 0) {
|
|
396
|
+
return image;
|
|
397
|
+
}
|
|
398
|
+
double fullWidth = MAX(1, round(image.size.width * image.scale * scale));
|
|
399
|
+
double fullHeight = MAX(1, round(image.size.height * image.scale * scale));
|
|
400
|
+
// At most the image's larger side (a larger box looks the same, and an
|
|
401
|
+
// absurd radius would overflow).
|
|
402
|
+
double box = MIN(floor((radius * 3 * sqrt(2 * M_PI) / 4 + 0.5) / 2), MAX(fullWidth, fullHeight));
|
|
403
|
+
double factor = !downscale ? 1 : box >= 88 ? 8 : box >= 44 ? 4 : box >= 22 ? 2 : 1;
|
|
404
|
+
size_t width = MAX(1, (size_t) round(fullWidth / factor));
|
|
405
|
+
size_t height = MAX(1, (size_t) round(fullHeight / factor));
|
|
406
|
+
// Odd, so it's centered.
|
|
407
|
+
uint32_t boxSize = factor == 1 ? (uint32_t) box | 1 : (uint32_t) round((box / factor - 1) / 2) * 2 + 1;
|
|
408
|
+
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
409
|
+
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, colorSpace,
|
|
410
|
+
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host);
|
|
411
|
+
CGColorSpaceRelease(colorSpace);
|
|
412
|
+
if (!context) {
|
|
413
|
+
return image;
|
|
414
|
+
}
|
|
415
|
+
// Drawn as UIKit draws (top-left origin), so it follows the image's
|
|
416
|
+
// orientation.
|
|
417
|
+
CGContextTranslateCTM(context, 0, height);
|
|
418
|
+
CGContextScaleCTM(context, 1, -1);
|
|
419
|
+
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
|
|
420
|
+
UIGraphicsPushContext(context);
|
|
421
|
+
[image drawInRect: CGRectMake(0, 0, width, height)];
|
|
422
|
+
UIGraphicsPopContext();
|
|
423
|
+
|
|
424
|
+
if (boxSize > 1) {
|
|
425
|
+
vImage_Buffer buffer = {
|
|
426
|
+
.data = CGBitmapContextGetData(context),
|
|
427
|
+
.height = height,
|
|
428
|
+
.width = width,
|
|
429
|
+
.rowBytes = CGBitmapContextGetBytesPerRow(context),
|
|
430
|
+
};
|
|
431
|
+
vImage_Buffer other = buffer;
|
|
432
|
+
other.data = malloc(buffer.rowBytes * height);
|
|
433
|
+
vImage_Error tempSize = vImageBoxConvolve_ARGB8888(&buffer, &other, NULL, 0, 0, boxSize, boxSize, NULL,
|
|
434
|
+
kvImageGetTempBufferSize | kvImageEdgeExtend);
|
|
435
|
+
void* temp = tempSize > 0 ? malloc(tempSize) : NULL;
|
|
436
|
+
if (other.data && temp) {
|
|
437
|
+
vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
438
|
+
vImageBoxConvolve_ARGB8888(&other, &buffer, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
439
|
+
vImageBoxConvolve_ARGB8888(&buffer, &other, temp, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend);
|
|
440
|
+
memcpy(buffer.data, other.data, buffer.rowBytes * height);
|
|
441
|
+
}
|
|
442
|
+
free(other.data);
|
|
443
|
+
free(temp);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
CGImageRef cgImage = CGBitmapContextCreateImage(context);
|
|
447
|
+
CGContextRelease(context);
|
|
448
|
+
if (!cgImage) {
|
|
449
|
+
return image;
|
|
450
|
+
}
|
|
451
|
+
// The same size in points, also when it's smaller.
|
|
452
|
+
UIImage* blurred = [UIImage imageWithCGImage: cgImage scale: width / image.size.width orientation: UIImageOrientationUp];
|
|
453
|
+
CGImageRelease(cgImage);
|
|
454
|
+
return blurred;
|
|
455
|
+
}
|
|
456
|
+
|
|
250
457
|
// The error's description, with the HTTP status code when there is one (as on
|
|
251
458
|
// Android): SDWebImage keeps the code out of the description. Also for preload
|
|
252
459
|
// results.
|
|
@@ -381,7 +588,7 @@ NSString *FFFErrorMessage(NSError *error)
|
|
|
381
588
|
return;
|
|
382
589
|
}
|
|
383
590
|
// Already at full size (it's no larger than the view it was decoded for).
|
|
384
|
-
UIImage* image = self.
|
|
591
|
+
UIImage* image = self.originalImage ?: super.image;
|
|
385
592
|
if (self.hasCompleted && CGSizeEqualToSize(image.size, [FFFDownsampledImage sourceSizeOfImage: image])) {
|
|
386
593
|
return;
|
|
387
594
|
}
|
|
@@ -108,6 +108,7 @@ RCT_EXPORT_VIEW_PROPERTY(loopCount, NSInteger)
|
|
|
108
108
|
RCT_EXPORT_VIEW_PROPERTY(imageRendering, NSString)
|
|
109
109
|
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
|
|
110
110
|
RCT_EXPORT_VIEW_PROPERTY(downsample, BOOL)
|
|
111
|
+
RCT_EXPORT_VIEW_PROPERTY(blurRadius, CGFloat)
|
|
111
112
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
|
|
112
113
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageProgress, RCTDirectEventBlock)
|
|
113
114
|
RCT_EXPORT_VIEW_PROPERTY(onFastImageError, RCTDirectEventBlock)
|