1. 前言

在移动应用开发中,悬浮可拖拽组件是一种常见的交互模式,它可以让用户自由调整关键功能按钮的位置,既保持了功能的可访问性,又不会长期占用屏幕空间。Flutter 生态中有一个名为 draggable_float_widget 的第三方库,专门简化了这类组件的开发。本文将详细介绍如何使用该库快速实现高质量的悬浮可拖拽组件。

2. 介绍

draggable_float_widget 是一个 Flutter 插件,它提供了开箱即用的悬浮可拖拽组件功能,具有以下特点:

  • 支持自由拖拽并自动吸附到屏幕边缘
  • 可自定义拖拽区域、大小和样式
  • 支持设置拖拽边界和禁区
  • 提供拖拽状态回调
  • 轻量级实现,性能优异
  • 支持 Android 和 iOS 双平台

3. 快速开始

首先,在 pubspec.yaml 文件中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  draggable_float_widget: ^1.0.3  # 请使用最新版本

然后运行命令安装依赖:

flutter pub get

3.1. 最简单的示例

下面是一个基本用法示例,创建一个可拖拽的悬浮按钮:

import 'package:flutter/material.dart';
import 'package:draggable_float_widget/draggable_float_widget.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Draggable Widget Demo')),
        body: const Center(child: Text('主页面内容')),
        // 添加悬浮可拖拽组件
        floatingActionButton: DraggableFloatWidget(
          // 拖拽组件的子Widget
          child: Container(
            width: 60,
            height: 60,
            decoration: const BoxDecoration(
              color: Colors.blue,
              shape: BoxShape.circle,
            ),
            child: const Icon(Icons.add, color: Colors.white, size: 30),
          ),
          // 拖拽结束后的回调
          onDragEnd: (Offset offset) {
            debugPrint('拖拽结束,位置:$offset');
          },
        ),
      ),
    );
  }
}

这个简单的示例就实现了一个可以在屏幕上自由拖拽的蓝色圆形按钮,松开后会自动吸附到最近的屏幕边缘。

4. 核心配置详解

DraggableFloatWidget 提供了丰富的配置选项,让我们可以根据需求定制组件行为:

4.1. 位置与边界设置

DraggableFloatWidget(
  // 初始位置(相对于屏幕左上角)
  initialOffset: const Offset(300, 500),
  // 是否允许越界
  canCrossBoundary: false,
  // 边界内边距(限制拖拽范围)
  boundaryPadding: const EdgeInsets.all(10),
  // 吸附到边缘的距离阈值
 吸附距离
 吸附到边缘的动画时长
  child: ...,
)

4.2. 拖拽行为控制

DraggableFloatWidget(
  // 是否允许拖拽
  canDrag: true,
  // 拖拽时的透明度
  dragOpacity: 0.8,
  // 拖拽时的缩放比例
  dragScale: 1.1,
  // 只有特定区域可触发拖拽(默认为整个组件)
  dragAreaBuilder: (child) {
    return Stack(
      children: [
        child,
        // 只有右上角区域可拖拽
        Positioned(
          top: 0,
          right: 0,
          width: 20,
          height: 20,
          child: Container(color: Colors.red.withOpacity(0.3)),
        ),
      ],
    );
  },
  child: ...,
)

4.3. 回调函数

DraggableFloatWidget(
  // 开始拖拽时回调
  onDragStart: () {
    debugPrint('开始拖拽');
  },
  // 拖拽过程中回调
  onDragging: (Offset offset) {
    debugPrint('拖拽中,当前位置:$offset');
  },
  // 结束拖拽时回调
  onDragEnd: (Offset offset) {
    debugPrint('结束拖拽,最终位置:$offset');
    // 可以在这里保存位置,以便下次打开时恢复
  },
  child: ...,
)

5. 实际应用场景

下面是一些实际应用场景:

5.1. 悬浮操作按钮

实现一个多功能悬浮按钮,点击展开更多操作选项:

