Flutter bottomSheet和ListView滑动冲突?无侵入不到80行代码解决

1,449 阅读1分钟

Screenshot_2024-08-22-11-01-51-576_com.example.flutter_listview_demo.jpg 需求如图。在bottomSheet中显示一个ListView,并且需要向下滑至顶部的时候关闭该弹窗。

问题:默认情况下在滑动到顶部后就会停止滑动,bottomSheet无法继续滑动关闭。

翻阅相关源码发现bottomSheet的滑动关闭逻辑在BottomSheet这个类中,内部添加了个手势识别器,看看_handleDrag都干了啥

image.png

很明显通过滑动手势操作了AnimationController来实现的下拉动画。解题思路就是操作AnimationController来解决手势冲突导致的无法滑动关闭。

image.png 继续看如何获得AnimationController

image.png transitionAnimationController 这不是可以从外部传吗??okok,封装一个类,把BottomSheet里的update和end方法对animationController的逻辑copy出来,尝试下。

import 'package:flutter/material.dart';

const double _minFlingVelocity = 700.0;
const double _closeProgressThreshold = 0.5;

class DragBottomSheet extends StatelessWidget {
  final AnimationController animationController;
  final GlobalKey _childKey = GlobalKey(debugLabel: 'DragBottomSheet child');
  final VoidCallback onClosing;
  final Widget child;

  double get _childHeight {
    final RenderBox renderBox =
        _childKey.currentContext!.findRenderObject()! as RenderBox;
    return renderBox.size.height;
  }

  DragBottomSheet({
    super.key,
    required this.animationController,
    required this.onClosing,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    return NotificationListener<ScrollNotification>(
        key: _childKey,
        onNotification: (ScrollNotification notification) {
          _handleScroller(notification);
          return true;
        },
        child: child);
  }

  void _handleScroller(ScrollNotification notification) {
    if (notification is OverscrollNotification) {
      if (notification.dragDetails != null) {
        _handleDragUpdate(notification.dragDetails!);
      }
    }
    if (notification is ScrollEndNotification) {
      if (notification.dragDetails != null) {
        _handleDragEnd(notification.dragDetails!);
      }
    }
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    animationController.value -= details.primaryDelta! / _childHeight;
  }

  void _handleDragEnd(DragEndDetails details) {
    bool isClosing = false;
    if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
      final double flingVelocity =
          -details.velocity.pixelsPerSecond.dy / _childHeight;
      if (animationController.value > 0.0) {
        animationController.fling(velocity: flingVelocity);
      }
      if (flingVelocity < 0.0) {
        isClosing = true;
      }
    } else if (animationController.value < _closeProgressThreshold) {
      if (animationController.value > 0.0) {
        animationController.fling(velocity: -1.0);
      }
      isClosing = true;
    } else {
      animationController.forward();
    }
    if (isClosing) {
      onClosing.call();
    }
  }
}

外部调用弹窗的地方传入自己创建的AnimationController

late final AnimationController animationController = AnimationController(
    vsync: this, duration: const Duration(milliseconds: 250));

Future _showBottomSheet() async {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    // 传入controller
    transitionAnimationController: animationController,
    builder: (_) {
      return ConstrainedBox(
        constraints: const BoxConstraints(maxHeight: 650),
        // 传入controller
        child: DragBottomSheet(
          animationController: animationController,
          onClosing: () {
            Navigator.of(context).pop();
          },
          child: ListView(
            children: [
              for (int i = 0; i < 200; i++)
                Container(
                  alignment: Alignment.center,
                  height: 50,
                  child: Text("第$i项"),
                ),
            ],
          ),
        ),
      );
    },
  );
}

看看最终效果,搞定!下课!

Screenrecorder-2024- -middle-original.gif