您的浏览器不支持CSS3,建议使用Firfox、Chrome等浏览器,以取得最佳显示效果

Android滚动组件图片加载优化与滚动速度的精确监听

开发技术 265℃ 0 3个月前 (03-04)

摘要

Android滚动组件图片加载优化与滚动速度的精确监听,含源码实现。

背景

在Android应用中,ListView / RecyclerView / ScrollView 滚动时,如果有过多图片加载容易导致卡顿,特别是快速滚动时,bindView中大量图片加载操作,会导致系统频繁分配回收内存,不仅消耗大量CPU和网络流量资源,而且极端情况下还会因为内存来不及回收产生OOM。

一种最基本的优化策略是滚动时暂停加载、滚动停止才加载图片。但是这种做法很影响用户体验,用户在慢速滚动时图片完全不会加载。

因此希望实现快速滚动时暂停加载图片,慢速滚动时继续加载图片,从而平衡图片加载和滚动流畅度的体验。

速度计算

速度一般按1s时间内滚动的像素值计算。

V = diffPixels * 1000 / t_ms

滚动距离精确监听

计算速度需要知道滚动距离和时间,时间很容易计算,滚动距离相对复杂一点。

ScrollView获取滚动距离

ScrollView可以通过View.OnScrollChangeListener获取滚动距离,高版本系统可以直接调用View.setOnScrollChangeListener设置,低版本系统覆写View的onScrollChanged方法即可。

还可以参考:ScrollView滚动事件和滚动状态的监听实现

RecyclerView获取滚动距离

RecyclerView的OnScrollListener可以直接获取滚动像素值,不需要特殊处理。

ListView滚动距离的精确监听

ListView的滚动使用的不是基类View提供的滚动机制,因此不能使用View提供的onScrollChanged方法监听滚动的像素值;而ListView的OnScrollListener只能监听滚动状态、滚动到第几个Item,也不能直接取到滚动像素值。

方案1:近似实现

监听单位时间内滚动的Item数量。

存在的问题:某些Item特别长或者特别短,会导致很大的误差。例如ListView的Header可能会包含超过1屏的内容。

方案2:精确获取滚动距离

如果连续两次回调onScroll,firstVisibleItem都是同一个,则通过第一个可见View的getTop之差,就可以知道滚动距离。

如果两次firstVisibleItem差1,可在每次回调时记录下第一个、第二个可见View的Top,然后两次对同一个View的Top求差,即为滚动距离。

对于两次firstVisibleItem相差超过1的情况,即一帧时间内,滚动的距离超过了一个Item。通常是由于Item特别短,此时可以考虑丢弃数据。

速度抖动的解决

获取到滚动像素后,计算出时间,就可以计算速度了。实际使用ListView进行了尝试。

在Android开启硬件加速、不卡顿的情况下,通常每次调用onScroll的时间间隔约为16.7ms(FPS=60)。

实际测试发现,在ListView中Item布局较为复杂的情况下,可能发生卡顿,特别是在getView复用Item的时候。卡顿时会出现某些帧时间间隔偏差很大,例如只有不到10ms;滚动距离也会有较大偏差。

最后导致计算出来的速度有很大偏差。这可能导致速度在阈值附近波动,频繁暂停、启动图片加载,有可能导致一些性能问题,效果不理想。

平滑滤波

解决上述问题,可以考虑对速度做平滑滤波。例如一种简单的滤波方式如下:

/**
 * 平滑后的速度
 */
private int mSmoothedVelocity = 0;

/**
 * 速度变化
 */
public void onVelocityChanged(int velocity) {
    L.d("VelocityTracker", "onVelocityChanged, velocity = %d", velocity);
    final int smoothedVelocity = mSmoothedVelocity * 4 / 5 + velocity / 5;
    if (smoothedVelocity != mSmoothedVelocity) {
        mSmoothedVelocity = smoothedVelocity;
        onSmoothedVelocityChanged(mSmoothedVelocity);
    }
}

/**
 * 平滑处理后的速度变化
 */
public void onSmoothedVelocityChanged(int velocity) {
    L.d("VelocityTracker", "onSmoothedVelocityChanged, velocity = %d", velocity);
}

