You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.15 10:45:05