@amazon-devices/expo-image 2.0.9000000000 → 3.0.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.
Files changed (92) hide show
  1. package/CMakeLists.txt +80 -58
  2. package/README.kepler.md +1 -0
  3. package/build/ExpoImageModules.kepler.d.ts.map +1 -1
  4. package/build/ExpoImageModules.kepler.js +1 -1
  5. package/build/ExpoImageModules.kepler.js.map +1 -1
  6. package/build/ImageViewNativeComponent.js +1 -1
  7. package/build/ImageViewNativeComponent.js.map +1 -1
  8. package/kepler/AutoLinkInit.cpp +17 -0
  9. package/kepler/Constants.h +21 -0
  10. package/kepler/ExpoImage/ExpoImageTurboModule.cpp +148 -0
  11. package/kepler/ExpoImage/ExpoImageTurboModule.h +41 -0
  12. package/kepler/ExpoImage/PrefetchListener.cpp +45 -0
  13. package/kepler/ExpoImage/PrefetchListener.h +40 -0
  14. package/kepler/ExpoImageComponent/AccessibilityDelegate.cpp +33 -0
  15. package/kepler/ExpoImageComponent/AccessibilityDelegate.h +30 -0
  16. package/kepler/ExpoImageComponent/ExpoImageComponentViewManager.cpp +132 -0
  17. package/kepler/ExpoImageComponent/ExpoImageComponentViewManager.h +66 -0
  18. package/kepler/ExpoImageComponent/ExpoImageStore.h +102 -0
  19. package/kepler/ExpoImageComponent/ExpoImageView.cpp +556 -0
  20. package/kepler/ExpoImageComponent/ExpoImageView.h +186 -0
  21. package/kepler/ExpoImageComponent/HashImageDecoder.cpp +91 -0
  22. package/kepler/ExpoImageComponent/HashImageDecoder.h +18 -0
  23. package/kepler/ExpoImageComponent/ImageLoadListeners.cpp +73 -0
  24. package/kepler/ExpoImageComponent/ImageLoadListeners.h +52 -0
  25. package/kepler/ExpoImageComponent/TransitionAnimator.cpp +113 -0
  26. package/kepler/ExpoImageComponent/TransitionAnimator.h +32 -0
  27. package/kepler/ImageSourceInfo.h +21 -0
  28. package/kepler/ViewManagerFactory.cpp +36 -0
  29. package/kepler/ViewManagerFactory.h +29 -0
  30. package/kepler/blurhash/common.h +11 -10
  31. package/kepler/blurhash/decode.cpp +26 -16
  32. package/kepler/contentposition/ImageContentPosition.cpp +50 -69
  33. package/kepler/contentposition/ImageContentPosition.h +36 -26
  34. package/kepler/enums/ImageContentFit.h +47 -15
  35. package/kepler/enums/ImagePriority.h +12 -13
  36. package/kepler/test/blurhash/decode.cpp +36 -27
  37. package/kepler/test/contentposition/ImageContentPosition.cpp +70 -76
  38. package/kepler/test/thumbhash/decode.cpp +18 -10
  39. package/kepler/test/utils/Base64.cpp +73 -0
  40. package/kepler/test/utils/ColorUtils.cpp +53 -0
  41. package/kepler/test/utils/SourceSelection.cpp +87 -0
  42. package/kepler/thumbhash/decode.cpp +7 -16
  43. package/kepler/utils/Base64.cpp +85 -0
  44. package/kepler/utils/Base64.h +36 -0
  45. package/kepler/utils/ColorUtils.h +26 -0
  46. package/kepler/utils/SourceSelection.cpp +80 -0
  47. package/kepler/utils/SourceSelection.h +65 -0
  48. package/lib/aarch64-release/com.amazon.kepler.uitoolkit.react.expoimageapp.so +0 -0
  49. package/lib/aarch64-release/libexpo-image-3.so +0 -0
  50. package/lib/armv7-release/com.amazon.kepler.uitoolkit.react.expoimageapp.so +0 -0
  51. package/lib/armv7-release/libexpo-image-3.so +0 -0
  52. package/lib/x86_64-release/com.amazon.kepler.uitoolkit.react.expoimageapp.so +0 -0
  53. package/lib/x86_64-release/libexpo-image-3.so +0 -0
  54. package/package.json +1 -1
  55. package/react-native.config.js +12 -4
  56. package/src/ExpoImageModules.kepler.ts +1 -1
  57. package/src/ImageViewNativeComponent.ts +1 -1
  58. package/kepler/ExpoImage.cpp +0 -63
  59. package/kepler/ExpoImage.h +0 -29
  60. package/kepler/ExpoImageCacheHandler.cpp +0 -15
  61. package/kepler/ExpoImageCacheHandler.h +0 -28
  62. package/kepler/ExpoImageComponent.cpp +0 -579
  63. package/kepler/ExpoImageComponent.h +0 -199
  64. package/kepler/ImageWrapper/BlurhashImageWrapper.cpp +0 -37
  65. package/kepler/ImageWrapper/BlurhashImageWrapper.h +0 -23
  66. package/kepler/ImageWrapper/CacheImageWrapper.cpp +0 -80
  67. package/kepler/ImageWrapper/CacheImageWrapper.h +0 -41
  68. package/kepler/ImageWrapper/HashImageWrapper.cpp +0 -21
  69. package/kepler/ImageWrapper/HashImageWrapper.h +0 -22
  70. package/kepler/ImageWrapper/ImageWrapper.cpp +0 -24
  71. package/kepler/ImageWrapper/ImageWrapper.h +0 -33
  72. package/kepler/ImageWrapper/LocalImageWrapper.cpp +0 -61
  73. package/kepler/ImageWrapper/LocalImageWrapper.h +0 -30
  74. package/kepler/ImageWrapper/ThumbhashImageWrapper.cpp +0 -33
  75. package/kepler/ImageWrapper/ThumbhashImageWrapper.h +0 -20
  76. package/kepler/KeplerImageViewComponent.h +0 -64
  77. package/kepler/KeplerInit.cpp +0 -35
  78. package/kepler/managers/ExpoImageManager.cpp +0 -38
  79. package/kepler/managers/ExpoImageManager.h +0 -23
  80. package/kepler/test/ExpoImage.cpp +0 -73
  81. package/kepler/test/ExpoImageComponent.cpp +0 -304
  82. package/kepler/test/managers/ExpoImageManager.cpp +0 -39
  83. package/kepler/test/utils/LoaderDelegate.cpp +0 -130
  84. package/kepler/utils/PlaceholderImageLoaderDelegate.cpp +0 -26
  85. package/kepler/utils/PlaceholderImageLoaderDelegate.h +0 -26
  86. package/kepler/utils/PrefetchImageLoaderDelegate.cpp +0 -55
  87. package/kepler/utils/PrefetchImageLoaderDelegate.h +0 -35
  88. package/kepler/utils/SourceImageLoaderDelegate.cpp +0 -47
  89. package/kepler/utils/SourceImageLoaderDelegate.h +0 -42
  90. package/kepler/utils/SourceImageLoaderDelegateEmitter.cpp +0 -15
  91. package/kepler/utils/SourceImageLoaderDelegateEmitter.h +0 -28
  92. package/kepler-compatibility.json +0 -6
