能否像Google Maps一样对PDFtron Flutter中的PDF批注进行聚类?
实现类似Google Maps的PDF批注聚类功能(基于Flutter Pdftron)
核心思路
借助Pdftron的批注API获取所有批注的位置信息,结合当前PDF视图的缩放比例、滚动偏移完成PDF坐标到屏幕坐标的转换,通过密度聚类算法将邻近批注分组,在缩小时渲染聚类标记(显示组内批注数量),点击标记时自动放大并展开组内所有批注,解决缩小后批注重叠无法操作的问题。
具体实现步骤
1. 获取批注及坐标转换
通过Pdftron的AnnotationManager提取所有批注的边界框,转换为屏幕坐标系下的位置:
// 获取当前PDF所有批注 List<Annot> annotations = await PdftronFlutter.getAnnotations(); // 存储转换后的批注数据 List<Map<String, dynamic>> annotScreenData = []; // 获取当前视图状态 double currentZoom = await PdftronFlutter.getZoom(); Offset scrollOffset = await PdftronFlutter.getScrollOffset(); for (var annot in annotations) { Rect pdfRect = annot.boundingRect; // 将PDF坐标转换为屏幕坐标 Rect screenRect = Rect.fromLTWH( (pdfRect.left - scrollOffset.dx) * currentZoom, (pdfRect.top - scrollOffset.dy) * currentZoom, pdfRect.width * currentZoom, pdfRect.height * currentZoom, ); annotScreenData.add({ 'annot': annot, 'screenCenter': Offset(screenRect.center.dx, screenRect.center.dy), 'screenRect': screenRect, }); }
2. 实现密度聚类算法
采用DBSCAN算法(适合空间密度聚类),根据屏幕距离阈值将邻近批注分组:
// 聚类核心方法:输入批注数据、距离阈值,返回聚类结果 List<List<Map<String, dynamic>>> clusterAnnotations(List<Map<String, dynamic>> annots, double eps) { List<List<Map<String, dynamic>>> clusters = []; List<bool> visited = List.filled(annots.length, false); for (int i = 0; i < annots.length; i++) { if (visited[i]) continue; visited[i] = true; List<Map<String, dynamic>> neighbors = _getNearbyAnnots(annots, i, eps); // 仅保留包含多个批注的聚类 if (neighbors.length >= 1) { List<Map<String, dynamic>> cluster = [annots[i]]; for (var neighbor in neighbors) { int idx = annots.indexOf(neighbor); if (!visited[idx]) { visited[idx] = true; cluster.add(neighbor); neighbors.addAll(_getNearbyAnnots(annots, idx, eps)); } } clusters.add(cluster); } } return clusters; } // 获取指定批注的邻近批注 List<Map<String, dynamic>> _getNearbyAnnots(List<Map<String, dynamic>> annots, int idx, double eps) { List<Map<String, dynamic>> result = []; Offset targetCenter = annots[idx]['screenCenter']; for (int i = 0; i < annots.length; i++) { if (i == idx) continue; double distance = (annots[i]['screenCenter'] as Offset).distanceTo(targetCenter); if (distance <= eps) result.add(annots[i]); } return result; }
3. 渲染聚类标记与交互逻辑
在Pdftron视图上方叠加自定义绘制层,根据缩放比例切换显示单个批注或聚类标记:
// 设定触发聚类的缩放阈值 const double clusterZoomThreshold = 0.5; double currentZoom = 1.0; List<List<Map<String, dynamic>>> currentClusters = []; @override Widget build(BuildContext context) { return Stack( children: [ // Pdftron PDF视图 PdftronFlutter.view( onDocumentLoaded: () => _refreshAnnotations(), onZoomChanged: (double zoom) { currentZoom = zoom; if (zoom < clusterZoomThreshold) { // 缩小时计算聚类 currentClusters = clusterAnnotations(annotScreenData, 40); } else { currentClusters.clear(); } setState(() {}); }, ), // 聚类标记绘制层 if (currentZoom < clusterZoomThreshold) Positioned.fill( child: GestureDetector( onTapUp: (TapUpDetails details) => _handleClusterTap(details.localPosition), child: CustomPaint( painter: ClusterMarkerPainter(currentClusters), ), ), ), ], ); } // 处理聚类标记点击:放大到阈值比例并滚动到聚类中心 void _handleClusterTap(Offset tapPos) { for (var cluster in currentClusters) { Offset clusterCenter = Offset( cluster.map((a) => a['screenCenter'].dx).reduce((a,b) => a+b) / cluster.length, cluster.map((a) => a['screenCenter'].dy).reduce((a,b) => a+b) / cluster.length, ); if ((tapPos - clusterCenter).distance <= 20) { // 转换回PDF坐标并滚动 PdftronFlutter.setZoom(clusterZoomThreshold); PdftronFlutter.scrollTo( clusterCenter.dx / clusterZoomThreshold, clusterCenter.dy / clusterZoomThreshold, ); break; } } } // 自定义聚类标记绘制器 class ClusterMarkerPainter extends CustomPainter { final List<List<Map<String, dynamic>>> clusters; ClusterMarkerPainter(this.clusters); @override void paint(Canvas canvas, Size size) { final circlePaint = Paint()..color = Colors.deepOrange.withOpacity(0.8); final textPainter = TextPainter(textDirection: TextDirection.ltr); for (var cluster in clusters) { Offset center = Offset( cluster.map((a) => a['screenCenter'].dx).reduce((a,b) => a+b) / cluster.length, cluster.map((a) => a['screenCenter'].dy).reduce((a,b) => a+b) / cluster.length, ); // 绘制聚类圆形标记 canvas.drawCircle(center, 20, circlePaint); // 绘制批注数量文本 textPainter.text = TextSpan( text: cluster.length.toString(), style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), ); textPainter.layout(); textPainter.paint( canvas, Offset(center.dx - textPainter.width/2, center.dy - textPainter.height/2), ); } } @override bool shouldRepaint(covariant ClusterMarkerPainter oldDelegate) { return oldDelegate.clusters != clusters; } }
4. 关键优化点
- 动态阈值调整:可根据当前缩放比例动态修改聚类的距离阈值,缩放越小阈值越大,适配不同视图层级
- 性能优化:批注数量较多时,将聚类计算放在
compute异步线程执行,避免UI卡顿 - 坐标同步:监听PDF视图的滚动、缩放事件,实时更新批注的屏幕坐标,确保聚类位置准确
内容的提问来源于stack exchange,提问作者Ruchit Soni
相关产品推荐
相关产品推荐

