如何在 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中 safeArea组件
flutter·safearea
祖国的好青年1 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴1 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭2 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首2 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil3 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙3 小时前
echarts,3d堆叠图
android·3d·echarts
Hello__77773 小时前
开源鸿蒙 Flutter 实战|自定义头像组件全流程实现
flutter·华为·harmonyos
李白的天不白3 小时前
如何项目发布到github上
android·vue.js
summerkissyou19873 小时前
Android-RTC、NTP 和 System Time(系统时间)
android