class FloatingActionMenu extends StatefulWidget {
  const FloatingActionMenu({super.key});

  
  State<FloatingActionMenu> createState() => _FloatingActionMenuState();
}

class _FloatingActionMenuState extends State<FloatingActionMenu> 
    with SingleTickerProviderStateMixin {
  bool _isExpanded = false;
  late AnimationController _controller;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _toggleMenu() {
    setState(() => _isExpanded = !_isExpanded);
    _isExpanded ? _controller.forward() : _controller.reverse();
  }

  
  Widget build(BuildContext context) {
    return DraggableFloatWidget(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          // 子菜单按钮(根据展开状态动画显示)
          if (_isExpanded)
            ScaleTransition(
              scale: Tween(begin: 0.0, end: 1.0).animate(_controller),
              child: _buildActionButton(
                icon: Icons.share,
                color: Colors.green,
                onTap: () { /* 分享功能 */ },
              ),
            ),
          if (_isExpanded)
            const SizedBox(height: 10),
          if (_isExpanded)
            ScaleTransition(
              scale: Tween(begin: 0.0, end: 1.0).animate(
                CurvedAnimation(
                  parent: _controller,
                  curve: const Interval(0.2, 1.0),
                ),
              ),
              child: _buildActionButton(
                icon: Icons.favorite,
                color: Colors.red,
                onTap: () { /* 收藏功能 */ },
              ),
            ),
          const SizedBox(height: 10),
          // 主按钮
          FloatingActionButton(
            onPressed: _toggleMenu,
            child: AnimatedIcon(
              icon: AnimatedIcons.menu_close,
              progress: _controller,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildActionButton({
    required IconData icon,
    required Color color,
    required VoidCallback onTap,
  }) {
    return Container(
      width: 50,
      height: 50,
      decoration: BoxDecoration(
        color: color,
        shape: BoxShape.circle,
      ),
      child: IconButton(
        icon: Icon(icon, color: Colors.white),
        onPressed: onTap,
      ),
    );
  }
}

5.2. 视频悬浮窗

实现一个可以拖拽的小视频窗口,支持在应用内任意拖动:

DraggableFloatWidget(
  // 初始位置在右下角
  initialOffset: Offset(
    MediaQuery.of(context).size.width - 120,
    MediaQuery.of(context).size.height - 200,
  ),
  // 拖拽时稍微缩小
  dragScale: 0.95,
  // 设置边界,避免完全移出屏幕
  boundaryPadding: const EdgeInsets.all(10),
  child: Container(
    width: 100,
    height: 180,
    decoration: BoxDecoration(
      color: Colors.black,
      borderRadius: BorderRadius.circular(8),
      boxShadow: const [
        BoxShadow(
          color: Colors.black26,
          blurRadius: 10,
          spreadRadius: 2,
        )
      ],
    ),
    child: Stack(
      children: [
        // 视频播放器
        const Center(child: CircularProgressIndicator(color: Colors.white)),
        // 关闭按钮
        Positioned(
          top: 5,
          right: 5,
          child: GestureDetector(
            onTap: () {
              // 关闭悬浮窗逻辑
            },
            child: Container(
              width: 20,
              height: 20,
              decoration: const BoxDecoration(
                color: Colors.black54,
                shape: BoxShape.circle,
              ),
              child: const Icon(Icons.close, color: Colors.white, size: 14),
            ),
          ),
        ),
      ],
    ),
  ),
)

5.3. 保存和恢复位置

实现记住用户上次拖拽的位置,下次打开应用时恢复:

class PersistentDraggableWidget extends StatefulWidget {
  const PersistentDraggableWidget({super.key});

  
  State<PersistentDraggableWidget> createState() => 
      _PersistentDraggableWidgetState();
}

class _PersistentDraggableWidgetState extends State<PersistentDraggableWidget> {
  Offset? _savedOffset;

  
  void initState() {
    super.initState();
    // 从存储中加载上次保存的位置
    _loadSavedPosition();
  }

  // 从SharedPreferences加载位置
  Future<void> _loadSavedPosition() async {
    final prefs = await SharedPreferences.getInstance();
    final x = prefs.getDouble('float_x');
    final y = prefs.getDouble('float_y');
    if (x != null && y != null) {
      setState(() {
        _savedOffset = Offset(x, y);
      });
    }
  }

  // 保存位置到SharedPreferences
  Future<void> _savePosition(Offset offset) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setDouble('float_x', offset.dx);
    await prefs.setDouble('float_y', offset.dy);
  }

  
  Widget build(BuildContext context) {
    return DraggableFloatWidget(
      initialOffset: _savedOffset ?? const Offset(300, 500),
      onDragEnd: (offset) {
        _savePosition(offset);
      },
      child: const Icon(Icons.access_time, size: 40, color: Colors.orange),
    );
  }
}

6. 高级技巧与最佳实践

6.1. 处理屏幕旋转

当屏幕旋转时,保持悬浮组件的相对位置:

DraggableFloatWidget(
  // 使用百分比位置而非绝对位置
  initialOffset: Offset(
    MediaQuery.of(context).size.width * 0.8,
    MediaQuery.of(context).size.height * 0.6,
  ),
  // 监听屏幕尺寸变化,调整位置
  onLayoutChanged: (oldSize, newSize, currentOffset) {
    // 计算相对位置比例
    final double xRatio = currentOffset.dx / oldSize.width;
    final double yRatio = currentOffset.dy / oldSize.height;
    
    // 返回新的位置
    return Offset(newSize.width * xRatio, newSize.height * yRatio);
  },
  child: ...,
)

6.2. 避免遮挡关键内容

在某些页面,可能需要临时隐藏悬浮组件或调整其位置:

class SmartDraggableWidget extends StatelessWidget {
  final bool isImportantContentVisible;

  const SmartDraggableWidget({
    super.key,
    required this.isImportantContentVisible,
  });

  
  Widget build(BuildContext context) {
    return DraggableFloatWidget(
      // 当重要内容可见时,调整位置或隐藏
      child: isImportantContentVisible
          ? Container(
              width: 40,
              height: 40,
              decoration: const BoxDecoration(
                color: Colors.grey,
                shape: BoxShape.circle,
              ),
              child: const Icon(Icons.arrow_up, color: Colors.white),
            )
          : const SizedBox.shrink(), // 隐藏组件
    );
  }
}

6.3. 拖拽禁区设置

某些区域不允许悬浮组件进入(如底部导航栏):

DraggableFloatWidget(
  // 自定义位置验证
  positionValidator: (offset) {
    final screenHeight = MediaQuery.of(context).size.height;
    // 底部60像素为禁区(假设导航栏高度为60)
    if (offset.dy > screenHeight - 60) {
      // 返回调整后的位置
      return Offset(offset.dx, screenHeight - 70);
    }
    return offset;
  },
  child: ...,
)

7. 常见问题

下面是一些常见问题:

7.1. 组件层级问题

如果悬浮组件被其他组件遮挡,可以使用 Stack 提升层级:

Stack(
  children: [
    // 主内容
    const MainContent(),
    // 悬浮组件放在Stack的最后,确保在最上层
    const DraggableFloatWidget(
      child: ...,
    ),
  ],
)

7.2. 列表滚动时的冲突

在可滚动列表上使用时,可能会出现拖拽与滚动的冲突:

DraggableFloatWidget(
  // 增加拖拽触发的敏感度
  dragSensitivity: 1.5,
  // 延迟拖拽开始,避免误触
  dragDelay: const Duration(milliseconds: 100),
  child: ...,
)

7.3. 性能优化

对于复杂的悬浮组件,确保做好性能优化:

DraggableFloatWidget(
  // 使用RepaintBoundary避免不必要的重绘
  child: RepaintBoundary(
    child: MyComplexWidget(),
  ),
)

想要了解更多关于 draggable_float_widget 的信息,可以查看其 官方文档 获取最新的 API 详情和更新日志。


本次分享就到这儿啦,我是鹏多多,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

往期文章

个人主页

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