@repliqo/sdk-react-native 0.3.1 → 0.3.3
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/android/src/main/java/com/repliqo/screencapture/PixelCopyScan.java +279 -0
- package/android/src/main/java/com/repliqo/screencapture/ScreenCaptureModule.java +8 -1
- package/dist/components/RepliqoScrollView.js +11 -6
- package/dist/core/client.js +4 -3
- package/package.json +1 -1
- package/src/components/RepliqoScrollView.tsx +15 -5
- package/src/core/client.ts +4 -3
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
package com.repliqo.screencapture;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
import android.graphics.Rect;
|
|
6
|
+
import android.os.Build;
|
|
7
|
+
import android.os.Handler;
|
|
8
|
+
import android.os.HandlerThread;
|
|
9
|
+
import android.util.Base64;
|
|
10
|
+
import android.util.Log;
|
|
11
|
+
import android.view.PixelCopy;
|
|
12
|
+
import android.view.View;
|
|
13
|
+
import android.view.ViewGroup;
|
|
14
|
+
import android.view.Window;
|
|
15
|
+
import android.widget.ScrollView;
|
|
16
|
+
|
|
17
|
+
import com.facebook.react.bridge.Arguments;
|
|
18
|
+
import com.facebook.react.bridge.WritableArray;
|
|
19
|
+
import com.facebook.react.bridge.WritableMap;
|
|
20
|
+
|
|
21
|
+
import java.io.ByteArrayOutputStream;
|
|
22
|
+
import java.lang.reflect.Field;
|
|
23
|
+
import java.util.ArrayList;
|
|
24
|
+
import java.util.concurrent.CountDownLatch;
|
|
25
|
+
import java.util.concurrent.TimeUnit;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Captures the FULL content of a ScrollView using PixelCopy API +
|
|
29
|
+
* programmatic scrolling.
|
|
30
|
+
*
|
|
31
|
+
* Unlike View.draw() which only renders what the view tree has laid out,
|
|
32
|
+
* PixelCopy captures directly from the GPU's Surface — it sees exactly
|
|
33
|
+
* what's on screen, including content that React Native has rendered
|
|
34
|
+
* but that View.draw() misses.
|
|
35
|
+
*
|
|
36
|
+
* Flow:
|
|
37
|
+
* 1. Find the main ScrollView
|
|
38
|
+
* 2. Save scroll position
|
|
39
|
+
* 3. For each viewport-sized step:
|
|
40
|
+
* a. scrollTo(y) — instant
|
|
41
|
+
* b. Wait 2 frames for RN to render
|
|
42
|
+
* c. PixelCopy the Window surface (captures what's actually visible)
|
|
43
|
+
* d. Crop to the ScrollView's bounds on screen
|
|
44
|
+
* 4. Restore scroll position
|
|
45
|
+
* 5. Return all tiles
|
|
46
|
+
*
|
|
47
|
+
* Requires API 26+ (Android 8.0). Falls back to MultiWindowCapture on older.
|
|
48
|
+
*/
|
|
49
|
+
public class PixelCopyScan {
|
|
50
|
+
private static final String TAG = "RepliqoCapture";
|
|
51
|
+
private static final int TARGET_WIDTH = 390;
|
|
52
|
+
private static final int JPEG_QUALITY = 45;
|
|
53
|
+
private static final int FRAME_WAIT_MS = 50; // Wait for RN to render
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Scan the ScrollView content using PixelCopy + programmatic scroll.
|
|
57
|
+
* Must be called on the UI thread. Blocks for ~100ms per tile.
|
|
58
|
+
*
|
|
59
|
+
* @return WritableArray of tiles, or null on failure
|
|
60
|
+
*/
|
|
61
|
+
public static WritableArray scan(Activity activity) {
|
|
62
|
+
if (activity == null || activity.isFinishing()) return null;
|
|
63
|
+
|
|
64
|
+
// PixelCopy requires API 26+
|
|
65
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
66
|
+
Log.d(TAG, "PixelCopy not available (API < 26), using fallback");
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
View scrollView = findVerticalScrollView(activity);
|
|
72
|
+
if (scrollView == null) {
|
|
73
|
+
Log.d(TAG, "No ScrollView found for PixelCopy scan");
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
View contentView = getContentChild(scrollView);
|
|
78
|
+
if (contentView == null) return null;
|
|
79
|
+
|
|
80
|
+
int viewportHeight = scrollView.getHeight();
|
|
81
|
+
int contentHeight = contentView.getHeight();
|
|
82
|
+
int viewportWidth = scrollView.getWidth();
|
|
83
|
+
|
|
84
|
+
if (viewportWidth <= 0 || viewportHeight <= 0 || contentHeight <= 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get ScrollView position on screen (for cropping)
|
|
89
|
+
int[] scrollViewLoc = new int[2];
|
|
90
|
+
scrollView.getLocationOnScreen(scrollViewLoc);
|
|
91
|
+
Rect scrollViewBounds = new Rect(
|
|
92
|
+
scrollViewLoc[0], scrollViewLoc[1],
|
|
93
|
+
scrollViewLoc[0] + viewportWidth,
|
|
94
|
+
scrollViewLoc[1] + viewportHeight
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
Window window = activity.getWindow();
|
|
98
|
+
int originalScrollY = scrollView.getScrollY();
|
|
99
|
+
|
|
100
|
+
WritableArray tiles = Arguments.createArray();
|
|
101
|
+
int step = (int) (viewportHeight * 0.75); // 25% overlap
|
|
102
|
+
|
|
103
|
+
// Create a handler thread for PixelCopy callbacks
|
|
104
|
+
HandlerThread handlerThread = new HandlerThread("PixelCopyThread");
|
|
105
|
+
handlerThread.start();
|
|
106
|
+
Handler handler = new Handler(handlerThread.getLooper());
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
int y = 0;
|
|
110
|
+
while (y < contentHeight) {
|
|
111
|
+
int scrollTarget = Math.min(y, contentHeight - viewportHeight);
|
|
112
|
+
if (scrollTarget < 0) scrollTarget = 0;
|
|
113
|
+
|
|
114
|
+
// Scroll to position
|
|
115
|
+
scrollView.scrollTo(0, scrollTarget);
|
|
116
|
+
|
|
117
|
+
// Wait for React Native to render the new content
|
|
118
|
+
Thread.sleep(FRAME_WAIT_MS);
|
|
119
|
+
|
|
120
|
+
// Capture using PixelCopy
|
|
121
|
+
WritableMap tile = captureWithPixelCopy(
|
|
122
|
+
window, scrollViewBounds, handler,
|
|
123
|
+
scrollTarget, viewportWidth, viewportHeight, contentHeight
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (tile != null) {
|
|
127
|
+
tiles.pushMap(tile);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (y >= contentHeight - viewportHeight) break;
|
|
131
|
+
y += step;
|
|
132
|
+
if (y > contentHeight - viewportHeight) {
|
|
133
|
+
y = contentHeight - viewportHeight;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
// Restore scroll position
|
|
138
|
+
scrollView.scrollTo(0, originalScrollY);
|
|
139
|
+
handlerThread.quitSafely();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Log.d(TAG, "PixelCopy scan: " + tiles.size() + " tiles, " +
|
|
143
|
+
"content=" + contentHeight + "px");
|
|
144
|
+
|
|
145
|
+
return tiles;
|
|
146
|
+
|
|
147
|
+
} catch (Exception e) {
|
|
148
|
+
Log.w(TAG, "PixelCopyScan failed: " + e.getMessage());
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Capture a tile using PixelCopy, cropping to the ScrollView's bounds.
|
|
155
|
+
*/
|
|
156
|
+
private static WritableMap captureWithPixelCopy(
|
|
157
|
+
Window window, Rect cropBounds, Handler handler,
|
|
158
|
+
int scrollY, int viewportWidth, int viewportHeight, int contentHeight) {
|
|
159
|
+
|
|
160
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
// Create bitmap for the crop area
|
|
164
|
+
Bitmap bitmap = Bitmap.createBitmap(
|
|
165
|
+
cropBounds.width(), cropBounds.height(),
|
|
166
|
+
Bitmap.Config.ARGB_8888
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
CountDownLatch latch = new CountDownLatch(1);
|
|
170
|
+
final int[] resultCode = new int[]{-1};
|
|
171
|
+
|
|
172
|
+
PixelCopy.request(window, cropBounds, bitmap, (result) -> {
|
|
173
|
+
resultCode[0] = result;
|
|
174
|
+
latch.countDown();
|
|
175
|
+
}, handler);
|
|
176
|
+
|
|
177
|
+
// Wait for PixelCopy to complete (max 1 second)
|
|
178
|
+
if (!latch.await(1, TimeUnit.SECONDS)) {
|
|
179
|
+
bitmap.recycle();
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (resultCode[0] != PixelCopy.SUCCESS) {
|
|
184
|
+
bitmap.recycle();
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Scale to target width
|
|
189
|
+
float scale = (float) TARGET_WIDTH / viewportWidth;
|
|
190
|
+
int outWidth = TARGET_WIDTH;
|
|
191
|
+
int outHeight = Math.round(viewportHeight * scale);
|
|
192
|
+
|
|
193
|
+
Bitmap scaled = Bitmap.createScaledBitmap(bitmap, outWidth, outHeight, true);
|
|
194
|
+
bitmap.recycle();
|
|
195
|
+
|
|
196
|
+
// Compress to JPEG
|
|
197
|
+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
198
|
+
scaled.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, baos);
|
|
199
|
+
String base64 = Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP);
|
|
200
|
+
scaled.recycle();
|
|
201
|
+
|
|
202
|
+
WritableMap tile = Arguments.createMap();
|
|
203
|
+
tile.putString("image", base64);
|
|
204
|
+
tile.putInt("width", outWidth);
|
|
205
|
+
tile.putInt("height", outHeight);
|
|
206
|
+
tile.putInt("scrollOffsetY", scrollY);
|
|
207
|
+
tile.putInt("viewportHeight", viewportHeight);
|
|
208
|
+
tile.putInt("contentHeight", contentHeight);
|
|
209
|
+
tile.putString("format", "jpeg");
|
|
210
|
+
return tile;
|
|
211
|
+
|
|
212
|
+
} catch (Exception e) {
|
|
213
|
+
Log.w(TAG, "PixelCopy at Y=" + scrollY + " failed: " + e.getMessage());
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- View finders (same as ScrollScanCapture) ---
|
|
219
|
+
|
|
220
|
+
@SuppressWarnings("unchecked")
|
|
221
|
+
private static View findVerticalScrollView(Activity activity) {
|
|
222
|
+
View decorView = activity.getWindow().getDecorView();
|
|
223
|
+
View found = findInTree(decorView);
|
|
224
|
+
if (found != null) return found;
|
|
225
|
+
try {
|
|
226
|
+
Class<?> wmgClass = Class.forName("android.view.WindowManagerGlobal");
|
|
227
|
+
Object wmg = wmgClass.getMethod("getInstance").invoke(null);
|
|
228
|
+
Field viewsField = wmgClass.getDeclaredField("mViews");
|
|
229
|
+
viewsField.setAccessible(true);
|
|
230
|
+
ArrayList<View> views = (ArrayList<View>) viewsField.get(wmg);
|
|
231
|
+
if (views != null) {
|
|
232
|
+
for (View root : views) {
|
|
233
|
+
if (root == decorView) continue;
|
|
234
|
+
found = findInTree(root);
|
|
235
|
+
if (found != null) return found;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (Exception e) { }
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private static View findInTree(View view) {
|
|
243
|
+
if (view == null) return null;
|
|
244
|
+
if (isVerticalScroll(view) && view instanceof ViewGroup
|
|
245
|
+
&& ((ViewGroup) view).getChildCount() > 0) {
|
|
246
|
+
return view;
|
|
247
|
+
}
|
|
248
|
+
if (view instanceof ViewGroup) {
|
|
249
|
+
ViewGroup group = (ViewGroup) view;
|
|
250
|
+
for (int i = 0; i < group.getChildCount(); i++) {
|
|
251
|
+
View found = findInTree(group.getChildAt(i));
|
|
252
|
+
if (found != null) return found;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private static boolean isVerticalScroll(View view) {
|
|
259
|
+
if (view instanceof ScrollView) return true;
|
|
260
|
+
Class<?> clazz = view.getClass();
|
|
261
|
+
while (clazz != null && clazz != View.class) {
|
|
262
|
+
String name = clazz.getName();
|
|
263
|
+
if (name.contains("Horizontal")) return false;
|
|
264
|
+
if (name.contains("NestedScrollView") ||
|
|
265
|
+
name.contains("ReactScrollView") ||
|
|
266
|
+
name.endsWith("ScrollView")) return true;
|
|
267
|
+
clazz = clazz.getSuperclass();
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private static View getContentChild(View scrollView) {
|
|
273
|
+
if (scrollView instanceof ViewGroup) {
|
|
274
|
+
ViewGroup group = (ViewGroup) scrollView;
|
|
275
|
+
if (group.getChildCount() > 0) return group.getChildAt(0);
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -152,7 +152,14 @@ public class ScreenCaptureModule extends ReactContextBaseJavaModule {
|
|
|
152
152
|
|
|
153
153
|
mainHandler.post(() -> {
|
|
154
154
|
try {
|
|
155
|
-
|
|
155
|
+
// Try PixelCopy (API 26+, captures from GPU framebuffer)
|
|
156
|
+
WritableArray tiles = PixelCopyScan.scan(activity);
|
|
157
|
+
|
|
158
|
+
// Fallback to View.draw scan
|
|
159
|
+
if (tiles == null || tiles.size() == 0) {
|
|
160
|
+
tiles = ScrollScanCapture.scan(activity);
|
|
161
|
+
}
|
|
162
|
+
|
|
156
163
|
promise.resolve(tiles);
|
|
157
164
|
} catch (Exception e) {
|
|
158
165
|
Log.w(TAG, "scanFullContent failed: " + e.getMessage());
|
|
@@ -38,11 +38,11 @@ const react_1 = __importStar(require("react"));
|
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
39
|
const client_1 = require("../core/client");
|
|
40
40
|
/** Minimum scroll delta (px) to trigger a new tile capture. */
|
|
41
|
-
const MIN_DELTA_PX =
|
|
41
|
+
const MIN_DELTA_PX = 20;
|
|
42
42
|
/** Debounce: wait this long after scroll stops before capturing. */
|
|
43
|
-
const DEBOUNCE_MS =
|
|
43
|
+
const DEBOUNCE_MS = 300;
|
|
44
44
|
/** Throttle: minimum time between tile captures. */
|
|
45
|
-
const THROTTLE_MS =
|
|
45
|
+
const THROTTLE_MS = 500;
|
|
46
46
|
/**
|
|
47
47
|
* Drop-in replacement for React Native's ScrollView that:
|
|
48
48
|
*
|
|
@@ -51,7 +51,7 @@ const THROTTLE_MS = 3000;
|
|
|
51
51
|
* screen building — the backend composites these into a
|
|
52
52
|
* full-content image over time from multiple user sessions
|
|
53
53
|
*/
|
|
54
|
-
exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumScrollEnd, onLayout, ...props }, ref) => {
|
|
54
|
+
exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumScrollEnd, onScrollEndDrag, onLayout, ...props }, ref) => {
|
|
55
55
|
const debounceTimer = (0, react_1.useRef)(null);
|
|
56
56
|
const lastCaptureTime = (0, react_1.useRef)(0);
|
|
57
57
|
const lastCaptureY = (0, react_1.useRef)(-999);
|
|
@@ -109,12 +109,17 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
109
109
|
}
|
|
110
110
|
onScroll?.(event);
|
|
111
111
|
}, [onScroll]);
|
|
112
|
+
const handleScrollEndDrag = (0, react_1.useCallback)((event) => {
|
|
113
|
+
// User lifted finger — capture immediately
|
|
114
|
+
doCapture();
|
|
115
|
+
onScrollEndDrag?.(event);
|
|
116
|
+
}, [onScrollEndDrag]);
|
|
112
117
|
const handleMomentumEnd = (0, react_1.useCallback)((event) => {
|
|
113
118
|
if (debounceTimer.current) {
|
|
114
119
|
clearTimeout(debounceTimer.current);
|
|
115
120
|
debounceTimer.current = null;
|
|
116
121
|
}
|
|
117
|
-
|
|
122
|
+
doCapture();
|
|
118
123
|
onMomentumScrollEnd?.(event);
|
|
119
124
|
}, [onMomentumScrollEnd]);
|
|
120
125
|
const handleLayout = (0, react_1.useCallback)((event) => {
|
|
@@ -130,6 +135,6 @@ exports.RepliqoScrollView = react_1.default.forwardRef(({ onScroll, onMomentumSc
|
|
|
130
135
|
}
|
|
131
136
|
onLayout?.(event);
|
|
132
137
|
}, [onLayout]);
|
|
133
|
-
return (<react_native_1.ScrollView ref={ref} {...props} onScroll={handleScroll} onMomentumScrollEnd={handleMomentumEnd} onLayout={handleLayout} scrollEventThrottle={props.scrollEventThrottle ?? 16}/>);
|
|
138
|
+
return (<react_native_1.ScrollView ref={ref} {...props} onScroll={handleScroll} onScrollEndDrag={handleScrollEndDrag} onMomentumScrollEnd={handleMomentumEnd} onLayout={handleLayout} scrollEventThrottle={props.scrollEventThrottle ?? 16}/>);
|
|
134
139
|
});
|
|
135
140
|
exports.RepliqoScrollView.displayName = 'RepliqoScrollView';
|
package/dist/core/client.js
CHANGED
|
@@ -284,14 +284,15 @@ class AppAnalytics {
|
|
|
284
284
|
this.scrollOffsetY = 0;
|
|
285
285
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
286
286
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
287
|
-
// Auto-scan:
|
|
288
|
-
//
|
|
287
|
+
// Auto-scan with PixelCopy: captures entire ScrollView content
|
|
288
|
+
// by scrolling programmatically + PixelCopy from GPU framebuffer.
|
|
289
|
+
// Fires 2s after screen enter to let content fully render.
|
|
289
290
|
if (this.config.enableHeatmapCapture) {
|
|
290
291
|
setTimeout(() => {
|
|
291
292
|
if (this.currentScreen !== screenName || !this.sessionId)
|
|
292
293
|
return;
|
|
293
294
|
this.autoScanScreen(screenName);
|
|
294
|
-
},
|
|
295
|
+
}, 2000);
|
|
295
296
|
}
|
|
296
297
|
this.logger.log('Screen entered:', screenName);
|
|
297
298
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repliqo/sdk-react-native",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Session replay & analytics SDK for React Native. Captures screenshots (including modals/alerts), navigation, screen visits, and custom events.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
import { AppAnalytics } from '../core/client';
|
|
10
10
|
|
|
11
11
|
/** Minimum scroll delta (px) to trigger a new tile capture. */
|
|
12
|
-
const MIN_DELTA_PX =
|
|
12
|
+
const MIN_DELTA_PX = 20;
|
|
13
13
|
/** Debounce: wait this long after scroll stops before capturing. */
|
|
14
|
-
const DEBOUNCE_MS =
|
|
14
|
+
const DEBOUNCE_MS = 300;
|
|
15
15
|
/** Throttle: minimum time between tile captures. */
|
|
16
|
-
const THROTTLE_MS =
|
|
16
|
+
const THROTTLE_MS = 500;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Drop-in replacement for React Native's ScrollView that:
|
|
@@ -24,7 +24,7 @@ const THROTTLE_MS = 3000;
|
|
|
24
24
|
* full-content image over time from multiple user sessions
|
|
25
25
|
*/
|
|
26
26
|
export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
27
|
-
({ onScroll, onMomentumScrollEnd, onLayout, ...props }, ref) => {
|
|
27
|
+
({ onScroll, onMomentumScrollEnd, onScrollEndDrag, onLayout, ...props }, ref) => {
|
|
28
28
|
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
29
29
|
const lastCaptureTime = useRef(0);
|
|
30
30
|
const lastCaptureY = useRef(-999);
|
|
@@ -99,13 +99,22 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
99
99
|
[onScroll],
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
+
const handleScrollEndDrag = useCallback(
|
|
103
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
104
|
+
// User lifted finger — capture immediately
|
|
105
|
+
doCapture();
|
|
106
|
+
onScrollEndDrag?.(event);
|
|
107
|
+
},
|
|
108
|
+
[onScrollEndDrag],
|
|
109
|
+
);
|
|
110
|
+
|
|
102
111
|
const handleMomentumEnd = useCallback(
|
|
103
112
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
104
113
|
if (debounceTimer.current) {
|
|
105
114
|
clearTimeout(debounceTimer.current);
|
|
106
115
|
debounceTimer.current = null;
|
|
107
116
|
}
|
|
108
|
-
|
|
117
|
+
doCapture();
|
|
109
118
|
onMomentumScrollEnd?.(event);
|
|
110
119
|
},
|
|
111
120
|
[onMomentumScrollEnd],
|
|
@@ -133,6 +142,7 @@ export const RepliqoScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
|
|
|
133
142
|
ref={ref}
|
|
134
143
|
{...props}
|
|
135
144
|
onScroll={handleScroll}
|
|
145
|
+
onScrollEndDrag={handleScrollEndDrag}
|
|
136
146
|
onMomentumScrollEnd={handleMomentumEnd}
|
|
137
147
|
onLayout={handleLayout}
|
|
138
148
|
scrollEventThrottle={props.scrollEventThrottle ?? 16}
|
package/src/core/client.ts
CHANGED
|
@@ -416,13 +416,14 @@ export class AppAnalytics {
|
|
|
416
416
|
this.scrollOffsetY = 0;
|
|
417
417
|
this.errorTracker?.setCurrentScreen(screenName);
|
|
418
418
|
this.snapshotCapture?.setCurrentScreen(screenName);
|
|
419
|
-
// Auto-scan:
|
|
420
|
-
//
|
|
419
|
+
// Auto-scan with PixelCopy: captures entire ScrollView content
|
|
420
|
+
// by scrolling programmatically + PixelCopy from GPU framebuffer.
|
|
421
|
+
// Fires 2s after screen enter to let content fully render.
|
|
421
422
|
if (this.config.enableHeatmapCapture) {
|
|
422
423
|
setTimeout(() => {
|
|
423
424
|
if (this.currentScreen !== screenName || !this.sessionId) return;
|
|
424
425
|
this.autoScanScreen(screenName);
|
|
425
|
-
},
|
|
426
|
+
}, 2000);
|
|
426
427
|
}
|
|
427
428
|
|
|
428
429
|
this.logger.log('Screen entered:', screenName);
|