@@ -0,0 +1,556 @@
1
+ #include "ExpoImageView.h"
2
+ #include "HashImageDecoder.h"
3
+ #include "ImageLoadListeners.h"
4
+ #include "TransitionAnimator.h"
5
+ #include "../Constants.h"
6
+ #include "../utils/ColorUtils.h"
7
+ #include "../utils/SourceSelection.h"
8
+
9
+ #include <apmf/iface/com/amazon/kepler/uitoolkit/graphics/Color.h>
10
+ #include <apmf/iface/com/amazon/kepler/uitoolkit/graphics/Point.h>
11
+ #include <apmf/iface/com/amazon/kepler/uitoolkit/react/IFrame.h>
12
+ #include <apmf/iface/com/amazon/kepler/uitoolkit/react/IJsObjectBuilder.h>
13
+ #include <apmf/iface/com/amazon/kepler/uitoolkit/view/IImageOptionsBuilder.h>
14
+ #include <apmf/log.h>
15
+ #include <apmf/iface/com/amazon/kepler/uitoolkit/view/AspectRatioMode.h>
16
+ #include <apmf/iface/com/amazon/kepler/uitoolkit/view/IImageViewStyle.h>
17
+
18
+ #include <algorithm>
19
+ #include <cmath>
20
+ #include <limits>
21
+
22
+ namespace expoimage {
23
+
24
+ namespace gfx = apmf::iface::com::amazon::kepler::uitoolkit::graphics;
25
+
26
+ using apmf::iface::com::amazon::kepler::uitoolkit::view::AspectRatioMode;
27
+ using apmf::iface::com::amazon::kepler::uitoolkit::view::Fit;
28
+ using apmf::iface::com::amazon::kepler::uitoolkit::view::IImageViewStyle;
29
+
30
+ APMF_DEFINE_LOGGER(ExpoImageViewLogger, "com.amazon.kepler.uitoolkit.react.expoimage.view");
31
+ using namespace apmf::log_levels;
32
+
33
+ namespace {
34
+
35
+ void parseSourceArray(View<IJsArray> arr, std::vector<ImageSourceInfo>& out) {
36
+ out.clear();
37
+ if (!arr) return;
38
+ auto count = arr->getElementCount();
39
+ out.reserve(count);
40
+ for (int32_t i = 0; i < count; ++i) {
41
+ auto elem = arr->getObject(i);
42
+ if (elem) out.push_back(parseImageSource(elem));
43
+ }
44
+ }
45
+
46
+ } // anonymous namespace
47
+
48
+ ExpoImageView::ExpoImageView(
49
+ Ptr<IViewModule> viewModule,
50
+ Ptr<IView> container,
51
+ Ptr<IView> clipView,
52
+ Ptr<IImageView3> imageView,
53
+ std::shared_ptr<ExpoImageStore> store)
54
+ : viewModule_(viewModule),
55
+ view_(container),
56
+ clipView_(clipView),
57
+ imageView_(imageView),
58
+ store_(std::move(store)) {
59
+ // use the SHARED loader (owned by the singleton view manager)
60
+ // so an in-flight load survives this cell being torn down during navigation and
61
+ // can complete + populate the cache. Fall back to a per-cell loader only if the
62
+ // store is somehow absent.
63
+ if (store_ && store_->loader) {
64
+ imageLoader_ = store_->loader;
65
+ } else {
66
+ imageLoader_ = viewModule_->makeImageLoader();
67
+ }
68
+ accessibilityDelegate_ = Ptr<ExpoImageAccessibilityDelegate>::Make();
69
+ accessibilityService_ = viewModule_->makeAccessibilityService();
70
+ if (accessibilityService_) accessibilityService_->setDelegate(view_, accessibilityDelegate_);
71
+ }
72
+
73
+ void ExpoImageView::updateProps(View<IJsObject> newProps, View<IContext> context) {
74
+ context_ = context;
75
+ auto keys = newProps->getAllKeys();
76
+ bool sourceChanged = false;
77
+ bool placeholderChanged = false;
78
+ for (const auto& key : keys) {
79
+ if (key == "contentFit") setContentFit(newProps->getString("contentFit"));
80
+ else if (key == "placeholderContentFit") setPlaceholderContentFit(newProps->getString("placeholderContentFit"));
81
+ else if (key == "allowDownscaling") setAllowDownscaling(newProps->getBool("allowDownscaling"));
82
+ else if (key == "contentPosition") setContentPosition(newProps->getObject("contentPosition"));
83
+ else if (key == "tintColor") setTintColor(newProps->getInt("tintColor"));
84
+ else if (key == "overlayColor") setOverlayColor(newProps->getInt("overlayColor"));
85
+ else if (key == "blurRadius") setBlurRadius(newProps->getDouble("blurRadius"));
86
+ else if (key == "placeholder") {
87
+ auto arr = newProps->getArray("placeholder");
88
+ if (arr) { setPlaceholder(arr); placeholderChanged = true; }
89
+ }
90
+ else if (key == "source") {
91
+ auto arr = newProps->getArray("source");
92
+ if (arr) { setSource(arr); sourceChanged = true; }
93
+ }
94
+ else if (key == "priority") setPriority(newProps->getString("priority"));
95
+ else if (key == "transition") setTransition(newProps->getObject("transition"));
96
+ else if (key == "recyclingKey") setRecyclingKey(newProps->getString("recyclingKey"));
97
+ else if (key == "accessibilityLabel") setAccessibilityLabel(newProps->getString("accessibilityLabel"));
98
+ else if (key == "alt") setAlt(newProps->getString("alt"));
99
+ else if (key == "accessible") setAccessible(newProps->getBool("accessible"));
100
+ }
101
+ if (sourceChanged) loadSource(true);
102
+ if (placeholderChanged) loadPlaceholder();
103
+ }
104
+
105
+ void ExpoImageView::updateLayout(View<ILayout> newLayout) {
106
+ auto size = newLayout->getContentFrame()->getSize();
107
+ view_->setSize(size);
108
+ if (clipView_) clipView_->setSize(size);
109
+ bool sizeChanged = (viewSize_.width != size.width || viewSize_.height != size.height);
110
+ viewSize_ = size;
111
+ displayState_->viewSize = size;
112
+ if (sizeChanged) {
113
+ loadSource(false);
114
+ loadPlaceholder();
115
+ applyContentPositionOffset();
116
+ }
117
+ }
118
+
119
+ // --- Prop handlers ---
120
+
121
+ void ExpoImageView::setContentFit(const std::string& v) {
122
+ contentFit_ = parseContentFit(v);
123
+ applyContentPositionOffset(contentFit_, allowDownscaling_);
124
+ }
125
+
126
+ void ExpoImageView::setPlaceholderContentFit(const std::string& v) {
127
+ placeholderContentFit_ = parseContentFit(v, ImageContentFit::ScaleDown);
128
+ }
129
+
130
+ void ExpoImageView::setAllowDownscaling(bool v) {
131
+ if (allowDownscaling_ == v) return;
132
+ allowDownscaling_ = v;
133
+ applyContentPositionOffset(contentFit_, allowDownscaling_);
134
+ }
135
+
136
+ void ExpoImageView::setTintColor(int v) {
137
+ tintColor_ = v;
138
+ if (!imageView_) return;
139
+ auto style = imageView_.TryQueryInterface<IImageViewStyle>();
140
+ if (v == std::numeric_limits<int>::max()) {
141
+ style->setTintColor(gfx::Color{0.0f, 0.0f, 0.0f, 0.0f});
142
+ } else {
143
+ style->setTintColor(intToColor(v));
144
+ }
145
+ }
146
+
147
+ void ExpoImageView::setOverlayColor(int v) {
148
+ overlayColor_ = v;
149
+ if (!imageView_) return;
150
+ auto style = imageView_.TryQueryInterface<IImageViewStyle>();
151
+ if (v == std::numeric_limits<int>::max()) {
152
+ style->setOverlayColor(gfx::Color{0.0f, 0.0f, 0.0f, 0.0f});
153
+ } else {
154
+ style->setOverlayColor(intToColor(v));
155
+ }
156
+ }
157
+
158
+ void ExpoImageView::setBlurRadius(double v) {
159
+ blurRadius_ = v;
160
+ if (imageView_) {
161
+ auto style = imageView_.TryQueryInterface<IImageViewStyle>();
162
+ if (style) style->setBlurRadius(static_cast<float>(v));
163
+ }
164
+ }
165
+
166
+ void ExpoImageView::setPriority(const std::string& v) { priority_ = parsePriority(v); }
167
+
168
+ void ExpoImageView::setRecyclingKey(const std::string& v) {
169
+ shouldClearOnSourceChange_ = !recyclingKey_.empty() && recyclingKey_ != v;
170
+ recyclingKey_ = v;
171
+ }
172
+
173
+ void ExpoImageView::setAccessibilityLabel(const std::string& v) {
174
+ accessibilityLabel_ = v;
175
+ accessibilityDelegate_->setDescription(alt_.empty() ? accessibilityLabel_ : alt_);
176
+ if (accessibilityService_) accessibilityService_->invalidate(view_);
177
+ }
178
+
179
+ void ExpoImageView::setAlt(const std::string& v) {
180
+ alt_ = v;
181
+ accessibilityDelegate_->setDescription(alt_.empty() ? accessibilityLabel_ : alt_);
182
+ if (accessibilityService_) accessibilityService_->invalidate(view_);
183
+ }
184
+
185
+ void ExpoImageView::setAccessible(bool v) {
186
+ accessibilityDelegate_->setAccessible(v);
187
+ if (accessibilityService_) accessibilityService_->invalidate(view_);
188
+ }
189
+
190
+ void ExpoImageView::setContentPosition(View<IJsObject> posObj) {
191
+ if (!posObj) return;
192
+ contentPosition_ = ImageContentPosition(
193
+ parsePositionValue(posObj, "top"), parsePositionValue(posObj, "bottom"),
194
+ parsePositionValue(posObj, "right"), parsePositionValue(posObj, "left"));
195
+ displayState_->contentPosition = contentPosition_;
196
+ applyContentPositionOffset();
197
+ }
198
+
199
+ void ExpoImageView::applyContentPositionOffset() {
200
+ applyContentPositionOffset(displayState_->activeFit, displayState_->activeAllowDownscaling);
201
+ }
202
+
203
+ void ExpoImageView::applyContentPositionOffset(ImageContentFit fit, bool allowDownscaling) {
204
+ displayState_->activeFit = fit;
205
+ displayState_->activeAllowDownscaling = allowDownscaling;
206
+
207
+ if (!imageView_) return;
208
+ layoutImageView(imageView_, fit, allowDownscaling, displayState_->loadedImageSize, viewSize_, contentPosition_);
209
+ }
210
+
211
+ void ExpoImageView::layoutImageView(
212
+ Ptr<IImageView3> imageView,
213
+ ImageContentFit fit,
214
+ bool allowDownscaling,
215
+ gfx::Size imageSize,
216
+ gfx::Size viewSize,
217
+ const ImageContentPosition& contentPosition) {
218
+ if (!imageView) return;
219
+
220
+ const bool imageSizeKnown = (imageSize.width > 0 && imageSize.height > 0);
221
+ const bool imageFitsInView = imageSizeKnown &&
222
+ (imageSize.width <= viewSize.width && imageSize.height <= viewSize.height);
223
+
224
+ const bool rendersAtNaturalSize =
225
+ !allowDownscaling
226
+ || fit == ImageContentFit::None
227
+ || (fit == ImageContentFit::ScaleDown && imageFitsInView);
228
+
229
+ imageView->setFit(resolveNativeFit(fit, allowDownscaling, imageSize, viewSize));
230
+
231
+ if (!rendersAtNaturalSize || !imageSizeKnown) {
232
+ imageView->setOffset(gfx::Point{0.0f, 0.0f});
233
+ imageView->setSize(viewSize);
234
+ return;
235
+ }
236
+
237
+ imageView->setSize(imageSize);
238
+ float offsetX = contentPosition.offsetX(imageSize, viewSize);
239
+ float offsetY = contentPosition.offsetY(imageSize, viewSize);
240
+ imageView->setOffset(gfx::Point{offsetX, offsetY});
241
+ }
242
+
243
+ void ExpoImageView::setTransition(View<IJsObject> transObj) {
244
+ if (!transObj) return;
245
+ auto keys = transObj->getAllKeys();
246
+ if (hasKey(keys, "duration")) transitionDuration_ = static_cast<float>(transObj->getDouble("duration")) / 1000.0f;
247
+ if (hasKey(keys, "timing")) transitionTiming_ = transObj->getString("timing");
248
+ shouldAnimate_ = transitionDuration_ > 0.0f;
249
+ }
250
+
251
+ void ExpoImageView::setSource(View<IJsArray> sourceArr) {
252
+ parseSourceArray(sourceArr, sources_);
253
+ displayState_->sourceLoaded = false;
254
+ displayState_->loadedUri.clear();
255
+ displayState_->pendingUri.clear();
256
+ displayState_->loadedImageSize = gfx::Size{0, 0};
257
+ }
258
+
259
+ void ExpoImageView::setPlaceholder(View<IJsArray> phArr) {
260
+ parseSourceArray(phArr, placeholders_);
261
+ }
262
+
263
+ // --- Image loading ---
264
+
265
+ void ExpoImageView::loadSource(bool animate) {
266
+ auto best = getBestSource(sources_, viewSize_);
267
+ if (!best || best->uri.empty()) return;
268
+ if (!imageView_) return;
269
+
270
+ if (best->uri == displayState_->loadedUri || best->uri == displayState_->pendingUri) return;
271
+ if (shouldDeferRemoteLoad(*best)) return;
272
+
273
+ if (best->width > 0 && best->height > 0) {
274
+ displayState_->loadedImageSize = gfx::Size{best->width, best->height};
275
+ applyContentPositionOffset(contentFit_, allowDownscaling_);
276
+ }
277
+
278
+ APMF_LOG(ExpoImageViewLogger, DEBUG) << "ExpoImageView::loadSource: uri=" << best->uri;
279
+
280
+ // Do NOT blank the view before the (asynchronous) load of
281
+ // the new source completes. clearImage() here empties the imageView for the
282
+ // entire decode/network window, which on a fast-navigated recycling row
283
+ // reads as a flicker. Keep the previously displayed frame and let the new
284
+ // image replace it atomically in applySourceImage() (onSuccess -> setImage).
285
+ // This mirrors the pre-migration behaviour, which held the old bitmap and
286
+ // cross-faded instead of clearing to empty.
287
+ shouldClearOnSourceChange_ = false;
288
+
289
+ if (isHashUri(best->uri)) {
290
+ loadHashSource(*best, animate);
291
+ } else {
292
+ loadRemoteSource(*best, animate);
293
+ }
294
+ }
295
+
296
+ bool ExpoImageView::shouldDeferRemoteLoad(const ImageSourceInfo& best) const {
297
+ bool needsViewSize = (best.width <= 0.0f && best.height <= 0.0f);
298
+ bool hasViewSize = (viewSize_.width > 0.0f && viewSize_.height > 0.0f);
299
+ return needsViewSize && !hasViewSize && !isHashUri(best.uri);
300
+ }
301
+
302
+ std::optional<gfx::Size> ExpoImageView::computePreferredSize(const ImageSourceInfo& best) const {
303
+ if (best.width > 0.0f && best.height > 0.0f) {
304
+ return gfx::Size{best.width, best.height};
305
+ }
306
+
307
+ // remote sources frequently arrive from JS
308
+ // with 0x0 dimensions. Fall back to the view size so the loader decodes at
309
+ // display resolution instead of full native resolution -- this keeps decodes
310
+ // fast (so they finish before the cell is recycled) and keeps the decoded-image
311
+ // cache small. Only when the view size is also unknown do we defer (caller's
312
+ // shouldDeferRemoteLoad waits for updateLayout).
313
+ if (viewSize_.width > 0.0f && viewSize_.height > 0.0f) {
314
+ return gfx::Size{viewSize_.width, viewSize_.height};
315
+ }
316
+
317
+ return std::nullopt;
318
+ }
319
+
320
+ void ExpoImageView::loadHashSource(const ImageSourceInfo& best, bool animate) {
321
+ auto emitter = context_ ? context_->getEventEmitter() : nullptr;
322
+ if (emitter) emitter->emit(view_, "loadStart");
323
+ bool decoded = loadHashImage(best.uri, best.width, best.height, animate);
324
+ applyContentPositionOffset(contentFit_, allowDownscaling_);
325
+ if (decoded) {
326
+ displayState_->sourceLoaded = true;
327
+ displayState_->loadedUri = best.uri;
328
+ if (emitter) {
329
+ emitter->emit(view_, "load");
330
+ emitter->emit(view_, "loadEnd");
331
+ }
332
+ } else {
333
+ if (emitter) {
334
+ emitter->emit(view_, "error");
335
+ emitter->emit(view_, "loadEnd");
336
+ }
337
+ }
338
+ }
339
+
340
+ void ExpoImageView::loadRemoteSource(const ImageSourceInfo& best, bool animate) {
341
+ if (!context_) return;
342
+
343
+ auto ob = viewModule_->makeImageOptionsBuilder();
344
+ ob->priority(static_cast<uint32_t>(priority_));
345
+
346
+ // Compute the decode-size hint once; it also feeds the cache key so two cells
347
+ // sharing a URI at different sizes don't serve each other's decode.
348
+ std::optional<gfx::Size> preferred;
349
+ if (allowDownscaling_ && contentFit_ != ImageContentFit::None) {
350
+ preferred = computePreferredSize(best);
351
+ if (preferred) {
352
+ ob->preferredSize(*preferred);
353
+ ob->aspectRatioMode(AspectRatioMode::KEEP_ASPECT_RATIO);
354
+ }
355
+ }
356
+ const std::string cacheKey = best.uri + "|" +
357
+ (preferred ? (std::to_string(static_cast<int>(preferred->width)) + "x" +
358
+ std::to_string(static_cast<int>(preferred->height)))
359
+ : std::string("natural"));
360
+
361
+ SourceLoadCaptures captures{
362
+ view_,
363
+ imageView_,
364
+ viewModule_,
365
+ displayState_,
366
+ contentFit_,
367
+ allowDownscaling_,
368
+ animate && shouldAnimate_,
369
+ transitionDuration_,
370
+ transitionTiming_,
371
+ store_,
372
+ best.uri,
373
+ cacheKey};
374
+
375
+ // synchronous cache hit -> paint THIS frame, no async gap.
376
+ // During fast navigation a recycling row re-creates cells faster than a network
377
+ // load completes; without this the cell never paints (flicker). The same handful
378
+ // of posters cycle repeatedly, so after the first load each re-created cell hits
379
+ // the cache and swaps in instantly -- matching the old volta cached behaviour.
380
+ if (store_) {
381
+ if (auto cached = store_->get(cacheKey)) {
382
+ // Instant cache hit: the image is available this frame, so never
383
+ // fade-in from blank (that would flash on every recycled cell).
384
+ captures.shouldAnimate = false;
385
+
386
+ // the async loader path emits the JS load
387
+ // lifecycle events via SourceImageLoadListener (loadStart on
388
+ // dispatch; load + loadEnd on success). The synchronous cache-hit
389
+ // path below bypasses that listener, so it MUST emit the same event
390
+ // sequence here -- otherwise JS consumers that drive UI state from
391
+ // onLoad / onLoadStart / onLoadEnd (loading spinners, analytics,
392
+ // the Expo demo Rest example counters) stop receiving callbacks for
393
+ // every remount after the first warm load. Mirrors loadHashSource,
394
+ // which already emits these events directly without the listener.
395
+ auto emitter = context_ ? context_->getEventEmitter() : nullptr;
396
+ if (emitter) emitter->emit(view_, "loadStart");
397
+ applySourceImage(captures, cached);
398
+ if (emitter) {
399
+ emitter->emit(view_, "load");
400
+ emitter->emit(view_, "loadEnd");
401
+ }
402
+ return;
403
+ }
404
+ }
405
+
406
+ // Mark this URI in-flight so updateLayout-driven reloads don't re-issue it;
407
+ // cleared on completion (success or failure) in applySourceImage.
408
+ displayState_->pendingUri = best.uri;
409
+
410
+ auto listener = Ptr<SourceImageLoadListener>::Make(
411
+ context_->getEventEmitter(), view_,
412
+ [captures = std::move(captures)](Ptr<vi::IImage> img) {
413
+ applySourceImage(captures, img);
414
+ });
415
+ imageLoader_->load(best.uri, listener, ob->build());
416
+ }
417
+
418
+ void ExpoImageView::applySourceImage(const SourceLoadCaptures& c, Ptr<vi::IImage> img) {
419
+ if (!c.state) return;
420
+
421
+ // clear the in-flight marker for this URI on BOTH success and
422
+ // failure, so a transiently failed load can be retried. (Previously the URI was
423
+ // marked loaded eagerly before the async result, so a failed load was never
424
+ // retried until the JS source prop was re-set.)
425
+ if (c.state->pendingUri == c.uri) c.state->pendingUri.clear();
426
+
427
+ if (!c.imageView) return;
428
+ if (!img) return; // failure: pending cleared above -> retry allowed
429
+
430
+ // cache the decoded image (keyed by uri + decode size) so a
431
+ // re-created cell for the same key paints synchronously next time. Runs even if
432
+ // THIS cell was already torn down, because the shared loader / captures keep the
433
+ // image alive -- that is how the cache gets warmed during navigation churn.
434
+ if (c.store) c.store->put(c.cacheKey, img);
435
+
436
+ c.state->loadedUri = c.uri;
437
+
438
+ // remember whether this cell already showed an image
439
+ // BEFORE this swap. setImage() replaces the previous bitmap atomically, so
440
+ // we never need to blank first.
441
+ const bool hadImage = c.state->hasDisplayedImage;
442
+ c.state->sourceLoaded = true;
443
+ c.state->hasDisplayedImage = true;
444
+
445
+ c.imageView->setImage(img);
446
+
447
+ const auto effectiveFit = c.allowDownscaling ? c.contentFit : ImageContentFit::None;
448
+
449
+ c.state->loadedImageSize = gfx::Size{
450
+ static_cast<float>(img->getWidth()), static_cast<float>(img->getHeight())};
451
+ c.state->activeFit = effectiveFit;
452
+ c.state->activeAllowDownscaling = c.allowDownscaling;
453
+
454
+ layoutImageView(c.imageView, effectiveFit, c.allowDownscaling,
455
+ c.state->loadedImageSize, c.state->viewSize, c.state->contentPosition);
456
+
457
+ // only run the fade-in (which starts at opacity 0 = a blank
458
+ // cell) for the FIRST image this view shows. On a recycled cell that already
459
+ // displays an image, fading from blank produces a visible flash on every
460
+ // navigation step; the setImage() above already swapped it cleanly.
461
+ if (c.shouldAnimate && c.duration > 0.0f && !hadImage) {
462
+ startFadeInTransition(c.viewModule, c.view, c.duration, c.timing);
463
+ }
464
+ }
465
+
466
+ void ExpoImageView::animateTransition() {
467
+ if (transitionDuration_ <= 0.0f) return;
468
+ startFadeInTransition(viewModule_, view_, transitionDuration_, transitionTiming_);
469
+ }
470
+
471
+ bool ExpoImageView::loadHashImage(
472
+ const std::string& uri, float width, float height, bool animate) {
473
+ APMF_LOG(ExpoImageViewLogger, DEBUG) << "ExpoImageView::loadHashImage: uri=" << uri;
474
+
475
+ auto gfxImage = decodeHashToImage(uri, width, height);
476
+ if (!gfxImage) return false;
477
+
478
+ if (imageView_) {
479
+ imageView_->setGraphicsImage(gfxImage);
480
+ }
481
+
482
+ // fade-in (opacity 0 -> 1) only on the first image this
483
+ // view shows; recycled cells swap atomically to avoid a blank-flash flicker.
484
+ if (animate && shouldAnimate_ && !displayState_->hasDisplayedImage) animateTransition();
485
+ displayState_->hasDisplayedImage = true;
486
+ return true;
487
+ }
488
+
489
+ void ExpoImageView::loadPlaceholder() {
490
+ if (displayState_->sourceLoaded) return;
491
+ // do not re-show a placeholder on a view that already
492
+ // displays an image. On a recycled cell, setSource() resets sourceLoaded to
493
+ // false while the previous (real) image is still on screen; without this
494
+ // guard loadPlaceholder() would overwrite that image with the (often hash)
495
+ // placeholder before the new source decodes -- a sharp->blur->sharp flicker
496
+ // on every navigation step -- and re-issue the placeholder load that floods
497
+ // the log with onFailure. The retained image stays until the new source
498
+ // swaps in atomically (applySourceImage).
499
+ if (displayState_->hasDisplayedImage) return;
500
+ auto best = getBestSource(placeholders_, viewSize_);
501
+ if (!best || best->uri.empty()) return;
502
+ if (!imageView_) return;
503
+
504
+ if (isHashUri(best->uri)) {
505
+ loadHashPlaceholder(*best);
506
+ } else if (!shouldDeferRemoteLoad(*best)) {
507
+ loadRemotePlaceholder(*best);
508
+ }
509
+ }
510
+
511
+ void ExpoImageView::loadHashPlaceholder(const ImageSourceInfo& best) {
512
+ displayState_->loadedImageSize = gfx::Size{best.width, best.height};
513
+ loadHashImage(best.uri, best.width, best.height, false);
514
+
515
+ applyContentPositionOffset(placeholderContentFit_, true);
516
+ }
517
+
518
+ void ExpoImageView::loadRemotePlaceholder(const ImageSourceInfo& best) {
519
+ if (best.width > 0 && best.height > 0) {
520
+ displayState_->loadedImageSize = gfx::Size{best.width, best.height};
521
+ }
522
+ applyContentPositionOffset(placeholderContentFit_, true);
523
+
524
+ auto ob = viewModule_->makeImageOptionsBuilder();
525
+
526
+ if (placeholderContentFit_ != ImageContentFit::None) {
527
+ if (auto preferred = computePreferredSize(best)) {
528
+ ob->preferredSize(*preferred);
529
+ ob->aspectRatioMode(AspectRatioMode::KEEP_ASPECT_RATIO);
530
+ }
531
+ }
532
+
533
+ auto capturedImageView = imageView_;
534
+ auto capturedPlaceholderFit = placeholderContentFit_;
535
+ auto capturedState = displayState_;
536
+
537
+ auto listener = Ptr<PlaceholderImageLoadListener>::Make(
538
+ [capturedImageView, capturedPlaceholderFit, capturedState](Ptr<vi::IImage> img) {
539
+ if (!capturedImageView || !capturedState || capturedState->sourceLoaded || !img) return;
540
+ capturedImageView->setImage(img);
541
+
542
+ auto imgSize = gfx::Size{
543
+ static_cast<float>(img->getWidth()),
544
+ static_cast<float>(img->getHeight())};
545
+
546
+ capturedState->loadedImageSize = imgSize;
547
+ capturedState->activeFit = capturedPlaceholderFit;
548
+ capturedState->activeAllowDownscaling = true;
549
+
550
+ layoutImageView(capturedImageView, capturedPlaceholderFit, true,
551
+ imgSize, capturedState->viewSize, capturedState->contentPosition);
552
+ });
553
+ imageLoader_->load(best.uri, listener, ob->build());
554
+ }
555
+
556
+ } // namespace expoimage