平滑前后的Log如下(启动滚动时的Log)。可以看出中间有几帧发生卡顿,原始速度从7000多减小到了1000,而平滑滤波后的速度,只是从3000多减小到2900,稳定性有了一定的提高。

D/VelocityTracker: diff = 150
D/VelocityTracker: onScrollBy, diff = 150, ms = 21
D/VelocityTracker: onVelocityChanged, velocity = 7142
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2384
D/VelocityTracker: diff = 125
D/VelocityTracker: onScrollBy, diff = 125, ms = 16
D/VelocityTracker: onVelocityChanged, velocity = 7812
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 3469
D/VelocityTracker: onReachThreshold, reach = true
D/VelocityTracker: diff = 37
D/VelocityTracker: onScrollBy, diff = 37, ms = 12
D/VelocityTracker: onVelocityChanged, velocity = 3083
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 3391
D/VelocityTracker: diff = 7
D/VelocityTracker: onScrollBy, diff = 7, ms = 7
D/VelocityTracker: onVelocityChanged, velocity = 1000
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2912
D/VelocityTracker: onReachThreshold, reach = false
D/VelocityTracker: diff = 96
D/VelocityTracker: onScrollBy, diff = 96, ms = 13
D/VelocityTracker: onVelocityChanged, velocity = 7384
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 3805
D/VelocityTracker: onReachThreshold, reach = true
D/VelocityTracker: diff = 117
D/VelocityTracker: onScrollBy, diff = 117, ms = 16
D/VelocityTracker: onVelocityChanged, velocity = 7312
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 4506

双阈值

用前面的平滑滤波,代码中final int smoothedVelocity = mSmoothedVelocity * 4 / 5 + velocity / 5,每次会把新速度的1/5和平滑速度的4/5相加。

这里的1/5如果取得太小,会导致平滑后的速度延迟很大;如果取得太大,则平滑效果不理想,速度波动仍然会比较大。

为了避免速度在固定阈值上下来回波动,可以使用双阈值的方式处理,例如速度下降到2000则启动图片加载,而上升到2500才暂停图片加载。当速度在2000~2500之间波动时,并不会反复切换图片加载。

延长采样周期

每一帧回调onScroll方法时都采样和计算速度,容易导致较大的速度抖动。采用了前面的平滑滤波、双阈值方法,效果还是不理想,尝试使用延长采样周期的方式处理。

实现思路是,每一帧都计算滚动距离并累加,但每8帧才做一次时间采样和速度计算。8帧会持续约0.13s,这样只要不出现连续很久的卡顿,速度的计算就是比较准确的。

下面是延长采样周期后,一次完整的滚动Log输出,可以看出,速度比较平稳的减小。即使不使用平滑滤波和双阈值,也能比较好的实现需要的效果。

