Flutter Draggable与DragTarget实现视频聊天预览框拖拽示例咨询
Flutter 可拖拽停靠摄像头预览容器示例
实现效果:
- 支持摄像头预览容器全屏幕自由拖拽
- 拖拽释放后自动吸附到屏幕上下两端的四个角落(可配置是否强制停靠,还是允许自由放置)
- 默认初始位置位于屏幕右下角
- 适配横竖屏切换场景
import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: '拖拽摄像头预览示例', theme: ThemeData(primarySwatch: Colors.blue), home: const VideoChatPage(), ); } } class VideoChatPage extends StatefulWidget { const VideoChatPage({super.key}); @override State<VideoChatPage> createState() => _VideoChatPageState(); } class _VideoChatPageState extends State<VideoChatPage> { // 预览容器尺寸,可根据需求调整 final double _previewWidth = 120; final double _previewHeight = 160; // 预览容器当前偏移量 late double _left; late double _top; @override void didChangeDependencies() { super.didChangeDependencies(); // 初始位置设为右下角 final screenSize = MediaQuery.of(context).size; _left = screenSize.width - _previewWidth - 16; // 右边距16 _top = screenSize.height - _previewHeight - kToolbarHeight - 16; // 下边距16,减去AppBar高度 } // 计算最近的停靠角落 Offset _getNearestCorner(Offset releasePosition, Size screenSize) { // 四个角落的目标偏移 final corners = [ Offset(16, 16 + kToolbarHeight), // 左上 Offset(screenSize.width - _previewWidth - 16, 16 + kToolbarHeight), // 右上 Offset(16, screenSize.height - _previewHeight - 16 - kToolbarHeight), // 左下 Offset(screenSize.width - _previewWidth - 16, screenSize.height - _previewHeight - 16 - kToolbarHeight), // 右下 ]; // 计算和每个角落的距离,取最小的 Offset nearest = corners.last; double minDistance = double.infinity; for (var corner in corners) { final distance = (releasePosition - corner).distanceSquared; if (distance < minDistance) { minDistance = distance; nearest = corner; } } return nearest; } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; return Scaffold( appBar: AppBar(title: const Text('视频聊天页面')), body: Stack( children: [ // 这里放你的主视频流、聊天消息等其他页面内容 Container(color: Colors.grey[200], child: const Center(child: Text('主视频区域'))), // 可拖拽的摄像头预览容器 Positioned( left: _left, top: _top, child: Draggable( // 拖拽时显示的半透明预览 feedback: Opacity( opacity: 0.7, child: Container( width: _previewWidth, height: _previewHeight, color: Colors.black, child: const Center(child: Text('摄像头预览', style: TextStyle(color: Colors.white))), ), ), // 拖拽时原位置显示空组件 childWhenDragging: const SizedBox.shrink(), // 拖拽结束回调 onDraggableCanceled: (velocity, offset) { // 校正坐标,避免容器拖出屏幕外 final releaseX = offset.dx.clamp(0.0, screenSize.width - _previewWidth); final releaseY = offset.dy.clamp(0.0, screenSize.height - _previewHeight - kToolbarHeight); final releasePosition = Offset(releaseX, releaseY); // 计算最近停靠位置 final targetPosition = _getNearestCorner(releasePosition, screenSize); setState(() { _left = targetPosition.dx; _top = targetPosition.dy; }); }, // 实际显示的预览容器 child: Container( width: _previewWidth, height: _previewHeight, color: Colors.black, child: const Center(child: Text('摄像头预览', style: TextStyle(color: Colors.white))), // 你可以把上面的child替换成 CameraPreview(yourCameraController) 接入真实摄像头画面 ), ), ) ], ), ); } }
关键逻辑说明
- 如果你希望用
DragTarget实现停靠判定的话,可以在Stack的四个角落分别放置透明的DragTarget,拖拽释放时判定落入哪个DragTarget就停靠到对应位置,逻辑和上面的自动计算是一致的,本示例采用的是更轻量的无DragTarget实现方案 - 如果不需要强制停靠四个角落,只要自由拖拽的话,直接把
onDraggableCanceled里的_getNearestCorner逻辑去掉,直接把releaseX和releaseY赋值给_left和_top即可 - 代码里的16是容器和屏幕边缘的边距,可以根据你的设计需求调整
- 如果页面没有AppBar,把所有和
kToolbarHeight相关的计算去掉即可 - 要适配横竖屏的话,只需要在
didChangeDependencies里监听屏幕尺寸变化,重新校正当前预览容器的位置即可
内容的提问来源于stack exchange,提问作者Sowmyan Soman
相关产品推荐
相关产品推荐

