​哈喽,我是老刘

几年前分享了一篇GestureDetector嵌套ListView的文章

Flutter多控件滑动事情联动(滑动抵触处理)

因为文章中只给出了关键部位的代码,别的运用的技术也偏底层

所以许多同学私信我要完好的源码

这儿把原先的方案整理一下,别的也给出完好的代码供大家参阅

咱们先一点一点来看这个问题

滑动封闭组件

首要大家一定在各种App中见过这个滑动封闭组件

Flutter滑动抵触处理——GestureDetector嵌套ListView

便是手指向下滑动,组件跟从手指移动

手指抬起后组件滑出屏幕

咱们先来完成这个组件,然后来讨论一下假如组件中的内容时一个ListView,要怎么处理

假如这个组件的内容时固定内容,不是ListView这样的可翻滚组件

完成起来其实很简略

我这儿直接放源码

import 'package:flutter/material.dart';
class CloseOnSwipeDownWidget extends StatefulWidget {
  final Widget child;
  const CloseOnSwipeDownWidget({
    Key? key,
    required this.child,
  }) : super(key: key);
  @override
  CloseOnSwipeDownWidgetState createState() => CloseOnSwipeDownWidgetState();
}
class CloseOnSwipeDownWidgetState extends State
    with TickerProviderStateMixin {
  double yOffset = 0.0;
  double initialPosition = 0.0;
  bool isAnimatingOut = false;
  int animTime = 0;
  @override
  void initState() {
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onVerticalDragDown: (details) {
        initialPosition = details.globalPosition.dy;
      },
      onVerticalDragUpdate: (details) {
        double updatedPosition = details.globalPosition.dy;
        double deltaY = updatedPosition - initialPosition;
        animTime = 0;
        setState(() {
          yOffset = yOffset + deltaY;
          initialPosition = updatedPosition;
        });
      },
      onVerticalDragEnd: (details) {
        animTime = 300;
        if (yOffset > 200) {
          // 触发滑出动画
          _startSlideOutAnimation();
        } else {
          // 触发回来原始方位的动画
          _startReturnToOriginalPositionAnimation();
        }
      },
      child: Stack(
        children: [
          AnimatedPositioned(  // 组件跟从手指位移,以及抬起手指后组件移动动画
            duration: Duration(milliseconds: animTime),
            curve: Curves.easeInOut,
            top: yOffset,
            left: 0,
            right: 0,
            child: widget.child,
            onEnd: () {
              if(isAnimatingOut) {
                Navigator.of(context).pop();
              }
            },
          ),
        ],
      ),
    );
  }
  // 开端滑出动画
  void _startSlideOutAnimation() {
    setState(() {
      isAnimatingOut = true;
      yOffset = MediaQuery.of(context).size.height;
    });
  }
  // 开端回来原始方位的动画
  void _startReturnToOriginalPositionAnimation() {
    setState(() {
      yOffset = 0.0;
    });
  }
}

这儿的原理很简略

便是经过GestureDetector检测用户的滑动行为

而且经过AnimatedPositioned将用户手指的每一段位移转换成整个组件的移动

而且在终究手指抬起时,经过AnimatedPositioned的动画作用让组件移出屏幕或许复位

