@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.
- package/CMakeLists.txt +80 -58
- package/README.kepler.md +1 -0
- package/build/ExpoImageModules.kepler.d.ts.map +1 -1
- package/build/ExpoImageModules.kepler.js +1 -1
- package/build/ExpoImageModules.kepler.js.map +1 -1
- package/build/ImageViewNativeComponent.js +1 -1
- package/build/ImageViewNativeComponent.js.map +1 -1
- package/kepler/AutoLinkInit.cpp +17 -0
- package/kepler/Constants.h +21 -0
- package/kepler/ExpoImage/ExpoImageTurboModule.cpp +148 -0
- package/kepler/ExpoImage/ExpoImageTurboModule.h +41 -0
- package/kepler/ExpoImage/PrefetchListener.cpp +45 -0
- package/kepler/ExpoImage/PrefetchListener.h +40 -0
- package/kepler/ExpoImageComponent/AccessibilityDelegate.cpp +33 -0
- package/kepler/ExpoImageComponent/AccessibilityDelegate.h +30 -0
- package/kepler/ExpoImageComponent/ExpoImageComponentViewManager.cpp +132 -0
- package/kepler/ExpoImageComponent/ExpoImageComponentViewManager.h +66 -0
- package/kepler/ExpoImageComponent/ExpoImageStore.h +102 -0
- package/kepler/ExpoImageComponent/ExpoImageView.cpp +556 -0
- package/kepler/ExpoImageComponent/ExpoImageView.h +186 -0
- package/kepler/ExpoImageComponent/HashImageDecoder.cpp +91 -0
- package/kepler/ExpoImageComponent/HashImageDecoder.h +18 -0
- package/kepler/ExpoImageComponent/ImageLoadListeners.cpp +73 -0
- package/kepler/ExpoImageComponent/ImageLoadListeners.h +52 -0
- package/kepler/ExpoImageComponent/TransitionAnimator.cpp +113 -0
- package/kepler/ExpoImageComponent/TransitionAnimator.h +32 -0
- package/kepler/ImageSourceInfo.h +21 -0
- package/kepler/ViewManagerFactory.cpp +36 -0
- package/kepler/ViewManagerFactory.h +29 -0
- package/kepler/blurhash/common.h +11 -10
- package/kepler/blurhash/decode.cpp +26 -16
- package/kepler/contentposition/ImageContentPosition.cpp +50 -69
- package/kepler/contentposition/ImageContentPosition.h +36 -26
- package/kepler/enums/ImageContentFit.h +47 -15
- package/kepler/enums/ImagePriority.h +12 -13
- package/kepler/test/blurhash/decode.cpp +36 -27
- package/kepler/test/contentposition/ImageContentPosition.cpp +70 -76
- package/kepler/test/thumbhash/decode.cpp +18 -10
- package/kepler/test/utils/Base64.cpp +73 -0
- package/kepler/test/utils/ColorUtils.cpp +53 -0
- package/kepler/test/utils/SourceSelection.cpp +87 -0
- package/kepler/thumbhash/decode.cpp +7 -16
- package/kepler/utils/Base64.cpp +85 -0
- package/kepler/utils/Base64.h +36 -0
- package/kepler/utils/ColorUtils.h +26 -0
- package/kepler/utils/SourceSelection.cpp +80 -0
- package/kepler/utils/SourceSelection.h +65 -0
- package/lib/aarch64-release/com.amazon.kepler.uitoolkit.react.expoimageapp.so +0 -0
- package/lib/aarch64-release/libexpo-image-3.so +0 -0
- package/lib/armv7-release/com.amazon.kepler.uitoolkit.react.expoimageapp.so +0 -0
- package/lib/armv7-release/libexpo-image-3.so +0 -0
- package/lib/x86_64-release/com.amazon.kepler.uitoolkit.react.expoimageapp.so +0 -0
- package/lib/x86_64-release/libexpo-image-3.so +0 -0
- package/package.json +1 -1
- package/react-native.config.js +12 -4
- package/src/ExpoImageModules.kepler.ts +1 -1
- package/src/ImageViewNativeComponent.ts +1 -1
- package/kepler/ExpoImage.cpp +0 -63
- package/kepler/ExpoImage.h +0 -29
- package/kepler/ExpoImageCacheHandler.cpp +0 -15
- package/kepler/ExpoImageCacheHandler.h +0 -28
- package/kepler/ExpoImageComponent.cpp +0 -579
- package/kepler/ExpoImageComponent.h +0 -199
- package/kepler/ImageWrapper/BlurhashImageWrapper.cpp +0 -37
- package/kepler/ImageWrapper/BlurhashImageWrapper.h +0 -23
- package/kepler/ImageWrapper/CacheImageWrapper.cpp +0 -80
- package/kepler/ImageWrapper/CacheImageWrapper.h +0 -41
- package/kepler/ImageWrapper/HashImageWrapper.cpp +0 -21
- package/kepler/ImageWrapper/HashImageWrapper.h +0 -22
- package/kepler/ImageWrapper/ImageWrapper.cpp +0 -24
- package/kepler/ImageWrapper/ImageWrapper.h +0 -33
- package/kepler/ImageWrapper/LocalImageWrapper.cpp +0 -61
- package/kepler/ImageWrapper/LocalImageWrapper.h +0 -30
- package/kepler/ImageWrapper/ThumbhashImageWrapper.cpp +0 -33
- package/kepler/ImageWrapper/ThumbhashImageWrapper.h +0 -20
- package/kepler/KeplerImageViewComponent.h +0 -64
- package/kepler/KeplerInit.cpp +0 -35
- package/kepler/managers/ExpoImageManager.cpp +0 -38
- package/kepler/managers/ExpoImageManager.h +0 -23
- package/kepler/test/ExpoImage.cpp +0 -73
- package/kepler/test/ExpoImageComponent.cpp +0 -304
- package/kepler/test/managers/ExpoImageManager.cpp +0 -39
- package/kepler/test/utils/LoaderDelegate.cpp +0 -130
- package/kepler/utils/PlaceholderImageLoaderDelegate.cpp +0 -26
- package/kepler/utils/PlaceholderImageLoaderDelegate.h +0 -26
- package/kepler/utils/PrefetchImageLoaderDelegate.cpp +0 -55
- package/kepler/utils/PrefetchImageLoaderDelegate.h +0 -35
- package/kepler/utils/SourceImageLoaderDelegate.cpp +0 -47
- package/kepler/utils/SourceImageLoaderDelegate.h +0 -42
- package/kepler/utils/SourceImageLoaderDelegateEmitter.cpp +0 -15
- package/kepler/utils/SourceImageLoaderDelegateEmitter.h +0 -28
- 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
|