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

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

组件功能概述

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

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

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

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;

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

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

  double verticalDistance = 0;

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

  
  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();
    }
  }

  
  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;

  
  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 的弹出框功能,还为用户提供了更多的交互可能性。如果你正在开发需要底部弹出框的应用,不妨尝试一下这个组件,为你的应用添加更丰富的交互体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zender Han

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值