Initial commit: 91dj android client dev_1.0.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user