expo-image 1.7.0 → 1.8.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/CHANGELOG.md CHANGED
@@ -10,6 +10,24 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 1.8.0 — 2023-11-13
14
+
15
+ ### 🎉 New features
16
+
17
+ - Return a promise in the `prefetch` method. ([#25196](https://github.com/expo/expo/pull/25196) by [@gkasdorf](https://github.com/gkasdorf))
18
+ - [Android] Added `autoplay` prop and `startAnimating()` and `stopAnimating()` functions to reflect changes made to iOS in [#25008](https://github.com/expo/expo/pull/25008). ([#25124](https://github.com/expo/expo/pull/25124) by [@gkasdorf](https://github.com/gkasdorf))
19
+
20
+ ### 🐛 Bug fixes
21
+
22
+ - [Android] Fix `contentFit` not working for `SVG` images. ([#25187](https://github.com/expo/expo/pull/25187) by [@behenate](https://github.com/behenate))
23
+ - [iOS] Start loading the image before the view mounts to fix issues with the PagerView. ([#25343](https://github.com/expo/expo/pull/25343) by [@tsapeta](https://github.com/tsapeta))
24
+ - [Android] Fix `SVG` not scaling correctly in the release mode. ([#25326](https://github.com/expo/expo/pull/25326) by [@lukmccall](https://github.com/lukmccall))
25
+ - [Android] Fix incorrect `intrinsicSize` returned for SVGs. ([#25048](https://github.com/expo/expo/pull/25048) by [@behenate](https://github.com/behenate))
26
+
27
+ ### 💡 Others
28
+
29
+ - [Android] Add tracing. ([#25251](https://github.com/expo/expo/pull/25251) by [@lukmccall](https://github.com/lukmccall))
30
+
13
31
  ## 1.7.0 — 2023-11-01
14
32
 
15
33
  ### 🎉 New features
@@ -67,7 +67,7 @@ android {
67
67
  namespace "expo.modules.image"
68
68
  defaultConfig {
69
69
  versionCode 1
70
- versionName "1.7.0"
70
+ versionName "1.8.0"
71
71
  consumerProguardFiles("proguard-rules.pro")
72
72
  }
73
73
  publishing {
@@ -1,9 +1,15 @@
1
1
  package expo.modules.image
2
2
 
3
+ import android.graphics.drawable.Drawable
3
4
  import android.view.View
4
5
  import androidx.core.view.doOnDetach
5
6
  import com.bumptech.glide.Glide
7
+ import com.bumptech.glide.load.DataSource
8
+ import com.bumptech.glide.load.engine.DiskCacheStrategy
9
+ import com.bumptech.glide.load.engine.GlideException
6
10
  import com.bumptech.glide.load.model.GlideUrl
11
+ import com.bumptech.glide.request.RequestListener
12
+ import com.bumptech.glide.request.target.Target
7
13
  import com.facebook.react.uimanager.PixelUtil
8
14
  import com.facebook.react.uimanager.Spacing
9
15
  import com.facebook.react.uimanager.ViewProps
@@ -14,6 +20,7 @@ import expo.modules.image.records.CachePolicy
14
20
  import expo.modules.image.records.ContentPosition
15
21
  import expo.modules.image.records.ImageTransition
16
22
  import expo.modules.image.records.SourceMap
23
+ import expo.modules.kotlin.Promise
17
24
  import expo.modules.kotlin.functions.Queues
18
25
  import expo.modules.kotlin.modules.Module
19
26
  import expo.modules.kotlin.modules.ModuleDefinition
@@ -23,12 +30,50 @@ class ExpoImageModule : Module() {
23
30
  override fun definition() = ModuleDefinition {
24
31
  Name("ExpoImage")
25
32
 
26
- Function("prefetch") { urls: List<String> ->
27
- val context = appContext.reactContext ?: return@Function
33
+ AsyncFunction("prefetch") { urls: List<String>, cachePolicy: CachePolicy, promise: Promise ->
34
+ val context = appContext.reactContext ?: return@AsyncFunction false
35
+
36
+ var imagesLoaded = 0
37
+ var failed = false
38
+
28
39
  urls.forEach {
29
40
  Glide
30
41
  .with(context)
31
- .download(GlideUrl(it))
42
+ .load(GlideUrl(it)) // Use `load` instead of `download` to store the asset in the memory cache
43
+ .apply {
44
+ if (cachePolicy == CachePolicy.MEMORY) {
45
+ diskCacheStrategy(DiskCacheStrategy.NONE)
46
+ }
47
+ }
48
+ .listener(object : RequestListener<Drawable> {
49
+ override fun onLoadFailed(
50
+ e: GlideException?,
51
+ model: Any?,
52
+ target: Target<Drawable>?,
53
+ isFirstResource: Boolean
54
+ ): Boolean {
55
+ if (!failed) {
56
+ failed = true
57
+ promise.resolve(false)
58
+ }
59
+ return true
60
+ }
61
+
62
+ override fun onResourceReady(
63
+ resource: Drawable?,
64
+ model: Any?,
65
+ target: Target<Drawable>?,
66
+ dataSource: DataSource?,
67
+ isFirstResource: Boolean
68
+ ): Boolean {
69
+ imagesLoaded++
70
+
71
+ if (imagesLoaded == urls.size) {
72
+ promise.resolve(true)
73
+ }
74
+ return true
75
+ }
76
+ })
32
77
  .submit()
33
78
  }
34
79
  }
@@ -33,6 +33,8 @@ import expo.modules.image.records.ImageProgressEvent
33
33
  import expo.modules.image.records.ImageTransition
34
34
  import expo.modules.image.records.SourceMap
35
35
  import expo.modules.kotlin.AppContext
36
+ import expo.modules.kotlin.tracing.beginAsyncTraceBlock
37
+ import expo.modules.kotlin.tracing.trace
36
38
  import expo.modules.kotlin.viewevent.EventDispatcher
37
39
  import expo.modules.kotlin.views.ExpoView
38
40
  import jp.wasabeef.glide.transformations.BlurTransformation
@@ -263,74 +265,83 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
263
265
  // However, in this case, it is safe to use as long as nothing else is added to the queue.
264
266
  // The intention is simply to wait for the Glide code to finish before the content of the underlying views is changed during the same rendering tick.
265
267
  mainHandler.postAtFrontOfQueue {
266
- val transitionDuration = (transition?.duration ?: 0).toLong()
268
+ trace(Trace.tag, "onResourceReady") {
269
+ val transitionDuration = (transition?.duration ?: 0).toLong()
270
+
271
+ // If provided resource is a placeholder, but the target doesn't have a source, we treat it as a normal image.
272
+ if (!isPlaceholder || !target.hasSource) {
273
+ val (newView, previousView) = if (firstView.drawable == null) {
274
+ firstView to secondView
275
+ } else {
276
+ secondView to firstView
277
+ }
278
+
279
+ val clearPreviousView = {
280
+ previousView
281
+ .recycleView()
282
+ ?.apply {
283
+ // When the placeholder is loaded, one target is displayed in both views.
284
+ // So we just have to move the reference to a new view instead of clearing the target.
285
+ if (this != target) {
286
+ clear(requestManager)
287
+ }
288
+ }
289
+ }
267
290
 
268
- // If provided resource is a placeholder, but the target doesn't have a source, we treat it as a normal image.
269
- if (!isPlaceholder || !target.hasSource) {
270
- val (newView, previousView) = if (firstView.drawable == null) {
271
- firstView to secondView
291
+ configureView(newView, target, resource, isPlaceholder)
292
+ if (transitionDuration <= 0) {
293
+ clearPreviousView()
294
+ newView.alpha = 1f
295
+ newView.bringToFront()
296
+ } else {
297
+ newView.bringToFront()
298
+ previousView.alpha = 1f
299
+ newView.alpha = 0f
300
+ previousView.animate().apply {
301
+ duration = transitionDuration
302
+ alpha(0f)
303
+ withEndAction {
304
+ clearPreviousView()
305
+ }
306
+ }
307
+ newView.animate().apply {
308
+ duration = transitionDuration
309
+ alpha(1f)
310
+ }
311
+ }
272
312
  } else {
273
- secondView to firstView
274
- }
313
+ // We don't want to show the placeholder if something is currently displayed.
314
+ // There is one exception - when we're displaying a different placeholder.
315
+ if ((firstView.drawable != null && !firstView.isPlaceholder) || secondView.drawable != null) {
316
+ return@trace
317
+ }
275
318
 
276
- val clearPreviousView = {
277
- previousView
319
+ firstView
278
320
  .recycleView()
279
321
  ?.apply {
280
- // When the placeholder is loaded, one target is displayed in both views.
281
- // So we just have to move the reference to a new view instead of clearing the target.
322
+ // The current target is already bound to the view. We don't want to cancel it in that case.
282
323
  if (this != target) {
283
324
  clear(requestManager)
284
325
  }
285
326
  }
286
- }
287
327
 
288
- configureView(newView, target, resource, isPlaceholder)
289
- if (transitionDuration <= 0) {
290
- clearPreviousView()
291
- newView.alpha = 1f
292
- newView.bringToFront()
293
- } else {
294
- newView.bringToFront()
295
- previousView.alpha = 1f
296
- newView.alpha = 0f
297
- previousView.animate().apply {
298
- duration = transitionDuration
299
- alpha(0f)
300
- withEndAction {
301
- clearPreviousView()
328
+ configureView(firstView, target, resource, isPlaceholder)
329
+ if (transitionDuration > 0) {
330
+ firstView.bringToFront()
331
+ firstView.alpha = 0f
332
+ secondView.isVisible = false
333
+ firstView.animate().apply {
334
+ duration = transitionDuration
335
+ alpha(1f)
302
336
  }
303
337
  }
304
- newView.animate().apply {
305
- duration = transitionDuration
306
- alpha(1f)
307
- }
308
- }
309
- } else {
310
- // We don't want to show the placeholder if something is currently displayed.
311
- // There is one exception - when we're displaying a different placeholder.
312
- if ((firstView.drawable != null && !firstView.isPlaceholder) || secondView.drawable != null) {
313
- return@postAtFrontOfQueue
314
338
  }
315
339
 
316
- firstView
317
- .recycleView()
318
- ?.apply {
319
- // The current target is already bound to the view. We don't want to cancel it in that case.
320
- if (this != target) {
321
- clear(requestManager)
322
- }
323
- }
324
-
325
- configureView(firstView, target, resource, isPlaceholder)
326
- if (transitionDuration > 0) {
327
- firstView.bringToFront()
328
- firstView.alpha = 0f
329
- secondView.isVisible = false
330
- firstView.animate().apply {
331
- duration = transitionDuration
332
- alpha(1f)
333
- }
340
+ // If our image is animated, we want to see if autoplay is disabled. If it is, we should
341
+ // stop the animation as soon as the resource is ready. Placeholders should not follow this
342
+ // value since the intention is almost certainly to display the animation (i.e. a spinner)
343
+ if (resource is Animatable && !isPlaceholder && !autoplay) {
344
+ resource.stop()
334
345
  }
335
346
  }
336
347
  }
@@ -428,7 +439,7 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
428
439
  requestManager.clear(secondTarget)
429
440
  }
430
441
 
431
- internal fun rerenderIfNeeded(shouldRerenderBecauseOfResize: Boolean = false) {
442
+ internal fun rerenderIfNeeded(shouldRerenderBecauseOfResize: Boolean = false) = trace(Trace.tag, "rerenderIfNeeded(shouldRerenderBecauseOfResize=$shouldRerenderBecauseOfResize)") {
432
443
  val bestSource = bestSource
433
444
  val bestPlaceholder = bestPlaceholder
434
445
 
@@ -446,7 +457,7 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
446
457
  loadedSource = null
447
458
  transformationMatrixChanged = false
448
459
  clearViewBeforeChangingSource = false
449
- return
460
+ return@trace
450
461
  }
451
462
 
452
463
  val shouldRerender = sourceToLoad != loadedSource || shouldRerender || (sourceToLoad == null && placeholder != null)
@@ -579,6 +590,9 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView(
579
590
  .encodeQuality(100)
580
591
  .apply(propOptions)
581
592
 
593
+ val cookie = Trace.getNextCookieValue()
594
+ beginAsyncTraceBlock(Trace.tag, Trace.loadNewImageBlock, cookie)
595
+ newTarget.setCookie(cookie)
582
596
  request.into(newTarget)
583
597
  } else {
584
598
  // In the case where the source didn't change, but the transformation matrix has to be
@@ -18,6 +18,7 @@ import com.bumptech.glide.util.Preconditions
18
18
  import com.bumptech.glide.util.Synthetic
19
19
  import expo.modules.core.utilities.ifNull
20
20
  import expo.modules.image.enums.ContentFit
21
+ import expo.modules.kotlin.tracing.endAsyncTraceBlock
21
22
  import java.lang.ref.WeakReference
22
23
  import kotlin.math.max
23
24
 
@@ -51,6 +52,15 @@ class ImageViewWrapperTarget(
51
52
  */
52
53
  var sourceWidth = -1
53
54
 
55
+ private var cookie = -1
56
+
57
+ fun setCookie(newValue: Int) {
58
+ endLoadingNewImageTraceBlock()
59
+ synchronized(this) {
60
+ cookie = newValue
61
+ }
62
+ }
63
+
54
64
  /**
55
65
  * The content fit of the placeholder
56
66
  */
@@ -59,11 +69,21 @@ class ImageViewWrapperTarget(
59
69
  private var request: Request? = null
60
70
  private var sizeDeterminer = SizeDeterminer(imageViewHolder)
61
71
 
72
+ private fun endLoadingNewImageTraceBlock() = synchronized(this) {
73
+ if (cookie < 0) {
74
+ return@synchronized
75
+ }
76
+
77
+ endAsyncTraceBlock(Trace.tag, Trace.loadNewImageBlock, cookie)
78
+ cookie = -1
79
+ }
80
+
62
81
  override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
63
82
  // The image view should always be valid. When the view is deallocated, all targets should be
64
83
  // canceled. Therefore that code shouldn't be called in that case. Instead of crashing, we
65
84
  // decided to ignore that.
66
85
  val imageView = imageViewHolder.get().ifNull {
86
+ endLoadingNewImageTraceBlock()
67
87
  Log.w("ExpoImage", "The `ExpoImageViewWrapper` was deallocated, but the target wasn't canceled in time.")
68
88
  return
69
89
  }
@@ -78,6 +98,10 @@ class ImageViewWrapperTarget(
78
98
  false
79
99
  }
80
100
 
101
+ if (!isPlaceholder) {
102
+ endLoadingNewImageTraceBlock()
103
+ }
104
+
81
105
  imageView.onResourceReady(this, resource, isPlaceholder)
82
106
  }
83
107
 
@@ -90,7 +114,9 @@ class ImageViewWrapperTarget(
90
114
  override fun onLoadStarted(placeholder: Drawable?) = Unit
91
115
 
92
116
  // When loading fails, it's handled by the global listener, therefore that method can be NOOP.
93
- override fun onLoadFailed(errorDrawable: Drawable?) = Unit
117
+ override fun onLoadFailed(errorDrawable: Drawable?) {
118
+ endLoadingNewImageTraceBlock()
119
+ }
94
120
 
95
121
  override fun onLoadCleared(placeholder: Drawable?) = Unit
96
122
 
@@ -0,0 +1,11 @@
1
+ package expo.modules.image
2
+
3
+ object Trace {
4
+ val tag = "ExpoImage"
5
+ val loadNewImageBlock = "load new image"
6
+
7
+ private var lastCookieValue = 0
8
+ fun getNextCookieValue() = synchronized(this) {
9
+ lastCookieValue++
10
+ }
11
+ }
@@ -11,6 +11,7 @@ import expo.modules.image.enums.ImageCacheType
11
11
  import expo.modules.image.records.ImageErrorEvent
12
12
  import expo.modules.image.records.ImageLoadEvent
13
13
  import expo.modules.image.records.ImageSource
14
+ import expo.modules.image.svg.SVGBitmapDrawable
14
15
  import java.lang.ref.WeakReference
15
16
  import java.util.*
16
17
 
@@ -47,13 +48,15 @@ class GlideRequestListener(
47
48
  dataSource: DataSource,
48
49
  isFirstResource: Boolean
49
50
  ): Boolean {
51
+ val intrinsicWidth = (resource as? SVGBitmapDrawable)?.svgIntrinsicWidth ?: resource.intrinsicWidth
52
+ val intrinsicHeight = (resource as? SVGBitmapDrawable)?.svgIntrinsicHeight ?: resource.intrinsicHeight
50
53
  expoImageViewWrapper.get()?.onLoad?.invoke(
51
54
  ImageLoadEvent(
52
55
  cacheType = ImageCacheType.fromNativeValue(dataSource).name.lowercase(Locale.getDefault()),
53
56
  source = ImageSource(
54
57
  url = model.toString(),
55
- width = resource.intrinsicWidth,
56
- height = resource.intrinsicHeight,
58
+ width = intrinsicWidth,
59
+ height = intrinsicHeight,
57
60
  mediaType = null // TODO(@lukmccall): add mediaType
58
61
  )
59
62
  )
@@ -2,6 +2,7 @@ package expo.modules.image.records
2
2
 
3
3
  import android.content.Context
4
4
  import android.net.Uri
5
+ import android.util.TypedValue
5
6
  import com.bumptech.glide.load.model.GlideUrl
6
7
  import com.bumptech.glide.load.model.Headers
7
8
  import com.bumptech.glide.load.model.LazyHeaders
@@ -38,13 +39,25 @@ data class SourceMap(
38
39
 
39
40
  private fun isLocalFileUri() = parsedUri?.scheme?.startsWith("file") ?: false
40
41
 
41
- private fun isSvg(): Boolean {
42
- var lastDotIndex = parsedUri?.toString()?.lastIndexOf('.')
42
+ private fun isSvg(context: Context): Boolean {
43
+ var uri = parsedUri?.toString()
44
+ if (uri?.startsWith("res:/") == true) {
45
+ val id = uri.removePrefix("res:/")
46
+ try {
47
+ val typedValue = TypedValue()
48
+ context.resources.getValue(id, typedValue, true)
49
+ uri = typedValue.string.toString()
50
+ } catch (e: Throwable) {
51
+ return false
52
+ }
53
+ }
54
+
55
+ var lastDotIndex = uri?.lastIndexOf('.')
43
56
  // if the path has no file extension and no . at all (e.g. file://path/to/file) return false
44
57
  if (lastDotIndex == -1 || lastDotIndex == null) {
45
58
  return false
46
59
  }
47
- return parsedUri?.toString()?.substring(lastDotIndex)?.startsWith(".svg") ?: false
60
+ return uri?.substring(lastDotIndex)?.startsWith(".svg") ?: false
48
61
  }
49
62
 
50
63
  fun isBlurhash() = parsedUri?.scheme?.startsWith("blurhash") ?: false
@@ -111,7 +124,7 @@ data class SourceMap(
111
124
 
112
125
  // Override the size for local assets (apart from SVGs). This ensures that
113
126
  // resizeMode "center" displays the image in the correct size.
114
- if (width != 0 && height != 0 && !isSvg()) {
127
+ if (width != 0 && height != 0 && !isSvg(context)) {
115
128
  override((width * scale).toInt(), (height * scale).toInt())
116
129
  }
117
130
 
@@ -1,6 +1,7 @@
1
1
  package expo.modules.image.svg
2
2
 
3
3
  import android.content.Context
4
+ import android.content.res.Resources
4
5
  import android.graphics.Bitmap
5
6
  import android.graphics.Canvas
6
7
  import android.graphics.Picture
@@ -11,8 +12,16 @@ import com.bumptech.glide.load.Options
11
12
  import com.bumptech.glide.load.engine.Resource
12
13
  import com.bumptech.glide.load.resource.SimpleResource
13
14
  import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
15
+ import com.caverock.androidsvg.PreserveAspectRatio
16
+ import com.caverock.androidsvg.RenderOptions
14
17
  import com.caverock.androidsvg.SVG
15
18
 
19
+ /**
20
+ * We have to use the intrinsicWidth/Height from the bitmap to render the image at a high enough resolution, but at the same time we want to return the actual
21
+ * preferred width and height of the SVG to JS. This class allows us to do that.
22
+ */
23
+ class SVGBitmapDrawable(res: Resources?, bitmap: Bitmap?, val svgIntrinsicWidth: Int, val svgIntrinsicHeight: Int) : BitmapDrawable(res, bitmap)
24
+
16
25
  /**
17
26
  * Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]).
18
27
  *
@@ -21,17 +30,30 @@ import com.caverock.androidsvg.SVG
21
30
  */
22
31
  class SVGDrawableTranscoder(val context: Context) : ResourceTranscoder<SVG?, Drawable> {
23
32
  override fun transcode(toTranscode: Resource<SVG?>, options: Options): Resource<Drawable> {
24
- val picture = toTranscode.get().renderToPicture()
25
- val drawable = PictureDrawable(picture)
26
- val width = drawable.intrinsicWidth
27
- val height = drawable.intrinsicHeight
33
+ val svgData = toTranscode.get()
34
+ val svgIntrinsicWidth = svgData.documentViewBox.width()
35
+ val svgIntrinsicHeight = svgData.documentViewBox.height()
36
+ val documentWidth = svgData.documentWidth
37
+ val documentHeight = svgData.documentHeight
38
+ val aspectRatio = svgIntrinsicWidth / svgIntrinsicHeight
28
39
 
29
- val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
40
+ // We have no information on what content fit the user wants, so when choosing render resolution we assume
41
+ // "cover" in order to prevent loss of quality after the bitmap is transformed to appropriate `contentFit` later.
42
+ val shouldUseHeightReference = documentWidth / aspectRatio > documentHeight
43
+ val renderWidth = if (shouldUseHeightReference) documentWidth else documentHeight * aspectRatio
44
+ val renderHeight = if (shouldUseHeightReference) documentWidth / aspectRatio else documentHeight
45
+ val renderOptions = RenderOptions().apply {
46
+ viewPort(0f, 0f, renderWidth, renderHeight)
47
+ preserveAspectRatio(PreserveAspectRatio.FULLSCREEN_START)
48
+ }
49
+
50
+ val picture = svgData.renderToPicture(renderWidth.toInt(), renderHeight.toInt(), renderOptions)
51
+ val drawable = PictureDrawable(picture)
52
+ val bitmap = Bitmap.createBitmap(renderWidth.toInt(), renderHeight.toInt(), Bitmap.Config.ARGB_8888)
30
53
  val canvas = Canvas(bitmap)
31
54
  drawable.setBounds(0, 0, canvas.width, canvas.height)
32
55
  drawable.draw(canvas)
33
56
 
34
- val newDrawable = BitmapDrawable(context.resources, bitmap)
35
- return SimpleResource(newDrawable)
57
+ return SimpleResource(SVGBitmapDrawable(context.resources, bitmap, svgIntrinsicWidth.toInt(), svgIntrinsicHeight.toInt()))
36
58
  }
37
59
  }
@@ -1,6 +1,6 @@
1
1
  import { ImageNativeProps } from './Image.types';
2
2
  export declare const ExpoImageModule: {
3
- prefetch(urls: string | string[]): void;
3
+ prefetch(urls: string | string[], _: any): Promise<boolean>;
4
4
  clearMemoryCache(): Promise<boolean>;
5
5
  clearDiskCache(): Promise<boolean>;
6
6
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoImage.web.d.ts","sourceRoot":"","sources":["../src/ExpoImage.web.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAmC,MAAM,eAAe,CAAC;AAQlF,eAAO,MAAM,eAAe;mBACX,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;wBAQb,QAAQ,OAAO,CAAC;sBAIlB,QAAQ,OAAO,CAAC;CAGzC,CAAC;AA+BF,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAChC,MAAM,EACN,WAAW,EACX,UAAU,EACV,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,MAAM,EACN,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,KAAK,EACL,GAAG,KAAK,EACT,EAAE,gBAAgB,eA+ElB"}
1
+ {"version":3,"file":"ExpoImage.web.d.ts","sourceRoot":"","sources":["../src/ExpoImage.web.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAmC,MAAM,eAAe,CAAC;AAQlF,eAAO,MAAM,eAAe;mBACL,MAAM,GAAG,MAAM,EAAE,WAAM,QAAQ,OAAO,CAAC;wBAqBlC,QAAQ,OAAO,CAAC;sBAIlB,QAAQ,OAAO,CAAC;CAGzC,CAAC;AA+BF,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAChC,MAAM,EACN,WAAW,EACX,UAAU,EACV,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,MAAM,EACN,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,KAAK,EACL,GAAG,KAAK,EACT,EAAE,gBAAgB,eA+ElB"}
@@ -6,11 +6,21 @@ import loadStyle from './web/imageStyles';
6
6
  import useSourceSelection from './web/useSourceSelection';
7
7
  loadStyle();
8
8
  export const ExpoImageModule = {
9
- prefetch(urls) {
9
+ async prefetch(urls, _) {
10
10
  const urlsArray = Array.isArray(urls) ? urls : [urls];
11
- urlsArray.forEach((url) => {
12
- const img = new Image();
13
- img.src = url;
11
+ return new Promise((resolve) => {
12
+ let imagesLoaded = 0;
13
+ urlsArray.forEach((url) => {
14
+ const img = new Image();
15
+ img.src = url;
16
+ img.onload = () => {
17
+ imagesLoaded++;
18
+ if (imagesLoaded === urlsArray.length) {
19
+ resolve(true);
20
+ }
21
+ };
22
+ img.onerror = () => resolve(false);
23
+ });
14
24
  });
15
25
  },
16
26
  async clearMemoryCache() {
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoImage.web.js","sourceRoot":"","sources":["../src/ExpoImage.web.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport { View } from 'react-native-web';\n\nimport { ImageNativeProps, ImageSource, ImageLoadEventData } from './Image.types';\nimport AnimationManager, { AnimationManagerNode } from './web/AnimationManager';\nimport ImageWrapper from './web/ImageWrapper';\nimport loadStyle from './web/imageStyles';\nimport useSourceSelection from './web/useSourceSelection';\n\nloadStyle();\n\nexport const ExpoImageModule = {\n prefetch(urls: string | string[]): void {\n const urlsArray = Array.isArray(urls) ? urls : [urls];\n urlsArray.forEach((url) => {\n const img = new Image();\n img.src = url;\n });\n },\n\n async clearMemoryCache(): Promise<boolean> {\n return false;\n },\n\n async clearDiskCache(): Promise<boolean> {\n return false;\n },\n};\n\nfunction onLoadAdapter(onLoad?: (event: ImageLoadEventData) => void) {\n return (event: React.SyntheticEvent<HTMLImageElement, Event>) => {\n const target = event.target as HTMLImageElement;\n onLoad?.({\n source: {\n url: target.currentSrc,\n width: target.naturalWidth,\n height: target.naturalHeight,\n mediaType: null,\n },\n cacheType: 'none',\n });\n };\n}\n\nfunction onErrorAdapter(onError?: { (event: { error: string }): void }) {\n return ({ source }: { source?: ImageSource | null }) => {\n onError?.({\n error: `Failed to load image from url: ${source?.uri}`,\n });\n };\n}\n\n// Used for some transitions to mimic native animations\nconst setCssVariables = (element: HTMLElement, size: DOMRect) => {\n element?.style.setProperty('--expo-image-width', `${size.width}px`);\n element?.style.setProperty('--expo-image-height', `${size.height}px`);\n};\n\nexport default function ExpoImage({\n source,\n placeholder,\n contentFit,\n contentPosition,\n placeholderContentFit,\n cachePolicy,\n onLoad,\n transition,\n onError,\n responsivePolicy,\n onLoadEnd,\n priority,\n blurRadius,\n recyclingKey,\n style,\n ...props\n}: ImageNativeProps) {\n const imagePlaceholderContentFit = placeholderContentFit || 'scale-down';\n const imageHashStyle = {\n objectFit: placeholderContentFit || contentFit,\n };\n const { containerRef, source: selectedSource } = useSourceSelection(\n source,\n responsivePolicy,\n setCssVariables\n );\n\n const initialNodeAnimationKey =\n (recyclingKey ? `${recyclingKey}-${placeholder?.[0]?.uri}` : placeholder?.[0]?.uri) ?? '';\n\n const initialNode: AnimationManagerNode | null = placeholder?.[0]?.uri\n ? [\n initialNodeAnimationKey,\n ({ onAnimationFinished }) =>\n (className, style) => (\n <ImageWrapper\n {...props}\n source={placeholder?.[0]}\n style={{\n objectFit: imagePlaceholderContentFit,\n ...(blurRadius ? { filter: `blur(${blurRadius}px)` } : {}),\n ...style,\n }}\n className={className}\n events={{\n onTransitionEnd: [onAnimationFinished],\n }}\n contentPosition={{ left: '50%', top: '50%' }}\n hashPlaceholderContentPosition={contentPosition}\n hashPlaceholderStyle={imageHashStyle}\n />\n ),\n ]\n : null;\n\n const currentNodeAnimationKey =\n (recyclingKey\n ? `${recyclingKey}-${selectedSource?.uri ?? placeholder?.[0]?.uri}`\n : selectedSource?.uri ?? placeholder?.[0]?.uri) ?? '';\n\n const currentNode: AnimationManagerNode = [\n currentNodeAnimationKey,\n ({ onAnimationFinished, onReady, onMount, onError: onErrorInner }) =>\n (className, style) => (\n <ImageWrapper\n {...props}\n source={selectedSource || placeholder?.[0]}\n events={{\n onError: [onErrorAdapter(onError), onLoadEnd, onErrorInner],\n onLoad: [onLoadAdapter(onLoad), onLoadEnd, onReady],\n onMount: [onMount],\n onTransitionEnd: [onAnimationFinished],\n }}\n style={{\n objectFit: selectedSource ? contentFit : imagePlaceholderContentFit,\n ...(blurRadius ? { filter: `blur(${blurRadius}px)` } : {}),\n ...style,\n }}\n className={className}\n cachePolicy={cachePolicy}\n priority={priority}\n contentPosition={selectedSource ? contentPosition : { top: '50%', left: '50%' }}\n hashPlaceholderContentPosition={contentPosition}\n hashPlaceholderStyle={imageHashStyle}\n accessibilityLabel={props.accessibilityLabel}\n />\n ),\n ];\n return (\n <View ref={containerRef} dataSet={{ expoimage: true }} style={[{ overflow: 'hidden' }, style]}>\n <AnimationManager transition={transition} recyclingKey={recyclingKey} initial={initialNode}>\n {currentNode}\n </AnimationManager>\n </View>\n );\n}\n"]}
1
+ {"version":3,"file":"ExpoImage.web.js","sourceRoot":"","sources":["../src/ExpoImage.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGxC,OAAO,gBAA0C,MAAM,wBAAwB,CAAC;AAChF,OAAO,YAAY,MAAM,oBAAoB,CAAC;AAC9C,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAC1C,OAAO,kBAAkB,MAAM,0BAA0B,CAAC;AAE1D,SAAS,EAAE,CAAC;AAEZ,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,KAAK,CAAC,QAAQ,CAAC,IAAuB,EAAE,CAAC;QACvC,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAEtD,OAAO,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;YACtC,IAAI,YAAY,GAAG,CAAC,CAAC;YAErB,SAAS,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;gBACxB,MAAM,GAAG,GAAG,IAAI,KAAK,EAAE,CAAC;gBACxB,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC;gBACd,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE;oBAChB,YAAY,EAAE,CAAC;oBAEf,IAAI,YAAY,KAAK,SAAS,CAAC,MAAM,EAAE;wBACrC,OAAO,CAAC,IAAI,CAAC,CAAC;qBACf;gBACH,CAAC,CAAC;gBACF,GAAG,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACrC,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,CAAC,cAAc;QAClB,OAAO,KAAK,CAAC;IACf,CAAC;CACF,CAAC;AAEF,SAAS,aAAa,CAAC,MAA4C;IACjE,OAAO,CAAC,KAAoD,EAAE,EAAE;QAC9D,MAAM,MAAM,GAAG,KAAK,CAAC,MAA0B,CAAC;QAChD,MAAM,EAAE,CAAC;YACP,MAAM,EAAE;gBACN,GAAG,EAAE,MAAM,CAAC,UAAU;gBACtB,KAAK,EAAE,MAAM,CAAC,YAAY;gBAC1B,MAAM,EAAE,MAAM,CAAC,aAAa;gBAC5B,SAAS,EAAE,IAAI;aAChB;YACD,SAAS,EAAE,MAAM;SAClB,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,OAA8C;IACpE,OAAO,CAAC,EAAE,MAAM,EAAmC,EAAE,EAAE;QACrD,OAAO,EAAE,CAAC;YACR,KAAK,EAAE,kCAAkC,MAAM,EAAE,GAAG,EAAE;SACvD,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED,uDAAuD;AACvD,MAAM,eAAe,GAAG,CAAC,OAAoB,EAAE,IAAa,EAAE,EAAE;IAC9D,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,oBAAoB,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;IACpE,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,qBAAqB,EAAE,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;AACxE,CAAC,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAChC,MAAM,EACN,WAAW,EACX,UAAU,EACV,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,MAAM,EACN,UAAU,EACV,OAAO,EACP,gBAAgB,EAChB,SAAS,EACT,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,KAAK,EACL,GAAG,KAAK,EACS;IACjB,MAAM,0BAA0B,GAAG,qBAAqB,IAAI,YAAY,CAAC;IACzE,MAAM,cAAc,GAAG;QACrB,SAAS,EAAE,qBAAqB,IAAI,UAAU;KAC/C,CAAC;IACF,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,kBAAkB,CACjE,MAAM,EACN,gBAAgB,EAChB,eAAe,CAChB,CAAC;IAEF,MAAM,uBAAuB,GAC3B,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,YAAY,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IAE5F,MAAM,WAAW,GAAgC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG;QACpE,CAAC,CAAC;YACE,uBAAuB;YACvB,CAAC,EAAE,mBAAmB,EAAE,EAAE,EAAE,CAC1B,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CACpB,CAAC,YAAY,CACX,IAAI,KAAK,CAAC,CACV,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CACzB,KAAK,CAAC,CAAC;oBACL,SAAS,EAAE,0BAA0B;oBACrC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC1D,GAAG,KAAK;iBACT,CAAC,CACF,SAAS,CAAC,CAAC,SAAS,CAAC,CACrB,MAAM,CAAC,CAAC;oBACN,eAAe,EAAE,CAAC,mBAAmB,CAAC;iBACvC,CAAC,CACF,eAAe,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAC7C,8BAA8B,CAAC,CAAC,eAAe,CAAC,CAChD,oBAAoB,CAAC,CAAC,cAAc,CAAC,EACrC,CACH;SACJ;QACH,CAAC,CAAC,IAAI,CAAC;IAET,MAAM,uBAAuB,GAC3B,CAAC,YAAY;QACX,CAAC,CAAC,GAAG,YAAY,IAAI,cAAc,EAAE,GAAG,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE;QACnE,CAAC,CAAC,cAAc,EAAE,GAAG,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IAE1D,MAAM,WAAW,GAAyB;QACxC,uBAAuB;QACvB,CAAC,EAAE,mBAAmB,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,CACnE,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CACpB,CAAC,YAAY,CACX,IAAI,KAAK,CAAC,CACV,MAAM,CAAC,CAAC,cAAc,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAC3C,MAAM,CAAC,CAAC;gBACN,OAAO,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,YAAY,CAAC;gBAC3D,MAAM,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC;gBACnD,OAAO,EAAE,CAAC,OAAO,CAAC;gBAClB,eAAe,EAAE,CAAC,mBAAmB,CAAC;aACvC,CAAC,CACF,KAAK,CAAC,CAAC;gBACL,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,0BAA0B;gBACnE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1D,GAAG,KAAK;aACT,CAAC,CACF,SAAS,CAAC,CAAC,SAAS,CAAC,CACrB,WAAW,CAAC,CAAC,WAAW,CAAC,CACzB,QAAQ,CAAC,CAAC,QAAQ,CAAC,CACnB,eAAe,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAChF,8BAA8B,CAAC,CAAC,eAAe,CAAC,CAChD,oBAAoB,CAAC,CAAC,cAAc,CAAC,CACrC,kBAAkB,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAC7C,CACH;KACJ,CAAC;IACF,OAAO,CACL,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,KAAK,CAAC,CAAC,CAC5F;MAAA,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,CAAC,YAAY,CAAC,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,CACzF;QAAA,CAAC,WAAW,CACd;MAAA,EAAE,gBAAgB,CACpB;IAAA,EAAE,IAAI,CAAC,CACR,CAAC;AACJ,CAAC","sourcesContent":["import React from 'react';\nimport { View } from 'react-native-web';\n\nimport { ImageNativeProps, ImageSource, ImageLoadEventData } from './Image.types';\nimport AnimationManager, { AnimationManagerNode } from './web/AnimationManager';\nimport ImageWrapper from './web/ImageWrapper';\nimport loadStyle from './web/imageStyles';\nimport useSourceSelection from './web/useSourceSelection';\n\nloadStyle();\n\nexport const ExpoImageModule = {\n async prefetch(urls: string | string[], _): Promise<boolean> {\n const urlsArray = Array.isArray(urls) ? urls : [urls];\n\n return new Promise<boolean>((resolve) => {\n let imagesLoaded = 0;\n\n urlsArray.forEach((url) => {\n const img = new Image();\n img.src = url;\n img.onload = () => {\n imagesLoaded++;\n\n if (imagesLoaded === urlsArray.length) {\n resolve(true);\n }\n };\n img.onerror = () => resolve(false);\n });\n });\n },\n\n async clearMemoryCache(): Promise<boolean> {\n return false;\n },\n\n async clearDiskCache(): Promise<boolean> {\n return false;\n },\n};\n\nfunction onLoadAdapter(onLoad?: (event: ImageLoadEventData) => void) {\n return (event: React.SyntheticEvent<HTMLImageElement, Event>) => {\n const target = event.target as HTMLImageElement;\n onLoad?.({\n source: {\n url: target.currentSrc,\n width: target.naturalWidth,\n height: target.naturalHeight,\n mediaType: null,\n },\n cacheType: 'none',\n });\n };\n}\n\nfunction onErrorAdapter(onError?: { (event: { error: string }): void }) {\n return ({ source }: { source?: ImageSource | null }) => {\n onError?.({\n error: `Failed to load image from url: ${source?.uri}`,\n });\n };\n}\n\n// Used for some transitions to mimic native animations\nconst setCssVariables = (element: HTMLElement, size: DOMRect) => {\n element?.style.setProperty('--expo-image-width', `${size.width}px`);\n element?.style.setProperty('--expo-image-height', `${size.height}px`);\n};\n\nexport default function ExpoImage({\n source,\n placeholder,\n contentFit,\n contentPosition,\n placeholderContentFit,\n cachePolicy,\n onLoad,\n transition,\n onError,\n responsivePolicy,\n onLoadEnd,\n priority,\n blurRadius,\n recyclingKey,\n style,\n ...props\n}: ImageNativeProps) {\n const imagePlaceholderContentFit = placeholderContentFit || 'scale-down';\n const imageHashStyle = {\n objectFit: placeholderContentFit || contentFit,\n };\n const { containerRef, source: selectedSource } = useSourceSelection(\n source,\n responsivePolicy,\n setCssVariables\n );\n\n const initialNodeAnimationKey =\n (recyclingKey ? `${recyclingKey}-${placeholder?.[0]?.uri}` : placeholder?.[0]?.uri) ?? '';\n\n const initialNode: AnimationManagerNode | null = placeholder?.[0]?.uri\n ? [\n initialNodeAnimationKey,\n ({ onAnimationFinished }) =>\n (className, style) => (\n <ImageWrapper\n {...props}\n source={placeholder?.[0]}\n style={{\n objectFit: imagePlaceholderContentFit,\n ...(blurRadius ? { filter: `blur(${blurRadius}px)` } : {}),\n ...style,\n }}\n className={className}\n events={{\n onTransitionEnd: [onAnimationFinished],\n }}\n contentPosition={{ left: '50%', top: '50%' }}\n hashPlaceholderContentPosition={contentPosition}\n hashPlaceholderStyle={imageHashStyle}\n />\n ),\n ]\n : null;\n\n const currentNodeAnimationKey =\n (recyclingKey\n ? `${recyclingKey}-${selectedSource?.uri ?? placeholder?.[0]?.uri}`\n : selectedSource?.uri ?? placeholder?.[0]?.uri) ?? '';\n\n const currentNode: AnimationManagerNode = [\n currentNodeAnimationKey,\n ({ onAnimationFinished, onReady, onMount, onError: onErrorInner }) =>\n (className, style) => (\n <ImageWrapper\n {...props}\n source={selectedSource || placeholder?.[0]}\n events={{\n onError: [onErrorAdapter(onError), onLoadEnd, onErrorInner],\n onLoad: [onLoadAdapter(onLoad), onLoadEnd, onReady],\n onMount: [onMount],\n onTransitionEnd: [onAnimationFinished],\n }}\n style={{\n objectFit: selectedSource ? contentFit : imagePlaceholderContentFit,\n ...(blurRadius ? { filter: `blur(${blurRadius}px)` } : {}),\n ...style,\n }}\n className={className}\n cachePolicy={cachePolicy}\n priority={priority}\n contentPosition={selectedSource ? contentPosition : { top: '50%', left: '50%' }}\n hashPlaceholderContentPosition={contentPosition}\n hashPlaceholderStyle={imageHashStyle}\n accessibilityLabel={props.accessibilityLabel}\n />\n ),\n ];\n return (\n <View ref={containerRef} dataSet={{ expoimage: true }} style={[{ overflow: 'hidden' }, style]}>\n <AnimationManager transition={transition} recyclingKey={recyclingKey} initial={initialNode}>\n {currentNode}\n </AnimationManager>\n </View>\n );\n}\n"]}
package/build/Image.d.ts CHANGED
@@ -4,11 +4,17 @@ export declare class Image extends React.PureComponent<ImageProps> {
4
4
  nativeViewRef: any;
5
5
  constructor(props: any);
6
6
  /**
7
- * Preloads images at the given urls that can be later used in the image view.
8
- * Preloaded images are always cached on the disk, so make sure to use
9
- * `disk` (default) or `memory-disk` cache policy.
7
+ * Preloads images at the given URLs that can be later used in the image view.
8
+ * Preloaded images are cached to the memory and disk by default, so make sure
9
+ * to use `disk` (default) or `memory-disk` [cache policy](#cachepolicy).
10
+ * @param urls - A URL string or an array of URLs of images to prefetch.
11
+ * @param cachePolicy - The cache policy for prefetched images.
12
+ * @return A promise resolving to `true` as soon as all images have been
13
+ * successfully prefetched. If an image fails to be prefetched, the promise
14
+ * will immediately resolve to `false` regardless of whether other images have
15
+ * finished prefetching.
10
16
  */
11
- static prefetch(urls: string | string[]): void;
17
+ static prefetch(urls: string | string[], cachePolicy?: 'memory-disk' | 'memory'): Promise<boolean>;
12
18
  /**
13
19
  * Asynchronously clears all images stored in memory.
14
20
  * @platform android
@@ -1 +1 @@
1
- {"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../src/Image.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAM3C,qBAAa,KAAM,SAAQ,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC;IACxD,aAAa,MAAC;gBAEF,KAAK,KAAA;IAKjB;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI;IAI9C;;;;;;;OAOG;WACU,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjD;;;;;;;OAOG;WACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/C;;;;;;;;;OASG;WACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIxE;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAIpC,MAAM;CAqCP"}
1
+ {"version":3,"file":"Image.d.ts","sourceRoot":"","sources":["../src/Image.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAM3C,qBAAa,KAAM,SAAQ,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC;IACxD,aAAa,MAAC;gBAEF,KAAK,KAAA;IAKjB;;;;;;;;;;OAUG;WACU,QAAQ,CACnB,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EACvB,WAAW,GAAE,aAAa,GAAG,QAAwB,GACpD,OAAO,CAAC,OAAO,CAAC;IAInB;;;;;;;OAOG;WACU,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjD;;;;;;;OAOG;WACU,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC;IAI/C;;;;;;;;;OASG;WACU,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIxE;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrC;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAIpC,MAAM;CAqCP"}
package/build/Image.js CHANGED
@@ -11,12 +11,18 @@ export class Image extends React.PureComponent {
11
11
  this.nativeViewRef = React.createRef();
12
12
  }
13
13
  /**
14
- * Preloads images at the given urls that can be later used in the image view.
15
- * Preloaded images are always cached on the disk, so make sure to use
16
- * `disk` (default) or `memory-disk` cache policy.
14
+ * Preloads images at the given URLs that can be later used in the image view.
15
+ * Preloaded images are cached to the memory and disk by default, so make sure
16
+ * to use `disk` (default) or `memory-disk` [cache policy](#cachepolicy).
17
+ * @param urls - A URL string or an array of URLs of images to prefetch.
18
+ * @param cachePolicy - The cache policy for prefetched images.
19
+ * @return A promise resolving to `true` as soon as all images have been
20
+ * successfully prefetched. If an image fails to be prefetched, the promise
21
+ * will immediately resolve to `false` regardless of whether other images have
22
+ * finished prefetching.
17
23
  */
18
- static prefetch(urls) {
19
- return ExpoImageModule.prefetch(Array.isArray(urls) ? urls : [urls]);
24
+ static async prefetch(urls, cachePolicy = 'memory-disk') {
25
+ return ExpoImageModule.prefetch(Array.isArray(urls) ? urls : [urls], cachePolicy);
20
26
  }
21
27
  /**
22
28
  * Asynchronously clears all images stored in memory.
@@ -1 +1 @@
1
- {"version":3,"file":"Image.js","sourceRoot":"","sources":["../src/Image.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,SAAS,EAAE,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,IAAI,qCAAqC,GAAG,KAAK,CAAC;AAElD,MAAM,OAAO,KAAM,SAAQ,KAAK,CAAC,aAAyB;IACxD,aAAa,CAAC;IAEd,YAAY,KAAK;QACf,KAAK,CAAC,KAAK,CAAC,CAAC;QACb,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;IACzC,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,IAAuB;QACrC,OAAO,eAAe,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,gBAAgB;QAC3B,OAAO,MAAM,eAAe,CAAC,gBAAgB,EAAE,CAAC;IAClD,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc;QACzB,OAAO,MAAM,eAAe,CAAC,cAAc,EAAE,CAAC;IAChD,CAAC;IAED;;;;;;;;;OASG;IACH,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,QAAgB;QAC7C,OAAO,MAAM,eAAe,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAC3D,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,cAAc;QAClB,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,aAAa;QACjB,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;IACnD,CAAC;IAED,MAAM;QACJ,MAAM,EACJ,KAAK,EACL,MAAM,EACN,WAAW,EACX,UAAU,EACV,eAAe,EACf,UAAU,EACV,YAAY,EACZ,UAAU,EAAE,cAAc,EAC1B,aAAa,EACb,sBAAsB,EACtB,GAAG,SAAS,EACb,GAAG,IAAI,CAAC,KAAK,CAAC;QAEf,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,SAAS,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACtF,MAAM,UAAU,GAAG,cAAc,IAAI,eAAe,CAAC;QAErD,IAAI,CAAC,aAAa,IAAI,sBAAsB,CAAC,IAAI,CAAC,qCAAqC,EAAE;YACvF,OAAO,CAAC,IAAI,CACV,4GAA4G,CAC7G,CAAC;YACF,qCAAqC,GAAG,IAAI,CAAC;SAC9C;QAED,OAAO,CACL,CAAC,SAAS,CACR,IAAI,SAAS,CAAC,CACd,KAAK,CAAC,CAAC,SAAS,CAAC,CACjB,MAAM,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAC/B,WAAW,CAAC,CAAC,cAAc,CAAC,WAAW,IAAI,aAAa,IAAI,sBAAsB,CAAC,CAAC,CACpF,UAAU,CAAC,CAAC,iBAAiB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CACtD,eAAe,CAAC,CAAC,sBAAsB,CAAC,eAAe,CAAC,CAAC,CACzD,UAAU,CAAC,CAAC,iBAAiB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,EACxD,CACH,CAAC;IACJ,CAAC;CACF","sourcesContent":["import React from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport ExpoImage, { ExpoImageModule } from './ExpoImage';\nimport { ImageProps } from './Image.types';\nimport { resolveContentFit, resolveContentPosition, resolveTransition } from './utils';\nimport { resolveSources } from './utils/resolveSources';\n\nlet loggedDefaultSourceDeprecationWarning = false;\n\nexport class Image extends React.PureComponent<ImageProps> {\n nativeViewRef;\n\n constructor(props) {\n super(props);\n this.nativeViewRef = React.createRef();\n }\n\n /**\n * Preloads images at the given urls that can be later used in the image view.\n * Preloaded images are always cached on the disk, so make sure to use\n * `disk` (default) or `memory-disk` cache policy.\n */\n static prefetch(urls: string | string[]): void {\n return ExpoImageModule.prefetch(Array.isArray(urls) ? urls : [urls]);\n }\n\n /**\n * Asynchronously clears all images stored in memory.\n * @platform android\n * @platform ios\n * @return A promise resolving to `true` when the operation succeeds.\n * It may resolve to `false` on Android when the activity is no longer available.\n * Resolves to `false` on Web.\n */\n static async clearMemoryCache(): Promise<boolean> {\n return await ExpoImageModule.clearMemoryCache();\n }\n\n /**\n * Asynchronously clears all images from the disk cache.\n * @platform android\n * @platform ios\n * @return A promise resolving to `true` when the operation succeeds.\n * It may resolve to `false` on Android when the activity is no longer available.\n * Resolves to `false` on Web.\n */\n static async clearDiskCache(): Promise<boolean> {\n return await ExpoImageModule.clearDiskCache();\n }\n\n /**\n * Asynchronously checks if an image exists in the disk cache and resolves to\n * the path of the cached image if it does.\n * @param cacheKey - The cache key for the requested image. Unless you have set\n * a custom cache key, this will be the source URL of the image.\n * @platform android\n * @platform ios\n * @return A promise resolving to the path of the cached image. It will resolve\n * to `null` if the image does not exist in the cache.\n */\n static async getCachePathAsync(cacheKey: string): Promise<string | null> {\n return await ExpoImageModule.getCachePathAsync(cacheKey);\n }\n\n /**\n * Asynchronously starts playback of the view's image if it is animated.\n * @platform ios\n */\n async startAnimating(): Promise<void> {\n await this.nativeViewRef.current.startAnimating();\n }\n\n /**\n * Asynchronously stops the playback of the view's image if it is animated.\n * @platform ios\n */\n async stopAnimating(): Promise<void> {\n await this.nativeViewRef.current.stopAnimating();\n }\n\n render() {\n const {\n style,\n source,\n placeholder,\n contentFit,\n contentPosition,\n transition,\n fadeDuration,\n resizeMode: resizeModeProp,\n defaultSource,\n loadingIndicatorSource,\n ...restProps\n } = this.props;\n\n const { resizeMode: resizeModeStyle, ...restStyle } = StyleSheet.flatten(style) || {};\n const resizeMode = resizeModeProp ?? resizeModeStyle;\n\n if ((defaultSource || loadingIndicatorSource) && !loggedDefaultSourceDeprecationWarning) {\n console.warn(\n '[expo-image]: `defaultSource` and `loadingIndicatorSource` props are deprecated, use `placeholder` instead'\n );\n loggedDefaultSourceDeprecationWarning = true;\n }\n\n return (\n <ExpoImage\n {...restProps}\n style={restStyle}\n source={resolveSources(source)}\n placeholder={resolveSources(placeholder ?? defaultSource ?? loadingIndicatorSource)}\n contentFit={resolveContentFit(contentFit, resizeMode)}\n contentPosition={resolveContentPosition(contentPosition)}\n transition={resolveTransition(transition, fadeDuration)}\n />\n );\n }\n}\n"]}
1
+ {"version":3,"file":"Image.js","sourceRoot":"","sources":["../src/Image.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport { StyleSheet } from 'react-native';\n\nimport ExpoImage, { ExpoImageModule } from './ExpoImage';\nimport { ImageProps } from './Image.types';\nimport { resolveContentFit, resolveContentPosition, resolveTransition } from './utils';\nimport { resolveSources } from './utils/resolveSources';\n\nlet loggedDefaultSourceDeprecationWarning = false;\n\nexport class Image extends React.PureComponent<ImageProps> {\n nativeViewRef;\n\n constructor(props) {\n super(props);\n this.nativeViewRef = React.createRef();\n }\n\n /**\n * Preloads images at the given URLs that can be later used in the image view.\n * Preloaded images are cached to the memory and disk by default, so make sure\n * to use `disk` (default) or `memory-disk` [cache policy](#cachepolicy).\n * @param urls - A URL string or an array of URLs of images to prefetch.\n * @param cachePolicy - The cache policy for prefetched images.\n * @return A promise resolving to `true` as soon as all images have been\n * successfully prefetched. If an image fails to be prefetched, the promise\n * will immediately resolve to `false` regardless of whether other images have\n * finished prefetching.\n */\n static async prefetch(\n urls: string | string[],\n cachePolicy: 'memory-disk' | 'memory' = 'memory-disk'\n ): Promise<boolean> {\n return ExpoImageModule.prefetch(Array.isArray(urls) ? urls : [urls], cachePolicy);\n }\n\n /**\n * Asynchronously clears all images stored in memory.\n * @platform android\n * @platform ios\n * @return A promise resolving to `true` when the operation succeeds.\n * It may resolve to `false` on Android when the activity is no longer available.\n * Resolves to `false` on Web.\n */\n static async clearMemoryCache(): Promise<boolean> {\n return await ExpoImageModule.clearMemoryCache();\n }\n\n /**\n * Asynchronously clears all images from the disk cache.\n * @platform android\n * @platform ios\n * @return A promise resolving to `true` when the operation succeeds.\n * It may resolve to `false` on Android when the activity is no longer available.\n * Resolves to `false` on Web.\n */\n static async clearDiskCache(): Promise<boolean> {\n return await ExpoImageModule.clearDiskCache();\n }\n\n /**\n * Asynchronously checks if an image exists in the disk cache and resolves to\n * the path of the cached image if it does.\n * @param cacheKey - The cache key for the requested image. Unless you have set\n * a custom cache key, this will be the source URL of the image.\n * @platform android\n * @platform ios\n * @return A promise resolving to the path of the cached image. It will resolve\n * to `null` if the image does not exist in the cache.\n */\n static async getCachePathAsync(cacheKey: string): Promise<string | null> {\n return await ExpoImageModule.getCachePathAsync(cacheKey);\n }\n\n /**\n * Asynchronously starts playback of the view's image if it is animated.\n * @platform ios\n */\n async startAnimating(): Promise<void> {\n await this.nativeViewRef.current.startAnimating();\n }\n\n /**\n * Asynchronously stops the playback of the view's image if it is animated.\n * @platform ios\n */\n async stopAnimating(): Promise<void> {\n await this.nativeViewRef.current.stopAnimating();\n }\n\n render() {\n const {\n style,\n source,\n placeholder,\n contentFit,\n contentPosition,\n transition,\n fadeDuration,\n resizeMode: resizeModeProp,\n defaultSource,\n loadingIndicatorSource,\n ...restProps\n } = this.props;\n\n const { resizeMode: resizeModeStyle, ...restStyle } = StyleSheet.flatten(style) || {};\n const resizeMode = resizeModeProp ?? resizeModeStyle;\n\n if ((defaultSource || loadingIndicatorSource) && !loggedDefaultSourceDeprecationWarning) {\n console.warn(\n '[expo-image]: `defaultSource` and `loadingIndicatorSource` props are deprecated, use `placeholder` instead'\n );\n loggedDefaultSourceDeprecationWarning = true;\n }\n\n return (\n <ExpoImage\n {...restProps}\n style={restStyle}\n source={resolveSources(source)}\n placeholder={resolveSources(placeholder ?? defaultSource ?? loadingIndicatorSource)}\n contentFit={resolveContentFit(contentFit, resizeMode)}\n contentPosition={resolveContentPosition(contentPosition)}\n transition={resolveTransition(transition, fadeDuration)}\n />\n );\n }\n}\n"]}
@@ -108,8 +108,26 @@ public final class ImageModule: Module {
108
108
  }
109
109
  }
110
110
 
111
- Function("prefetch") { (urls: [URL]) in
112
- SDWebImagePrefetcher.shared.prefetchURLs(urls)
111
+ AsyncFunction("prefetch") { (urls: [URL], cachePolicy: ImageCachePolicy, promise: Promise) in
112
+ var context = SDWebImageContext()
113
+ context[.storeCacheType] = cachePolicy.toSdCacheType().rawValue
114
+
115
+ var imagesLoaded = 0
116
+ var failed = false
117
+
118
+ urls.forEach { url in
119
+ SDWebImagePrefetcher.shared.prefetchURLs([url], context: context, progress: nil, completed: { loaded, skipped in
120
+ if skipped > 0 && !failed {
121
+ failed = true
122
+ promise.resolve(false)
123
+ } else {
124
+ imagesLoaded = imagesLoaded + 1
125
+ if imagesLoaded == urls.count {
126
+ promise.resolve(true)
127
+ }
128
+ }
129
+ })
130
+ }
113
131
  }
114
132
 
115
133
  AsyncFunction("clearMemoryCache") { () -> Bool in
@@ -67,8 +67,8 @@ public final class ImageView: ExpoView {
67
67
 
68
68
  public override var bounds: CGRect {
69
69
  didSet {
70
- // Reload the image when the bounds size has changed and the view is mounted.
71
- if oldValue.size != bounds.size && window != nil {
70
+ // Reload the image when the bounds size has changed and is not empty.
71
+ if oldValue.size != bounds.size && bounds.size != .zero {
72
72
  reload()
73
73
  }
74
74
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expo-image",
3
3
  "title": "Expo Image",
4
- "version": "1.7.0",
4
+ "version": "1.8.0",
5
5
  "description": "A cross-platform, performant image component for React Native and Expo with Web support",
6
6
  "main": "build/index.js",
7
7
  "types": "build/index.d.ts",
@@ -33,5 +33,5 @@
33
33
  "peerDependencies": {
34
34
  "expo": "*"
35
35
  },
36
- "gitHead": "8ff6964633de470960eb27390d67fcf93e58638d"
36
+ "gitHead": "edbb34bfc68e4e129be2f65432801bd89ea17fe6"
37
37
  }
@@ -10,11 +10,24 @@ import useSourceSelection from './web/useSourceSelection';
10
10
  loadStyle();
11
11
 
12
12
  export const ExpoImageModule = {
13
- prefetch(urls: string | string[]): void {
13
+ async prefetch(urls: string | string[], _): Promise<boolean> {
14
14
  const urlsArray = Array.isArray(urls) ? urls : [urls];
15
- urlsArray.forEach((url) => {
16
- const img = new Image();
17
- img.src = url;
15
+
16
+ return new Promise<boolean>((resolve) => {
17
+ let imagesLoaded = 0;
18
+
19
+ urlsArray.forEach((url) => {
20
+ const img = new Image();
21
+ img.src = url;
22
+ img.onload = () => {
23
+ imagesLoaded++;
24
+
25
+ if (imagesLoaded === urlsArray.length) {
26
+ resolve(true);
27
+ }
28
+ };
29
+ img.onerror = () => resolve(false);
30
+ });
18
31
  });
19
32
  },
20
33
 
package/src/Image.tsx CHANGED
@@ -17,12 +17,21 @@ export class Image extends React.PureComponent<ImageProps> {
17
17
  }
18
18
 
19
19
  /**
20
- * Preloads images at the given urls that can be later used in the image view.
21
- * Preloaded images are always cached on the disk, so make sure to use
22
- * `disk` (default) or `memory-disk` cache policy.
20
+ * Preloads images at the given URLs that can be later used in the image view.
21
+ * Preloaded images are cached to the memory and disk by default, so make sure
22
+ * to use `disk` (default) or `memory-disk` [cache policy](#cachepolicy).
23
+ * @param urls - A URL string or an array of URLs of images to prefetch.
24
+ * @param cachePolicy - The cache policy for prefetched images.
25
+ * @return A promise resolving to `true` as soon as all images have been
26
+ * successfully prefetched. If an image fails to be prefetched, the promise
27
+ * will immediately resolve to `false` regardless of whether other images have
28
+ * finished prefetching.
23
29
  */
24
- static prefetch(urls: string | string[]): void {
25
- return ExpoImageModule.prefetch(Array.isArray(urls) ? urls : [urls]);
30
+ static async prefetch(
31
+ urls: string | string[],
32
+ cachePolicy: 'memory-disk' | 'memory' = 'memory-disk'
33
+ ): Promise<boolean> {
34
+ return ExpoImageModule.prefetch(Array.isArray(urls) ? urls : [urls], cachePolicy);
26
35
  }
27
36
 
28
37
  /**