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

相关推荐
瓜子三百克4 小时前
七、性能优化
flutter·性能优化
雨白8 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹10 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空11 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭12 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日13 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安13 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑13 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟17 小时前
CTF Web的数组巧用
android
点金石游戏出海18 小时前
每周资讯 | Krafton斥资750亿日元收购日本动画公司ADK;《崩坏:星穹铁道》新版本首日登顶iOS畅销榜
游戏·ios·业界资讯·apple·崩坏星穹铁道