咱们写一个页面来运用这个组件

    class TestPage extends StatelessWidget {
      const TestPage({super.key});
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.white,
          body: CloseOnSwipeDownWidget(
            child: Column(
              children: [
                SizedBox(
                  height: MediaQuery.of(context).size.height - 500,
                ), // 让组件内容在页面底部
                Container(
                  height: 500,
                  color: Colors.blue,
                  alignment: Alignment.bottomCenter,
                  child: const Center(
                    child: Text('我是内容'),
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }

看一下作用

Flutter滑动抵触处理——GestureDetector嵌套ListView

那么现在假如把传入的内容换成一个ListView

你会发现ListView内部的内容能够正常滑动

可是外部的GestureDetector无法呼应用户的手势了

咱们下面就来处理这个问题

GestureDetector嵌套ListView

首要咱们要知道为什么嵌入ListView后GestureDetector会失效

这是Flutter的竞技场机制导致的

用户的一个滑动行为其实在底层时经过down、move和up三种事情完成的

当一个down事情呈现后,假如手指按下的坐标方位有多个组件能够呼应滑动事情

便是咱们现在例子中的GestureDetector嵌套ListView的场景

Flutter结构会将这些组件都参加竞技场

然后经过一定的逻辑挑选一个组件胜出

一般同类组件嵌套时最内层的组件胜出

胜出的组件会处理接下来的move和up事情,其它组件则不会继续处理这些事情了

在GestureDetector嵌套ListView的场景中

ListView终究胜出,所以后续的事情都交由ListView处理

而GestureDetector收不到后续的事情,也就不会呼应用户的手势了

因而,咱们处理这个问题的第一步便是要让GestureDetector在这种场景下也能收到后续的事情

决胜竞技场

其实要做到这一步很简略

GestureDetector真正处理用户手势事情的是内部的Recognizer

比方处理上下滑动的是VerticalDragGestureRecognizer

而Recognizer在竞技场失败后也能够单方面宣告自己胜出

这样即使在竞技场失败了,GestureDetector也能收到后续的手势事情

因而咱们现定义一个单方面宣告胜出的Recognizer

    class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
      @override
      void rejectGesture(int pointer) {
        // 单方面宣告自己胜出
        acceptGesture(pointer);
      }
    }

接下来,把这个Recognizer参加到GestureDetector中

这时就需求用到一个GestureDetector的底层组件RawGestureDetector

经过它咱们能够自己指定需求的Recognizer

    RawGestureDetector(
          gestures: {
            _MyVerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
                    _MyVerticalDragGestureRecognizer>(
                () => _MyVerticalDragGestureRecognizer(),
                (_MyVerticalDragGestureRecognizer recognizer) {
              recognizer
                ..onStart = (DragStartDetails details) {
                }
                ..onUpdate = (DragUpdateDetails details) {
                }
                ..onEnd = (DragEndDetails details) {
                };
            }),
          },
          child: ...
        );

这其中的onStart、onUpdate和onEnd 办法

就对应了GestureDetector中的onVerticalDragDown、onVerticalDragUpdate和onVerticalDragEnd办法

好的,到现在为止,咱们现已处理了竞技场形成的只要ListView能收到手势事情的问题

可是这样的话就会形成用户滑动,内外两层组件都在移动的问题

因而,接下来咱们就来处理怎么两个滑动组件怎么相互配合

监听ListView的翻滚

ListView是ScrollView的子类

所有的ScrollView都会在翻滚过程中沿着组件树向上发出各种翻滚状况改变的告诉

经过监听这些告诉事情,就能够判别ScrollView的翻滚状况

    NotificationListener(  // 监听内部ListView的滑动改变
                  onNotification: (ScrollNotification notification) {
                    if (notification is OverscrollNotification && notification.overscroll < 0) {
                      // 用户向下滑动,ListView现已滑动到顶部,处理GestureDetector的滑动事情
                    } else if (notification is ScrollUpdateNotification) {
                      // 用户在ListView中履行滑动动作,封闭外部GestureDetector的滑动处理
                    } else {
                    }
                    return false;
                  },
                  child:  //ListView
                ),

好的,把这些组合起来,完好的代码如下

    import 'package:flutter/gestures.dart';
    import 'package:flutter/material.dart';
    class CloseOnSwipeDownWidget2 extends StatefulWidget {
      final Widget child;
      const CloseOnSwipeDownWidget2({
        Key? key,
        required this.child,
      }) : super(key: key);
      @override
      CloseOnSwipeDownWidget2State createState() => CloseOnSwipeDownWidget2State();
    }
    class CloseOnSwipeDownWidget2State extends State
        with TickerProviderStateMixin {
      double yOffset = 0.0;
      double initialPosition = 0.0;
      bool isAnimatingOut = false;
      int animTime = 0;
      bool needDrag = true;
      @override
      void initState() {
        super.initState();
      }
      @override
      Widget build(BuildContext context) {
        return RawGestureDetector(
          gestures: {
            _MyVerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
                    _MyVerticalDragGestureRecognizer>(
                () => _MyVerticalDragGestureRecognizer(),
                (_MyVerticalDragGestureRecognizer recognizer) {
              recognizer
                ..onStart = (DragStartDetails details) {
                  initialPosition = details.globalPosition.dy;
                }
                ..onUpdate = (DragUpdateDetails details) {
                  if (!needDrag) {
                    return;
                  }
                  double updatedPosition = details.globalPosition.dy;
                  double deltaY = updatedPosition - initialPosition;
                  animTime = 0;
                  setState(() {
                    yOffset = yOffset + deltaY;
                    initialPosition = updatedPosition;
                  });
                }
                ..onEnd = (DragEndDetails details) {
                  animTime = 300;
                  if (yOffset > 200) {
                    // 触发滑出动画
                    _startSlideOutAnimation();
                  } else {
                    // 触发回来原始方位的动画
                    _startReturnToOriginalPositionAnimation();
                  }
                };
            }),
          },
          child: Stack(
            children: [
              AnimatedPositioned(
                duration: Duration(milliseconds: animTime),
                curve: Curves.easeInOut,
                top: yOffset,
                left: 0,
                right: 0,
                child: NotificationListener(  // 监听内部ListView的滑动改变
                  onNotification: (ScrollNotification notification) {
                    if (notification is OverscrollNotification && notification.overscroll < 0) {
                      // 用户向下滑动,ListView现已滑动到顶部,处理GestureDetector的滑动事情
                      needDrag = true;
                    } else if (notification is ScrollUpdateNotification) {
                      // 用户在ListView中履行滑动动作,封闭外部GestureDetector的滑动处理
                      needDrag = false;
                    } else {
                    }
                    return false;
                  },
                  child: widget.child,
                ),
                onEnd: () {
                  if (isAnimatingOut) {
                    Navigator.of(context).pop();
                  }
                },
              ),
            ],
          ),
        );
      }
      // 开端滑出动画
      void _startSlideOutAnimation() {
        setState(() {
          isAnimatingOut = true;
          yOffset = MediaQuery.of(context).size.height;
        });
      }
      // 开端回来原始方位的动画
      void _startReturnToOriginalPositionAnimation() {
        setState(() {
          yOffset = 0.0;
        });
      }
    }
    class _MyVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
      bool needDrag = true;
      @override
      void rejectGesture(int pointer) {
        // 单方面宣告自己胜出
        acceptGesture(pointer);
      }
    }

简略来说便是经过needDrag来判别外部GestureDetector是否跟从用户手势移动

needDrag的值基于监听ListView的状况

当ListView现已滑动到顶部,就开端呼应用户的手势动作

下面是运用这个组件的代码

    class TestPage2 extends StatelessWidget {
      const TestPage2({super.key});
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.white,
          body: CloseOnSwipeDownWidget2(
            child: Column(
              children: [
                SizedBox(
                  height: MediaQuery.of(context).size.height - 500,
                ), // 让组件内容在页面底部
                Container(
                  height: 500,
                  color: Colors.blue,
                  child: ListView.builder(
                      itemCount: 20,
                      itemBuilder: (context, index) {
                        return ListTile(
                          title: Text('index $index'),
                        );
                      }),
                ),
              ],
            ),
          ),
        );
      }
    }

完成作用如下

Flutter滑动抵触处理——GestureDetector嵌套ListView

好了,关于手势组件嵌套的问题就先聊到这儿

假如看到这儿的同学有学习Flutter的兴趣,欢迎联络老刘,咱们互相学习。

点击免费领老刘整理的《Flutter开发手册》,掩盖90%应用开发场景。

能够作为Flutter学习的知识地图。

掩盖90%开发场景的《Flutter开发手册》