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

发布于:2023-09-16 ⋅ 阅读:(192) ⋅ 点赞:(0)

一、先来看下实现的效果

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

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

  • 通过showModalBottomSheet显示底部对话框
showModalBottomSheet(
  context: context,
  isScrollControlled: true,
  backgroundColor: Colors.white,
  transitionAnimationController: _controller,
 
  builder: (_) {
    ///省略部分代码...
  },
);

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;

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、在向下滑动到顶,继续向下的时候,动态改变弹窗内部的高度来达到弹窗下拉的效果,这里本来是想通过改变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,
                ),
              ),
            ),
          ],
        ),
      );
    },
  );
}

2、解决原理:

  • 使用Listener包裹底部可滚动组件,然后监听用户的滑动,当滑动到了最顶部且继续向下滑动就将SingleChildScrollViewphysics设置为不可滚动
  • 同时改变内容的高度,同时也要改变transitionAnimationController.value的值这样内容区才会跟着移动,缩放
  • 最后在触摸结束的时候进行判断是需要收起弹窗还是关闭弹窗
本文含有隐藏内容,请 开通VIP 后查看