D/VelocityTracker: count = 0, diff = -2147483648, mDiff = 0
D/VelocityTracker: count = 1, diff = 39
D/VelocityTracker: count = 2, diff = 45
D/VelocityTracker: count = 3, diff = 312
D/VelocityTracker: count = 4, diff = 0
D/VelocityTracker: count = 5, diff = 79
D/VelocityTracker: count = 6, diff = 97
D/VelocityTracker: count = 7, diff = 102
D/VelocityTracker: onScrollBy, diff = 776, ms = 152
D/VelocityTracker: onVelocityChanged, velocity = 5105
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1021
D/VelocityTracker: count = 0, diff = 102
D/VelocityTracker: count = 1, diff = 96
D/VelocityTracker: count = 2, diff = 106
D/VelocityTracker: count = 3, diff = 94 
D/VelocityTracker: count = 4, diff = 98 
D/VelocityTracker: count = 5, diff = 90 
D/VelocityTracker: count = 6, diff = 95 
D/VelocityTracker: count = 7, diff = 119
D/VelocityTracker: onScrollBy, diff = 767, ms = 135
D/VelocityTracker: onVelocityChanged, velocity = 5681
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1952
D/VelocityTracker: count = 0, diff = 69
D/VelocityTracker: count = 1, diff = 77
D/VelocityTracker: count = 2, diff = 85
D/VelocityTracker: count = 3, diff = 77
D/VelocityTracker: count = 4, diff = 75
D/VelocityTracker: count = 5, diff = 76
D/VelocityTracker: count = 6, diff = 73
D/VelocityTracker: count = 7, diff = 66
D/VelocityTracker: onScrollBy, diff = 596, ms = 131
D/VelocityTracker: onVelocityChanged, velocity = 4549
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2470
D/VelocityTracker: count = 0, diff = 67
D/VelocityTracker: count = 1, diff = 64
D/VelocityTracker: count = 2, diff = 58
D/VelocityTracker: count = 3, diff = 58
D/VelocityTracker: count = 4, diff = 55
D/VelocityTracker: count = 5, diff = 49
D/VelocityTracker: count = 6, diff = 50
D/VelocityTracker: count = 7, diff = 45
D/VelocityTracker: onScrollBy, diff = 424, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 3187
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2613
D/VelocityTracker: count = 0, diff = 45
D/VelocityTracker: count = 1, diff = 43
D/VelocityTracker: count = 2, diff = 38
D/VelocityTracker: count = 3, diff = 39
D/VelocityTracker: count = 4, diff = 34
D/VelocityTracker: count = 5, diff = 43
D/VelocityTracker: count = 6, diff = 27
D/VelocityTracker: count = 7, diff = 28
D/VelocityTracker: onScrollBy, diff = 282, ms = 134
D/VelocityTracker: onVelocityChanged, velocity = 2104
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2510
D/VelocityTracker: count = 0, diff = 30
D/VelocityTracker: count = 1, diff = 28
D/VelocityTracker: count = 2, diff = 29
D/VelocityTracker: count = 3, diff = 24
D/VelocityTracker: count = 4, diff = 23
D/VelocityTracker: count = 5, diff = 22
D/VelocityTracker: count = 6, diff = 23
D/VelocityTracker: count = 7, diff = 22
D/VelocityTracker: onScrollBy, diff = 189, ms = 132
D/VelocityTracker: onVelocityChanged, velocity = 1431
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2294
D/VelocityTracker: count = 0, diff = 18
D/VelocityTracker: count = 1, diff = 20
D/VelocityTracker: count = 2, diff = 17
D/VelocityTracker: count = 3, diff = 18
D/VelocityTracker: count = 4, diff = 16
D/VelocityTracker: count = 5, diff = 16
D/VelocityTracker: count = 6, diff = 20
D/VelocityTracker: count = 7, diff = 11
D/VelocityTracker: onScrollBy, diff = 130, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 977
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2030
D/VelocityTracker: count = 0, diff = 12
D/VelocityTracker: count = 1, diff = 13
D/VelocityTracker: count = 2, diff = 14
D/VelocityTracker: count = 3, diff = 12
D/VelocityTracker: count = 4, diff = 11
D/VelocityTracker: count = 5, diff = 11
D/VelocityTracker: count = 6, diff = 11
D/VelocityTracker: count = 7, diff = 10
D/VelocityTracker: onScrollBy, diff = 92, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 691
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1762
D/VelocityTracker: count = 0, diff = 10
D/VelocityTracker: count = 1, diff = 10
D/VelocityTracker: count = 2, diff = 8
D/VelocityTracker: count = 3, diff = 9
D/VelocityTracker: count = 4, diff = 8
D/VelocityTracker: count = 5, diff = 8
D/VelocityTracker: count = 6, diff = 8
D/VelocityTracker: count = 7, diff = 7
D/VelocityTracker: onScrollBy, diff = 65, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 488
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1506
D/VelocityTracker: count = 0, diff = 7
D/VelocityTracker: count = 1, diff = 6
D/VelocityTracker: count = 2, diff = 7
D/VelocityTracker: count = 3, diff = 6
D/VelocityTracker: count = 4, diff = 6
D/VelocityTracker: count = 5, diff = 6
D/VelocityTracker: count = 6, diff = 5
D/VelocityTracker: count = 7, diff = 5
D/VelocityTracker: onScrollBy, diff = 46, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 345
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1273
D/VelocityTracker: count = 0, diff = 5
D/VelocityTracker: count = 1, diff = 5
D/VelocityTracker: count = 2, diff = 5
D/VelocityTracker: count = 3, diff = 4
D/VelocityTracker: count = 4, diff = 4
D/VelocityTracker: count = 5, diff = 4
D/VelocityTracker: count = 6, diff = 4
D/VelocityTracker: count = 7, diff = 4
D/VelocityTracker: onScrollBy, diff = 33, ms = 134
D/VelocityTracker: onVelocityChanged, velocity = 246
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1067
D/VelocityTracker: count = 0, diff = 3
D/VelocityTracker: count = 1, diff = 3
D/VelocityTracker: count = 2, diff = 4
D/VelocityTracker: count = 3, diff = 3
D/VelocityTracker: count = 4, diff = 3
D/VelocityTracker: count = 5, diff = 2
D/VelocityTracker: count = 6, diff = 3
D/VelocityTracker: count = 7, diff = 2
D/VelocityTracker: onScrollBy, diff = 23, ms = 132
D/VelocityTracker: onVelocityChanged, velocity = 174
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 887
D/VelocityTracker: count = 0, diff = 3
D/VelocityTracker: count = 1, diff = 2
D/VelocityTracker: count = 2, diff = 2
D/VelocityTracker: count = 3, diff = 2
D/VelocityTracker: count = 4, diff = 2
D/VelocityTracker: count = 5, diff = 2
D/VelocityTracker: count = 6, diff = 2
D/VelocityTracker: count = 7, diff = 1
D/VelocityTracker: onScrollBy, diff = 15, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 112
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 731
D/VelocityTracker: count = 0, diff = 2
D/VelocityTracker: count = 1, diff = 1
D/VelocityTracker: count = 2, diff = 2
D/VelocityTracker: count = 3, diff = 1
D/VelocityTracker: count = 4, diff = 1
D/VelocityTracker: count = 5, diff = 1
D/VelocityTracker: count = 6, diff = 1
D/VelocityTracker: count = 7, diff = 1
D/VelocityTracker: onScrollBy, diff = 9, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 67
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 597
D/VelocityTracker: count = 0, diff = 1
D/VelocityTracker: count = 1, diff = 1
D/VelocityTracker: count = 2, diff = 0
D/VelocityTracker: count = 3, diff = 1
D/VelocityTracker: count = 4, diff = 1
D/VelocityTracker: count = 5, diff = 0
D/VelocityTracker: count = 6, diff = 0
D/VelocityTracker: count = 7, diff = 1
D/VelocityTracker: onScrollBy, diff = 4, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 30
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 483
D/VelocityTracker: count = 0, diff = 0
D/VelocityTracker: count = 1, diff = 0
D/VelocityTracker: count = 2, diff = 1
D/VelocityTracker: count = 3, diff = 0
D/VelocityTracker: count = 4, diff = 0
D/VelocityTracker: count = 5, diff = 0
D/VelocityTracker: count = 6, diff = 0
D/VelocityTracker: count = 7, diff = 0
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 0
D/VelocityTracker: onVelocityChanged, velocity = 0

结论

经过尝试,最后确定同时使用延长采样周期、双阈值两种方法,比较好的解决了速度抖动的问题。

代码实现

将时间采样、速度计算、阈值处理相关的逻辑,放在一个单独的类ScrollVelocityTracker里,ScrollView、ListView、RecyclerView的监听器分别调用这个类,每次传入位移像素即可。最后在回调中,可以设置图片库暂停、继续加载,从而优化图片加载性能。

完整的代码实现和Demo示例在此:

https://github.com/jzj1993/AndroidPlayground/tree/master/app/src/scrollvelocity

最后,欢迎扫码关注微信公众号。微软 / Shopee / Coupang / BAT等国内外企业内推、行业和技术交流,也可以加我微信 jzj2015(注明来自博客),拉你进技术群。

本文由原创,转载请注明来源:https://www.paincker.com/android-scroll-velocity
(标注了原文链接的文章除外)

0

暂无评论

评论前:需填写以下信息,或 登录

用户登录

忘记密码?