• Flutter 通过BottomSheetDialog实现抖音打开评论区,内容自动上推、缩放效果


    一、先来看下实现的效果

    • 实现上面的效果需要解决俩个问题
      • 当列表进行向下滑动到顶部的时候,继续滑动可以让弹窗向下收起来
      • 弹出上下拖动的时候,视图内容跟着上下移动、缩放大小

    二、实现弹窗上下滑动的时候,动态改变内容区的位置和大小

    • 通过showModalBottomSheet显示底部对话框
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.white,
      transitionAnimationController: _controller,
     
      builder: (_) {
        ///省略部分代码...
      },
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1、那问题来了,怎么去监听对话框当前显示的高度呢?

    可以发现showModalBottomSheet有一个transitionAnimationController参数,这个就是对话框显示的动画控制器了值为[0,1],当全部显示是为1。
    那么当将弹窗设为固定高度时,就可以通过这个值进行计算了

    • 假设我们顶部留的最小空间为:宽度 = 屏幕宽度,高度 = 屏幕宽度 / (16 / 9),那么对话框的高度就等与 屏幕高度 - 顶部高度
    ///屏幕宽度
    double get screenWidth => MediaQuery.of(context).size.width;
    ///屏幕高度
    double get screenHeight => MediaQuery.of(context).size.height;
    ///顶部留的高度
    double get topSpaceHeight => screenWidth / (16 / 9);
    ///对话框高度
    double get bottomSheetHeight => screenHeight - topSpaceHeight;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    2、监听对话框高度改变
    
    void initState() {
      super.initState();
      _controller = BottomSheet.createAnimationController(this);
      _controller.addListener(() {
        final value = _controller.value * bottomSheetHeight;
        ///更新UI
        _bottomSheetController.sink.add(value);
      });
    }
    
    
    Widget build(BuildContext context) {
      final bottom = MediaQuery.of(context).padding.bottom;
      return ColoredBox(
        color: Colors.black,
        child: Stack(
          children: [
            StreamBuilder<double>(
              stream: _bottomSheetController.stream,
              initialData: 0,
              builder: (_, snapshot) {
                return Container(
                  height: screenHeight - snapshot.data!,
                  alignment: Alignment.center,
                  child: Image.network(
                    'https://5b0988e595225.cdn.sohucs.com/images/20200112/75b4a498fdaa48c7813419c2d4bac477.jpeg',
                  ),
                );
              },
            ),
          ],
        ),
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    通过上面这样处理,内容区的上移和缩小就已经实现了

    三、弹窗内容向下滑动,当滑动到顶继续向下滑动时,可以让对话框继续向下滑动(不打断此次触摸事件)

    1、在向下滑动到顶,继续向下的时候,动态改变弹窗内部的高度来达到弹窗下拉的效果,这里本来是想通过改变transitionAnimationController.value的值来改变弹窗的高度,但是实际中发现或的效果不理想,不知道为什么
    
    Widget build(BuildContext context) {
      return StreamBuilder<double>(
        stream: _dragController.stream,
        initialData: widget.height,
        builder: (context, snapshot) {
          return AnimatedContainer(
            height: snapshot.data ?? widget.height,
            duration: const Duration(milliseconds: 50),
            child: Column(
              children: [
                widget.pinedHeader ?? const SizedBox.shrink(),
                Expanded(
                  child: Listener(
                    onPointerMove: (event) {
                      ///没有滚动到顶部不处理
                      if (_scrollController.offset != 0) {
                        return;
                      }
                      ///获取滑动到顶部开始下拉的位置
                      _startY ??= event.position.dy;
                      final distance = event.position.dy - _startY!;
                      ///弹窗滑动后剩余高度
                      if ((widget.height - distance) > widget.height) {
                        return;
                      }
                      _dragController.sink.add(widget.height - distance);
                      ///剩余弹出高度所占百分比
                      final percent = 1 - distance / widget.height;
                      ///为了处理图片大小缩放需要使用
                      widget.transitionAnimationController.value = percent;
                    },
                    /// 触摸事件结束 恢复可滚动
                    onPointerUp: (event) {
                      _startY = null;
                      if (snapshot.data! <= widget.height * 0.5) {
                        ///下拉到了一半直接关闭
                        widget.transitionAnimationController.animateTo(0,
                            duration: const Duration(microseconds: 250));
                      } else {
                        ///未到一半 恢复展示
                        _dragController.sink.add(widget.height);
                        widget.transitionAnimationController.animateTo(1,
                            duration: const Duration(microseconds: 250));
                      }
                    },
                    child: SingleChildScrollView(
                      controller: _scrollController,
                      physics: snapshot.data == widget.height
                          ? const ClampingScrollPhysics()
                          : const NeverScrollableScrollPhysics(),
                      child: widget.child,
                    ),
                  ),
                ),
              ],
            ),
          );
        },
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    2、解决原理:
    • 使用Listener包裹底部可滚动组件,然后监听用户的滑动,当滑动到了最顶部且继续向下滑动就将SingleChildScrollViewphysics设置为不可滚动
    • 同时改变内容的高度,同时也要改变transitionAnimationController.value的值这样内容区才会跟着移动,缩放
    • 最后在触摸结束的时候进行判断是需要收起弹窗还是关闭弹窗
  • 相关阅读:
    组件封装 - 按钮组件和计数组件
    JVM相关
    【DOCKER】显示带UI的软件
    巡检机器人智能联网,促进工厂自动化
    Vue 官方文档2.x教程学习笔记 1 基础 1.6 Class 与 Style 绑定 1.6.2 绑定内联样式
    [开源福利] FreeRedis 历时两年正式发布 v1.0 [C#.NET Redis Client]
    量化投资 离散时间随机过程
    Nuxt - [高效简洁] 路由不存在时,跳转到自己定制的 404.vue 页面(当路由不存在时自动重定向到自定义的 404 组件)路由配置的解决方案
    FPGA面试笔试一些基础概念题目2
    Linux之bash常用命令
  • 原文地址:https://blog.csdn.net/a_zhon/article/details/132761775