美文网首页
Flutter滑动停止事件ScrollEndNotificati

Flutter滑动停止事件ScrollEndNotificati

作者: 厘米姑娘 | 来源:发表于2023-05-30 17:27 被阅读0次

    使用ScrollEndNotification可能要注意的点。

    发现问题

    在做Flutter流畅度优化之过度绘制的时候发现了一个体验问题--滑动过程中就发生了视频加载,如图所示,可以看到滑走的视频都会变成loading状态:

    正常情况下应该在滑动停止再进行加载,避免不必要的耗时操作。这里依赖的滑动停止事件是ScrollEndNotification,经过测试,在完整的一次滑动过程中的确会在完全停止后回调它,这是符合预期的;但是在快速滑动时,也就是发生多次滑动,滑动还处于Fling阶段就回调了,所以才会有上图的表现。

    定位原因

    ScrollEndNotification到底能否准确描述为“滑动完全停止”?如果不能,Flutter是否还提供其他方法呢?带着这两个疑问来看下具体实现。

    首先来分析下为何多次滑动在手指按下时都先回调ScrollEndNotification。在 ScrollPosition启动一个滑动任务(beginActivity)中找到了答案,会先停止掉旧的滑动事件(didEndScroll)再开始新的滑动事件(didStartScroll)。

    void beginActivity(ScrollActivity? newActivity) {
      if (newActivity == null)
        return;
      bool wasScrolling, oldIgnorePointer;
      if (_activity != null) {
        oldIgnorePointer = _activity!.shouldIgnorePointer;
        wasScrolling = _activity!.isScrolling;
        if (wasScrolling && !newActivity.isScrolling)
          didEndScroll(); // notifies and then saves the scroll offset
        _activity!.dispose();
      } else {
        oldIgnorePointer = false;
        wasScrolling = false;
      }
      _activity = newActivity;
      if (oldIgnorePointer != activity!.shouldIgnorePointer)
        context.setIgnorePointer(activity!.shouldIgnorePointer);
      isScrollingNotifier.value = activity!.isScrolling;
      if (!wasScrolling && _activity!.isScrolling)
        didStartScroll();
    }
    
    

    既然如此,再来看下ScrollEndNotification本身提供的信息能否能帮助区分是哪种停止:

    class ScrollEndNotification extends ScrollNotification {
      ScrollEndNotification({
        required super.metrics,
        required BuildContext super.context,
        this.dragDetails,
      });
      final DragEndDetails? dragDetails;
    }
    

    主要是这两方面信息:

    (1)ScrollMetrics表示当前可视区ViewPort和滑动位置等信息,常用属性如

    • pixels :当前滑动位置
    • maxScrollExtent/minScrollExtent :可最大/最小可滑动长度(如列表顶部/底部)
    • axisDirection/axis:滚动方向/轴
    • outOfRange:是否超过边界(pixels < minScrollExtent || pixels > maxScrollExtent)
    • atEdge:是否达到边界(pixels == minScrollExtent || pixels == maxScrollExtent)
    • extentBefore :滑出ViewPort顶部的长度(如列表顶部滑出屏幕的长度)
    • extentInsideViewPort内部长度(如屏幕内的列表长度)
    • extentAfter :列表中未滑入ViewPort部分的长度(如列表底部未显示到屏幕内的长度)

    (2)DragEndDetails 表示滑动停止相关信息,主要是速度信息,常用属性如

    • velocity:指针停止接触屏幕时的移动速度
    • primaryVelocity:指针停止时沿主轴移动的速度

    然而这个速度信息并非所有情况都会返回,若拖动结束的残余速度足以进行减速运动此时会返回空,只有当速度太小直接停止滑动的才有值,在本例中返回的都是null。看来并不能直接通过ScrollEndNotification知道什么时候滑动完全停止。而在Flutter熟知的其他滑动监听器也看了下:

    (1)滑动控制器 ScrollController,通过addListener方式增加监听器,主要还是滑动本身信息

    • offset:滑动偏移量
    • position:滑动位置信息,如minScrollExtent顶部位置、maxScrollExtent底部位置

    (2)手势监听器 GestureDetector,可以监听单击、双击、长按、多指、缩放、拖动、滑动等手势,其中滑动相关监听事件有

    • onPanDown:手指按下事件,回调信息DragDownDetails
    • onPanUpdate:手指滑动事件,回调信息DragUpdateDetails
    • onPanEnd:滑动停止事件,回调信息DragEndDetails(就是前面提到的)

    因为本例中的监听的对象本身已处理了手势,外部也无法获得滑动事件的处理权了。

    解决办法

    综上,目前没有很好的办法可以准确描述快速滑动下的滑动停止。那换个思路,在ScrollEndNotification延迟一定时间再去做启播,既可以解决误启动的问题,还可以避免和曝光检测任务同时抢占资源,从而提升滑动流畅度。

    通过实验测试,延迟30ms是较为有效的,即下一次ScrollStartNotification事件和上一次ScrollEndNotification事件差值基本在30ms内,且不会过度影响用户观看体验。

    bool _onScroll(ScrollNotification notification) {
      if (notification is ScrollStartNotification) {
        feedScrollHandler?.scrollStart();//滑动开始,取消检测曝光逻辑和自动播放逻辑
      } else if (notification is ScrollEndNotification) {
        feedScrollHandler?.scrollEnd(delay: FeedScrollBaseHandler.endScrollPlayDelay); //滑动结束,准备检测曝光逻辑和自动播放逻辑
      }
      return false;
    }
    

    改动后的效果如图,对比了改前后快速滑动下的帧率也确实得到了提升:

    总结

    在使用ScrollEndNotification监听滑动停止事件时,需要注意快速滑动场景下会产生多次回调,关注下是否会对业务产生不好的影响。

    相关文章

      网友评论

          本文标题:Flutter滑动停止事件ScrollEndNotificati

          本文链接:https://www.haomeiwen.com/subject/kwycjdtx.html