Flutter-底部弹出框(Widget层级)

简介: 文章描述了如何在Flutter中使用DraggableScrollableSheet创建一个底部弹出框,同时保持其可手势滑动关闭。作者遇到问题并提出对原控件进行扩展,以支持头部和列表布局的滑动关闭功能。
需求
  • 支持底部弹出对话框。
  • 支持手势滑动关闭。
  • 支持在widget中嵌入引用。
  • 支持底部弹出框弹出后不影响其他操作。
  • 支持弹出框中内容固定头部和下面列表时,支持触摸头部并在列表不在头部的时候支持滑动关闭
简述

通过上面的需求可知,就是在界面中可以支持底部弹出一个弹出框,但是又不影响除了这个弹出框外其他操作,同时支持手势滑动关闭。想到这里我们其实会想到一个控件DraggableScrollableSheet,Flutter提供一种可以支持在widget中引入的底部弹出布局,同时不影响其他的操作,所以我就使用这个控件测试了下,发现一个问题,就是当底部弹出框内容为上面有个头部,底部是个列表时,每次都需要列表滑动到顶部的时候才可以手势滑动关闭,所以需要支持触摸顶部布局也支持手势滑动关闭,所以在此控件上做一个修改。

效果

代码如下
`import 'package:flutter/material.dart';

/// 底部弹出Widget
/// 1、支持手势下拉关闭
/// 2、支持动画弹出收起
/// 3、支持弹出无法关闭
class DragBottomSheetWidget extends StatefulWidget {
  DragBottomSheetWidget({
    required Key key,
    required this.builder,
    this.duration,
    this.childHeightRatio = 0.8,
    this.onStateChange,
  }) : super(key: key);

  Function(bool)? onStateChange;

  final double childHeightRatio;

  final Duration? duration;

  final ScrollableWidgetBuilder builder;

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

class DragBottomSheetWidgetState extends State<DragBottomSheetWidget>
    with TickerProviderStateMixin {
  final controller = DraggableScrollableController();

  //是否可以关闭
  bool isCanClose = true;

  //是否展开
  bool isExpand = false;

  double verticalDistance = 0;

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

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

  Future show({bool isCanClose = true}) {
    return controller
        .animateTo(1,
            duration: widget.duration ?? const Duration(milliseconds: 200),
            curve: Curves.linear)
        .then((value) {
      if (!isCanClose) {
        setState(() {
          this.isCanClose = isCanClose;
        });
      }
    });
  }

  void hide() {
    controller.animateTo(
      0,
      duration: widget.duration ?? const Duration(milliseconds: 200),
      curve: Curves.linear,
    );
  }

  void _dragJumpTo(double y) {
    var size = y / MediaQuery.of(context).size.height;
    controller.jumpTo(widget.childHeightRatio - size);
  }

  void _dragEndChange() {
    controller.size >= widget.childHeightRatio / 2
        ? controller.animateTo(
            1,
            duration: widget.duration ?? const Duration(milliseconds: 200),
            curve: Curves.linear,
          )
        : hide();
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener<DraggableScrollableNotification>(
      onNotification: (notification) {
        if (notification.extent == widget.childHeightRatio) {
          if (!isExpand) {
            isExpand = true;
            widget.onStateChange?.call(true);
          }
        } else if (notification.extent < 0.00001) {
          if (isExpand) {
            isExpand = false;
            widget.onStateChange?.call(false);
          }
        }
        return true;
      },
      child: DraggableScrollableSheet(
        initialChildSize: isCanClose && !isExpand ? 0 : widget.childHeightRatio,
        minChildSize: isCanClose ? 0 : widget.childHeightRatio,
        maxChildSize: widget.childHeightRatio,
        expand: true,
        snap: true,
        controller: controller,
        builder: (BuildContext context, ScrollController scrollController) {
          return GestureDetector(
            behavior: HitTestBehavior.translucent,
            onVerticalDragDown: (details) {
              verticalDistance = 0;
            },
            onVerticalDragUpdate: (details) {
              verticalDistance += details.delta.dy;
              _dragJumpTo(verticalDistance);
            },
            onVerticalDragEnd: (details) {
              _dragEndChange();
            },
            child: widget.builder.call(context, scrollController),
          );
        },
      ),
    );
  }
}

使用

import 'package:flutter/material.dart';
import 'package:flutter_xy/widgets/xy_app_bar.dart';
import 'package:flutter_xy/xydemo/darg/drag_bottom_sheet_widget.dart';

class DragBottomSheetPage extends StatefulWidget {
  const DragBottomSheetPage({super.key});

  @override
  State<DragBottomSheetPage> createState() => _DragBottomSheetPageState();
}

class _DragBottomSheetPageState extends State<DragBottomSheetPage> {
  final GlobalKey<DragBottomSheetWidgetState> bottomSheetKey =
      GlobalKey<DragBottomSheetWidgetState>();

  bool isExpand = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.deepPurple.withAlpha(50),
      appBar: XYAppBar(
        title: "底部弹出布局",
        backgroundColor: Colors.transparent,
        titleColor: Colors.white,
        onBack: () {
          Navigator.pop(context);
        },
      ),
      body: Stack(
        children: [
          Positioned(
              top: 0,
              child: Column(
                mainAxisSize: MainAxisSize.max,
                children: [
                  InkWell(
                    onTap: () {
                      if (isExpand) {
                        bottomSheetKey.currentState?.hide();
                      } else {
                        bottomSheetKey.currentState?.show();
                      }
                    },
                    child: Container(
                      width: MediaQuery.of(context).size.width - 60,
                      alignment: Alignment.center,
                      height: 50,
                      margin: const EdgeInsets.symmetric(horizontal: 30),
                      decoration: BoxDecoration(
                        borderRadius: BorderRadius.circular(32),
                        color: Colors.deepPurple,
                      ),
                      child: Text(
                        isExpand ? "收起" : "弹出",
                        style: const TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.w600,
                          color: Colors.white,
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 20),
                ],
              )),
          Positioned(
            child: DragBottomSheetWidget(
              key: bottomSheetKey,
              onStateChange: (isExpand) {
                setState(() {
                  this.isExpand = isExpand;
                });
              },
              builder:
                  (BuildContext context, ScrollController scrollController) {
                return Container(
                  alignment: Alignment.topLeft,
                  child: Stack(
                    children: [
                      Container(
                        height: 50,
                        decoration: const BoxDecoration(
                          borderRadius: BorderRadius.only(
                              topLeft: Radius.circular(16),
                              topRight: Radius.circular(16)),
                          color: Colors.yellow,
                        ),
                      ),
                      Expanded(
                        child: Container(
                          margin: const EdgeInsets.only(top: 50),
                          child: ListView.builder(
                            itemCount: 500,
                            controller: scrollController,
                            itemBuilder: (context, index) {
                              return index % 2 == 0
                                  ? Container(
                                      height: 100,
                                      width: MediaQuery.of(context).size.width,
                                      color: Colors.red,
                                    )
                                  : Container(
                                      height: 100,
                                      width: MediaQuery.of(context).size.width,
                                      color: Colors.blue,
                                    );
                            },
                          ),
                        ),
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

完整代码见:https://github.com/yixiaolunhui/flutter_xy

相关文章
|
2天前
|
容器
Flutter Widget 解析
Flutter Widget 解析
|
2天前
|
存储 容器
Flutter 有状态Widget 和 无状态Widget
Flutter 有状态Widget 和 无状态Widget
|
1月前
深入理解Flutter鸿蒙next版本 中的Widget继承:使用extends获取数据与父类约束
本文详细介绍了Flutter中如何通过继承其他Widget来创建自定义组件。首先解释了Widget继承的基本概念,包括StatelessWidget和StatefulWidget的区别。接着通过具体示例展示了如何继承StatelessWidget和StatefulWidget,并在子类中访问父类的build方法和状态。最后,结合多个自定义Widget展示了如何在实际应用中灵活使用继承和组合来构建复杂的UI。
76 8
|
1月前
|
容器
flutter&鸿蒙next 使用 InheritedWidget 实现跨 Widget 传递状态
在 Flutter 中,状态管理至关重要。本文详细介绍了如何使用 InheritedWidget 实现跨 Widget 的状态传递。InheritedWidget 允许数据在 Widget 树中向下传递,适用于多层嵌套的场景。通过一个简单的计数器示例,展示了如何创建和使用 InheritedWidget,包括其基础概念、工作原理及代码实现。虽然 InheritedWidget 较底层,但它是许多高级状态管理解决方案的基础。
103 2
|
2月前
|
容器
flutter:第一个flutter&Widget的使用 (二)
本文介绍了Flutter框架下的基本组件及其用法,包括简单的 Stateless Widget 如文本和按钮,以及更复杂的 StatefulWidget 示例。详细解释了如何使用 `context` 获取祖先小部件的信息,并展示了 `MaterialApp` 的属性及用途。此外,还探讨了 `StatefulWidget` 与 `StatelessWidget` 的区别,以及 `AppBar` 的常见属性配置方法。适合Flutter初学者参考学习。
|
1月前
|
Dart JavaScript 前端开发
Flutter 的 Widget 概述与常用 Widgets 与鸿蒙 Next 的对比
Flutter 是 Google 开发的开源 UI 框架,用于快速构建高性能的移动、Web 和桌面应用。Flutter 通过 Widget 构建 UI,每个 UI 元素都是 Widget,包括文本、按钮、图片等。Widget 不仅描述外观,还描述行为,是不可变的。常见的 Widget 包括结构型(Container、Column、Row)、呈现型(Text、Image)、交互型(ElevatedButton)和状态管理型(StatefulWidget)。Flutter 与鸿蒙 Next 在组件化架构、开发语言、布局系统、性能和跨平台支持方面各有优势
75 0
|
5月前
|
缓存
Flutter仿Boss-6.底部tab切换
Flutter仿Boss-6.底部tab切换
42 0
|
6月前
Flutter StreamBuilder 实现局部刷新 Widget
Flutter StreamBuilder 实现局部刷新 Widget
52 0
|
7月前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
7月前
|
开发框架 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
【4月更文挑战第30天】探索Flutter的自定义Widget与渲染流程。自定义Widget是实现复杂UI设计的关键,优点在于个性化设计、功能扩展和代码复用,但也面临性能优化和复杂性管理的挑战。创建步骤包括设计结构、定义Widget类、实现构建逻辑和处理交互。Flutter渲染流程涉及渲染对象树、布局、绘制和合成阶段。实践案例展示如何创建带渐变背景和阴影的自定义按钮。了解这些知识能提升应用体验并应对开发挑战。查阅官方文档以深入学习。
86 0
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程