Files
hgmj_android/analytics_sdk/lib/widget/global_click_wrapper.dart
T
2026-10-08 11:37:25 +07:00

394 lines
13 KiB
Dart

import 'package:analytics_sdk/config/sdk_config.dart';
import 'package:analytics_sdk/entity/page_click_event.dart';
import 'package:analytics_sdk/manager/session_behavior_manager.dart';
import 'package:analytics_sdk/utils/logger.dart';
import 'package:analytics_sdk/manager/page_name_manager.dart';
import 'package:analytics_sdk/observer/page_lifecycle_observer.dart';
import 'package:analytics_sdk/utils/widget_bridge.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class GlobalClickWrapper extends StatefulWidget {
final Widget child;
const GlobalClickWrapper({
required this.child,
super.key,
});
@override
State<GlobalClickWrapper> createState() => _GlobalClickWrapperState();
}
class _GlobalClickWrapperState extends State<GlobalClickWrapper> {
DateTime? _lastClickTime;
Offset? _pointerDownPosition;
bool _pointerScrolled = false;
_ClickScrollSnapshot? _lastScrollSnapshot;
final Map<int, _PointerTouchStart> _touchStarts = <int, _PointerTouchStart>{};
final Map<int, Offset> _activeTouchPositions = <int, Offset>{};
double? _pinchStartDistance;
bool _pinchRecorded = false;
static const double _swipeThresholdPx = 50;
static const double _pinchThresholdPx = 20;
static const double _longPressMoveTolerancePx = 10;
static const _longPressDuration = Duration(milliseconds: 500);
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
_recordScroll(notification);
return false;
},
child: Listener(
onPointerDown: (PointerDownEvent event) {
_pointerDownPosition = event.position;
_pointerScrolled = false;
_recordPointerDown(event);
},
onPointerMove: (PointerMoveEvent event) {
if (!_pointerScrolled && _pointerDownPosition != null) {
final dist = (event.position - _pointerDownPosition!).distance;
if (dist > kTouchSlop) {
_pointerScrolled = true;
}
}
_recordPointerMove(event);
},
onPointerUp: (PointerUpEvent event) {
if (!_pointerScrolled) {
_handleTap(event);
}
_recordPointerUp(event);
_pointerDownPosition = null;
_pointerScrolled = false;
},
onPointerCancel: (event) {
_recordPointerCancel(event);
_pointerDownPosition = null;
_pointerScrolled = false;
},
// 透传所有事件,不影响原有交互
behavior: HitTestBehavior.translucent,
child: widget.child,
),
);
}
void _handleTap(PointerUpEvent event) {
try {
// 点击节流:限制点击事件上报频率,避免高频点击造成性能问题
final now = DateTime.now();
if (_lastClickTime != null &&
now.difference(_lastClickTime!) < SdkConfig.clickThrottleDuration) {
return;
}
_lastClickTime = now;
// 允许触摸(移动端)、鼠标(桌面/Web)、触控板(macOS);过滤触控笔等其他设备。
final kind = event.kind;
if (kind != PointerDeviceKind.touch &&
kind != PointerDeviceKind.mouse &&
kind != PointerDeviceKind.trackpad) {
return;
}
final pageKey = PageLifecycleObserver.currentPageKey;
if (pageKey.isEmpty || pageKey == 'unknown_page') {
return;
}
final pageName = PageNameMapper.getPageName(pageKey);
// 保存事件位置信息,避免在异步回调中访问已失效的事件对象
final clickPosition = event.position;
// 异步获取 RenderBox,避免在 build 阶段调用 findRenderObject
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
if (!mounted) return;
final renderObject = context.findRenderObject();
if (renderObject == null || renderObject is! RenderBox) return;
final renderBox = renderObject;
final size = renderBox.size;
final clickX = clickPosition.dx.round();
final clickY = clickPosition.dy.round();
final localPosition = renderBox.globalToLocal(clickPosition);
final clickViewportX = localPosition.dx.round();
final clickViewportY = localPosition.dy.round();
final viewportWidth = size.width.round();
final viewportHeight = size.height.round();
final percentX = viewportWidth > 0
? (clickViewportX / viewportWidth * 100).round().clamp(0, 100)
: 0;
final percentY = viewportHeight > 0
? (clickViewportY / viewportHeight * 100).round().clamp(0, 100)
: 0;
final scrollSnapshot = _scrollSnapshotFor(
pageKey: pageKey,
clickViewportX: clickViewportX,
clickViewportY: clickViewportY,
viewportWidth: viewportWidth,
viewportHeight: viewportHeight,
);
Logger.globalClick(
'上报点击成功 → $pageKey ($clickX,$clickY) [$percentX%,$percentY%]');
WidgetBridge.track(PageClickEvent.fromViewport(
pageKey: pageKey,
pageName: pageName,
clickPageX: clickX,
clickPageY: clickY,
clickViewportX: clickViewportX,
clickViewportY: clickViewportY,
viewportWidth: viewportWidth,
viewportHeight: viewportHeight,
clickDocumentX: scrollSnapshot?.clickDocumentX,
clickDocumentY: scrollSnapshot?.clickDocumentY,
pageWidth: scrollSnapshot?.pageWidth,
pageHeight: scrollSnapshot?.pageHeight,
scrollTop: scrollSnapshot?.scrollTop,
scrollLeft: scrollSnapshot?.scrollLeft,
));
} catch (e) {
Logger.globalClick('点击事件处理异常,已安全处理: $e');
}
});
} catch (e) {
Logger.globalClick('_handleTap() 异常,已安全处理: $e');
}
}
void _recordScroll(ScrollNotification notification) {
try {
final pageKey = PageLifecycleObserver.currentPageKey;
if (pageKey.isEmpty || pageKey == 'unknown_page') return;
final metrics = notification.metrics;
final scrollOffset = _roundedNonNegativeFinite(metrics.pixels);
final viewportExtent =
_roundedNonNegativeFinite(metrics.viewportDimension);
final contentExtent = _roundedNonNegativeFinite(
metrics.maxScrollExtent + metrics.viewportDimension);
if (scrollOffset == null ||
viewportExtent == null ||
contentExtent == null ||
contentExtent <= 0) {
return;
}
if (metrics.axis == Axis.vertical) {
SessionBehaviorManager.instance.recordScroll(
scrollTop: scrollOffset,
viewportHeight: viewportExtent,
contentHeight: contentExtent,
);
_lastScrollSnapshot = _ClickScrollSnapshot(
pageKey: pageKey,
scrollTop: scrollOffset,
scrollLeft: 0,
pageWidth: -1,
pageHeight: contentExtent,
);
} else if (metrics.axis == Axis.horizontal) {
_lastScrollSnapshot = _ClickScrollSnapshot(
pageKey: pageKey,
scrollTop: 0,
scrollLeft: scrollOffset,
pageWidth: contentExtent,
pageHeight: -1,
);
}
} catch (e) {
Logger.globalClick('滚动快照记录异常,已安全处理: $e');
}
}
void _recordPointerDown(PointerDownEvent event) {
try {
if (event.kind != PointerDeviceKind.touch) return;
final now = DateTime.now();
if (_activeTouchPositions.isEmpty) {
SessionBehaviorManager.instance.recordTouchStart(at: now);
} else {
_pointerScrolled = true;
}
_touchStarts[event.pointer] = _PointerTouchStart(
position: event.position,
startedAt: now,
);
_activeTouchPositions[event.pointer] = event.position;
_updatePinchBaseline();
} catch (e) {
Logger.globalClick('触摸开始行为记录异常,已安全处理: $e');
}
}
void _recordPointerMove(PointerMoveEvent event) {
try {
if (event.kind != PointerDeviceKind.touch) return;
if (!_activeTouchPositions.containsKey(event.pointer)) return;
_activeTouchPositions[event.pointer] = event.position;
if (_activeTouchPositions.length < 2) return;
final distance = _firstTwoTouchDistance();
if (distance == null) return;
_pinchStartDistance ??= distance;
if (!_pinchRecorded &&
(distance - _pinchStartDistance!).abs() >= _pinchThresholdPx) {
_pinchRecorded = true;
SessionBehaviorManager.instance.recordPinch();
}
} catch (e) {
Logger.globalClick('触摸移动行为记录异常,已安全处理: $e');
}
}
void _recordPointerUp(PointerUpEvent event) {
try {
if (event.kind != PointerDeviceKind.touch) return;
final now = DateTime.now();
final wasSingleTouch = _activeTouchPositions.length == 1;
final start = _touchStarts[event.pointer];
if (start != null && wasSingleTouch) {
_recordFinishedSingleTouch(
start: start,
endPosition: event.position,
endedAt: now,
);
}
_touchStarts.remove(event.pointer);
_activeTouchPositions.remove(event.pointer);
_updatePinchBaseline();
} catch (e) {
Logger.globalClick('触摸结束行为记录异常,已安全处理: $e');
}
}
void _recordPointerCancel(PointerCancelEvent event) {
try {
if (event.kind != PointerDeviceKind.touch) return;
_touchStarts.remove(event.pointer);
_activeTouchPositions.remove(event.pointer);
_updatePinchBaseline();
} catch (e) {
Logger.globalClick('触摸取消行为记录异常,已安全处理: $e');
}
}
void _recordFinishedSingleTouch({
required _PointerTouchStart start,
required Offset endPosition,
required DateTime endedAt,
}) {
final distance = (endPosition - start.position).distance;
if (distance >= _swipeThresholdPx) {
SessionBehaviorManager.instance.recordSwipe(at: endedAt);
return;
}
if (endedAt.difference(start.startedAt) >= _longPressDuration &&
distance <= _longPressMoveTolerancePx) {
SessionBehaviorManager.instance.recordLongPress(at: endedAt);
}
}
void _updatePinchBaseline() {
if (_activeTouchPositions.length < 2) {
_pinchStartDistance = null;
_pinchRecorded = false;
return;
}
_pinchStartDistance = _firstTwoTouchDistance();
_pinchRecorded = false;
}
double? _firstTwoTouchDistance() {
if (_activeTouchPositions.length < 2) return null;
final iterator = _activeTouchPositions.values.iterator;
iterator.moveNext();
final first = iterator.current;
iterator.moveNext();
final second = iterator.current;
return (first - second).distance;
}
_ClickScrollSnapshot? _scrollSnapshotFor({
required String pageKey,
required int clickViewportX,
required int clickViewportY,
required int viewportWidth,
required int viewportHeight,
}) {
final snapshot = _lastScrollSnapshot;
if (snapshot == null || snapshot.pageKey != pageKey) return null;
return snapshot.resolve(
clickViewportX: clickViewportX,
clickViewportY: clickViewportY,
viewportWidth: viewportWidth,
viewportHeight: viewportHeight,
);
}
int _nonNegative(int value) => value < 0 ? 0 : value;
int? _roundedNonNegativeFinite(double value) {
if (!value.isFinite) return null;
return _nonNegative(value.round());
}
}
class _PointerTouchStart {
final Offset position;
final DateTime startedAt;
const _PointerTouchStart({
required this.position,
required this.startedAt,
});
}
class _ClickScrollSnapshot {
final String pageKey;
final int scrollTop;
final int scrollLeft;
final int pageWidth;
final int pageHeight;
final int clickDocumentX;
final int clickDocumentY;
const _ClickScrollSnapshot({
required this.pageKey,
required this.scrollTop,
required this.scrollLeft,
required this.pageWidth,
required this.pageHeight,
this.clickDocumentX = -1,
this.clickDocumentY = -1,
});
_ClickScrollSnapshot resolve({
required int clickViewportX,
required int clickViewportY,
required int viewportWidth,
required int viewportHeight,
}) {
final resolvedPageWidth = pageWidth >= 0 ? pageWidth : viewportWidth;
final resolvedPageHeight = pageHeight >= 0 ? pageHeight : viewportHeight;
return _ClickScrollSnapshot(
pageKey: pageKey,
scrollTop: scrollTop,
scrollLeft: scrollLeft,
pageWidth: resolvedPageWidth,
pageHeight: resolvedPageHeight,
clickDocumentX: clickViewportX + scrollLeft,
clickDocumentY: clickViewportY + scrollTop,
);
}
}