使用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
顶部的长度(如列表顶部滑出屏幕的长度) -
extentInside
:ViewPort
内部长度(如屏幕内的列表长度) -
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
监听滑动停止事件时,需要注意快速滑动场景下会产生多次回调,关注下是否会对业务产生不好的影响。
网友评论