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

相关推荐
lqj_本人8 小时前
鸿蒙next选择 Flutter 开发跨平台应用的原因
flutter·华为·harmonyos
2401_865854889 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
帅得不敢出门10 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
lqj_本人11 小时前
Flutter&鸿蒙next 状态管理框架对比分析
flutter·华为·harmonyos
我又来搬代码了12 小时前
【Android】使用productFlavors构建多个变体
android
德育处主任13 小时前
Mac和安卓手机互传文件(ADB)
android·macos
芦半山13 小时前
Android“引用们”的底层原理
android·java
迃-幵14 小时前
力扣:225 用队列实现栈
android·javascript·leetcode
大风起兮云飞扬丶14 小时前
Android——从相机/相册获取图片
android
Rverdoser15 小时前
Android Studio 多工程公用module引用
android·ide·android studio