如何在 Flutter 中实现可拖动的底部弹出框

在 Flutter 开发中,底部弹出框(Bottom Sheet)是一种常见的 UI 组件,通常用于显示一些额外的操作选项或详细信息。在这篇文章中,我将介绍一个自定义的 DragBottomSheetWidget 组件,它不仅支持手势拖动关闭,还可以通过动画进行弹出和收起。

组件功能概述

DragBottomSheetWidget 是一个支持手势拖动和动画效果的底部弹出框组件。它具有以下几个主要功能:

  1. 手势下拉关闭:用户可以通过向下拖动来关闭底部弹出框。
  2. 动画弹出收起:支持平滑的动画效果,弹出或收起时更加自然。
  3. 弹出后无法关闭:在特定场景下,弹出框可以设置为无法通过手势关闭。
代码解析

首先,我们来看一下 DragBottomSheetWidget 的代码实现:

dart 复制代码
import 'package:flutter/material.dart';

class DragBottomSheetWidget extends StatefulWidget {
  const DragBottomSheetWidget({
    super.key,
    required this.builder,
    this.duration = const Duration(milliseconds: 200),
    this.childHeightRatio = 0.8,
    this.onStateChange,
  });

  final Function(bool)? onStateChange;
  final double childHeightRatio;
  final Duration duration;
  final ScrollableWidgetBuilder builder;

  @override
  State<DragBottomSheetWidget> createState() => DragBottomSheetWidgetState();
}

class DragBottomSheetWidgetState extends State<DragBottomSheetWidget> {
  final DraggableScrollableController controller =
      DraggableScrollableController();
  final ValueNotifier<bool> isExpandNotifier = ValueNotifier<bool>(false);

  double verticalDistance = 0;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    controller.dispose();
    isExpandNotifier.dispose();
    super.dispose();
  }

  Future<void> show({bool isCanClose = true}) async {
    try {
      await controller.animateTo(
        1,
        duration: widget.duration,
        curve: Curves.linear,
      );

      if (!isCanClose) {
        isExpandNotifier.value = true;
      }
    } catch (e) {
      print('Error animating to full size: $e');
    }
  }

  void hide() {
    controller.animateTo(
      0,
      duration: widget.duration,
      curve: Curves.linear,
    );
  }

  void _dragJumpTo(double y) {
    final size = y / MediaQuery.sizeOf(context).height;
    final jumpToValue = widget.childHeightRatio - size;
    controller.jumpTo(jumpToValue.clamp(0, widget.childHeightRatio));
  }

  void _dragEndChange(DragEndDetails dragEndDetails) {
    if (controller.size >= widget.childHeightRatio / 2) {
      controller.animateTo(
        1,
        duration: widget.duration,
        curve: Curves.linear,
      );
    } else {
      hide();
    }
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener<DraggableScrollableNotification>(
      onNotification: (notification) {
        isExpandNotifier.value = notification.extent == widget.childHeightRatio;
        widget.onStateChange?.call(isExpandNotifier.value);
        return true;
      },
      child: ValueListenableBuilder<bool>(
        valueListenable: isExpandNotifier,
        builder: (context, isExpand, child) {
          return DraggableScrollableSheet(
            initialChildSize: !isExpand ? 0 : widget.childHeightRatio,
            minChildSize: 0,
            maxChildSize: widget.childHeightRatio,
            expand: true,
            snap: true,
            controller: controller,
            builder: (BuildContext context, ScrollController scrollController) {
              return _DragHandler(
                onDragDown: () => verticalDistance = 0,
                onDragUpdate: (details) {
                  verticalDistance += details.delta.dy;
                  _dragJumpTo(verticalDistance);
                },
                onDragEnd: _dragEndChange,
                child: widget.builder(context, scrollController),
              );
            },
          );
        },
      ),
    );
  }
}

class _DragHandler extends StatelessWidget {
  const _DragHandler({
    required this.onDragDown,
    required this.onDragUpdate,
    required this.onDragEnd,
    required this.child,
  });

  final VoidCallback onDragDown;
  final GestureDragUpdateCallback onDragUpdate;
  final GestureDragEndCallback onDragEnd;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.translucent,
      onVerticalDragDown: (_) => onDragDown(),
      onVerticalDragUpdate: onDragUpdate,
      onVerticalDragEnd: onDragEnd,
      child: child,
    );
  }
}
核心功能解读
  1. 控制器与状态管理 :组件内部使用了 DraggableScrollableController 来控制弹出框的显示状态,ValueNotifier<bool> 用于监听弹出框的展开与收起状态。

  2. 显示与隐藏show 方法通过 animateTo 将弹出框平滑展开至全屏,而 hide 方法则将其收起至不可见状态。

  3. 手势控制 :通过 _dragJumpTo 方法和 _dragEndChange 方法,组件可以响应用户的手势操作,决定弹出框的滑动与状态变化。

  4. Builder 模式 :通过 builder 回调,开发者可以自定义弹出框中的内容,满足不同场景的需求。

使用场景

DragBottomSheetWidget 适用于需要在屏幕底部弹出一些交互式内容的场景,如表单输入、操作选项等。通过手势控制和动画效果,可以为用户提供更加流畅和直观的操作体验。

结语

自定义 DragBottomSheetWidget 组件不仅增强了 Flutter 的弹出框功能,还为用户提供了更多的交互可能性。如果你正在开发需要底部弹出框的应用,不妨尝试一下这个组件,为你的应用添加更丰富的交互体验。

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
拭心3 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王6 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
coder_pig6 小时前
📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK
flutter·ubuntu·jenkins
梦想平凡6 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道6 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库7 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道8 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
捡芝麻丢西瓜8 小时前
flutter自学笔记5- dart 编码规范
flutter·dart
MuYe8 小时前
Android Hook - 动态加载so库
android