Skip to content

Commit 25a3690

Browse files
committed
feat: Refactor screenshot capture for improved performance and reliability
This commit refactors the `captureScreenshot` method in `ScreenshotCapturer` to improve performance and reliability. The changes include: - **Synchronous Setup:** The method now performs synchronous operations first (getting context, render object, calculating pixel ratio, and getting the initial image) before executing the main process asynchronously. This ensures that the initial setup is done on the main UI thread, as required. - **Asynchronous Processing:** The main image processing and masking logic are now executed asynchronously using a `Future` and `Completer`, preventing UI jank. - **Snapshot Management:** The `_views` Expando is replaced with a `SnapshotManager` to manage the status of the view, improving code organization and maintainability.
1 parent 100e2f7 commit 25a3690

File tree

6 files changed

+197
-120
lines changed

6 files changed

+197
-120
lines changed

lib/src/posthog_widget_widget.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:async';
33
import 'package:flutter/material.dart';
44
import 'package:posthog_flutter/posthog_flutter.dart';
55
import 'package:posthog_flutter/src/replay/mask/posthog_mask_controller.dart';
6+
import 'package:posthog_flutter/src/util/logging.dart';
67

78
import 'replay/change_detector.dart';
89
import 'replay/native_communicator.dart';
@@ -65,14 +66,11 @@ class PostHogWidgetState extends State<PostHogWidget> {
6566
}
6667

6768
Future<void> _generateSnapshot() async {
68-
final isSessionReplayActive =
69-
await _nativeCommunicator?.isSessionReplayActive() ?? false;
70-
if (!isSessionReplayActive) {
71-
return;
72-
}
73-
69+
// Ensure no asynchronous calls occur before this function,
70+
// as it relies on a consistent state.
7471
final imageInfo = await _screenshotCapturer?.captureScreenshot();
7572
if (imageInfo == null) {
73+
printIfDebug('Error: Failed to capture screenshot.');
7674
return;
7775
}
7876

lib/src/replay/element_parsers/element_data.dart

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,20 @@ class ElementData {
1616
children?.add(elementData);
1717
}
1818

19-
List<Rect> extractRects([bool isRoot = true]) {
20-
List<Rect> rects = [];
21-
22-
if (!isRoot) {
23-
rects.add(rect);
24-
}
19+
List<ElementData> extractRects([bool isRoot = true]) {
20+
List<ElementData> rects = [];
2521

2622
if (children != null) {
2723
for (var child in children ?? []) {
2824
if (child.children == null) {
29-
rects.add(child.rect);
25+
rects.add(child);
3026
continue;
3127
} else if ((child.children?.length ?? 0) > 1) {
3228
for (var grandChild in child.children ?? []) {
33-
rects.add(grandChild.rect);
29+
rects.add(grandChild);
3430
}
3531
} else {
36-
rects.add(child.rect);
32+
rects.add(child);
3733
}
3834
}
3935
}
Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,18 @@
1-
import 'dart:ui' as ui;
21
import 'package:flutter/material.dart';
2+
import 'package:posthog_flutter/src/replay/element_parsers/element_data.dart';
33

44
class ImageMaskPainter {
5-
Future<ui.Image> drawMaskedImage(
6-
ui.Image image, List<Rect> rects, double pixelRatio) async {
7-
final recorder = ui.PictureRecorder();
8-
final canvas = Canvas(recorder);
9-
final paint = Paint();
10-
11-
canvas.drawImage(image, Offset.zero, paint);
12-
13-
final rectPaint = Paint()
14-
..color = Colors.black
15-
..style = PaintingStyle.fill;
16-
17-
for (Rect rect in rects) {
18-
Rect scaledRect = Rect.fromLTRB(
19-
rect.left * pixelRatio,
20-
rect.top * pixelRatio,
21-
rect.right * pixelRatio,
22-
rect.bottom * pixelRatio,
23-
);
24-
canvas.drawRect(scaledRect, rectPaint);
5+
void drawMaskedImage(
6+
Canvas canvas, List<ElementData> items, double pixelRatio) {
7+
final paint = Paint()..style = PaintingStyle.fill;
8+
for (var elementData in items) {
9+
paint.color = Colors.black;
10+
final scaled = Rect.fromLTRB(
11+
elementData.rect.left * pixelRatio,
12+
elementData.rect.top * pixelRatio,
13+
elementData.rect.right * pixelRatio,
14+
elementData.rect.bottom * pixelRatio);
15+
canvas.drawRect(scaled, paint);
2516
}
26-
27-
final picture = recorder.endRecording();
28-
29-
final maskedImage = await picture.toImage(
30-
(image.width * pixelRatio).round(),
31-
(image.height * pixelRatio).round(),
32-
);
33-
return maskedImage;
3417
}
3518
}

lib/src/replay/mask/posthog_mask_controller.dart

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'dart:async';
2-
31
import 'package:flutter/material.dart';
42
import 'package:posthog_flutter/posthog_flutter.dart';
53
import 'package:posthog_flutter/src/replay/element_parsers/element_data.dart';
@@ -10,6 +8,7 @@ import 'package:posthog_flutter/src/replay/element_parsers/element_data_factory.
108
import 'package:posthog_flutter/src/replay/element_parsers/element_object_parser.dart';
119
import 'package:posthog_flutter/src/replay/element_parsers/root_element_provider.dart';
1210
import 'package:posthog_flutter/src/replay/mask/widget_elements_decipher.dart';
11+
import 'package:posthog_flutter/src/util/logging.dart';
1312

1413
class PostHogMaskController {
1514
late final Map<String, ElementParser> parsers;
@@ -32,15 +31,41 @@ class PostHogMaskController {
3231
PostHogMaskController._privateConstructor(
3332
Posthog().config?.sessionReplayConfig);
3433

35-
Future<List<Rect>?> getCurrentScreenRects() async {
34+
/// Extracts a flattened list of [ElementData] objects representing the
35+
/// renderable elements in the widget tree.
36+
///
37+
/// This method traverses the tree of [ElementData] objects and returns a
38+
/// list of elements that have no children or only one child.
39+
///
40+
/// The method is designed to extract the elements that are directly
41+
/// renderable on the screen.
42+
///
43+
/// **Returns:**
44+
/// - `List<ElementData>`: A list of [ElementData] objects representing the
45+
/// renderable elements.
46+
///
47+
List<ElementData>? getCurrentWidgetsElements() {
3648
final BuildContext? context = containerKey.currentContext;
3749

3850
if (context == null) {
51+
printIfDebug('Error: containerKey.currentContext is null.');
3952
return null;
4053
}
41-
final ElementData? widgetElementsTree =
42-
_widgetScraper.parseRenderTree(context);
4354

44-
return widgetElementsTree?.extractRects();
55+
try {
56+
final ElementData? widgetElementsTree =
57+
_widgetScraper.parseRenderTree(context);
58+
59+
if (widgetElementsTree == null) {
60+
printIfDebug('Error: widgetElementsTree is null after parsing.');
61+
return null;
62+
}
63+
64+
return widgetElementsTree.extractRects();
65+
} catch (e) {
66+
printIfDebug(
67+
'Error during render tree parsing or rectangle extraction: $e');
68+
return null;
69+
}
4570
}
4671
}

0 commit comments

Comments
 (0)