当时是为了满足需求, 没想太多顺手写的. 虽然功能上能满足, 但是代码上还是有点low
.
这几天, 我的徒弟傻豆
在写一个IM
项目, 需要滚动到底部
. 于是我重写了一个ScrollHelper
滚动操作类.
文章目录
- 特性
- 需求分析
- 1.滚动, 滚动偏移, 滚动动画
- 2. 滚动到顶部, 底部, 居中
- 3.锁定滚动
- 使用方法
- 1.初始化
- 2.操作方法
- 3.锁定滚动
特性
- 1.支持滚动时的
动画
控制 - 2.支持滚动到任意
position
- 3.支持滚动
offset
控制 - 4.支持滚动到
顶部
or底部
or居中
- 5.支持
锁定滚动
, 短时间之内强制滚动到目标position
- 6.支持智能
锁定滚动
(达到某个条件, 自动滚动到设置的目标position
)
需求分析
1.滚动, 滚动偏移, 滚动动画
需要动画使用:
//带偏移, 带动画
androidx.recyclerview.widget.RecyclerView#smoothScrollBy
//滚动, 带动画
androidx.recyclerview.widget.RecyclerView#smoothScrollToPosition
不需要动画使用:
//带偏移, 不带动画
androidx.recyclerview.widget.RecyclerView#scrollBy
//滚动, 不带动画
androidx.recyclerview.widget.RecyclerView#scrollToPosition
注意:
如果触发了androidx.recyclerview.widget.RecyclerView.ItemAnimator
动画,
那么androidx.recyclerview.widget.RecyclerView#scrollToPosition
和androidx.recyclerview.widget.RecyclerView#smoothScrollToPosition
都会在一定程度上产生滚动动画.
提示
为什么需要使用scrollToPosition
和scrollBy
呢?
这里给大家推荐一套滚动方案:
如果需要滚动的目标
已经出现在屏幕内
, 那么直接使用scrollBy
orsmoothScrollBy
.
如果需要滚动的目标
没有出现在屏幕内
, 那么先使用scrollToPosition
orsmoothScrollToPosition
,再使用scrollBy
orsmoothScrollBy
.
如果调用了androidx.recyclerview.widget.RecyclerView.Adapter#notifyItemInserted
, 那么scrollToPosition
orsmoothScrollToPosition
方法可能会无效果.通常此时都需要使用post
, 文章后面会给出我的方法.
2. 滚动到顶部, 底部, 居中
需要细粒度
的控制滚动, 必须要保证目标已经出现的屏幕内
, 才看完美控制.
控制方法就是scrollBy
orsmoothScrollBy
.
/**当需要滚动的目标位置已经在屏幕上可见*/
internal fun scrollWithVisible(scrollParams: ScrollParams) {
when (scrollType) {
SCROLL_TYPE_NORMAL -> {//不处理
//nothing
}
SCROLL_TYPE_TOP -> {//滚动到顶部
viewByPosition(scrollParams.scrollPosition)?.also { child ->
recyclerView?.apply {
val dx = layoutManager!!.getDecoratedLeft(child) -
paddingLeft - scrollParams.scrollOffset
val dy = layoutManager!!.getDecoratedTop(child) -
paddingTop - scrollParams.scrollOffset
if (scrollParams.scrollAnim) {
smoothScrollBy(dx, dy)
} else {
scrollBy(dx, dy)
}
}
}
}
SCROLL_TYPE_BOTTOM -> {//滚动到底部
viewByPosition(scrollParams.scrollPosition)?.also { child ->
recyclerView?.apply {
val dx =
layoutManager!!.getDecoratedRight(child) -
measuredWidth + paddingRight + scrollParams.scrollOffset
val dy =
layoutManager!!.getDecoratedBottom(child) -
measuredHeight + paddingBottom + scrollParams.scrollOffset
if (scrollParams.scrollAnim) {
smoothScrollBy(dx, dy)
} else {
scrollBy(dx, dy)
}
}
}
}
SCROLL_TYPE_CENTER -> {//滚动到居中
viewByPosition(scrollParams.scrollPosition)?.also { child ->
recyclerView?.apply {
val recyclerCenterX =
(measuredWidth - paddingLeft - paddingRight) / 2 + paddingLeft
val recyclerCenterY =
(measuredHeight - paddingTop - paddingBottom) / 2 + paddingTop
val dx = layoutManager!!.getDecoratedLeft(child) - recyclerCenterX +
layoutManager!!.getDecoratedMeasuredWidth(child) / 2 + scrollParams.scrollOffset
val dy = layoutManager!!.getDecoratedTop(child) - recyclerCenterY +
layoutManager!!.getDecoratedMeasuredHeight(child) / 2 + scrollParams.scrollOffset
if (scrollParams.scrollAnim) {
smoothScrollBy(dx, dy)
} else {
scrollBy(dx, dy)
}
}
}
}
}
}
private fun viewByPosition(position: Int): View? {
return recyclerView?.layoutManager?.findViewByPosition(position)
}
3.锁定滚动
锁定滚动我这里使用了ViewTreeObserver.OnGlobalLayoutListener
orViewTreeObserver.OnDrawListener
当做触发时机, 这样就不用自己写handle post
了, 而且触发更及时.
inner abstract class LockScrollListener : ViewTreeObserver.OnGlobalLayoutListener,
ViewTreeObserver.OnDrawListener,
IAttachListener, Runnable {
/**激活滚动动画*/
var scrollAnim: Boolean = true
/**激活第一个滚动的动画*/
var firstScrollAnim: Boolean = false
/**不检查界面 情况, 强制滚动到最后的位置. 关闭后. 会智能判断*/
var force: Boolean = false
/**第一次时, 是否强制滚动*/
var firstForce: Boolean = true
/**滚动阈值, 倒数第几个可见时, 就允许滚动*/
var scrollThreshold = 2
/**锁定需要滚动的position, -1就是最后一个*/
var lockPosition = RecyclerView.NO_POSITION
/**是否激活功能*/
var enableLock = true
/**锁定时长, 毫秒*/
var lockDuration: Long = -1
//记录开始的统计时间
var _lockStartTime = 0L
override fun run() {
if (!enableLock || recyclerView?.layoutManager?.itemCount ?: 0 <= 0) {
return
}
isScrollAnim = if (firstForce) firstScrollAnim else scrollAnim
scrollType = SCROLL_TYPE_BOTTOM
val position =
if (lockPosition == RecyclerView.NO_POSITION) lastItemPosition() else lockPosition
if (force || firstForce) {
scroll(position)
onScrollTrigger()
L.i("锁定滚动至->$position $force $firstForce")
} else {
val lastItemPosition = lastItemPosition()
if (lastItemPosition != RecyclerView.NO_POSITION) {
//智能判断是否可以锁定
if (position == 0) {
//滚动到顶部
val findFirstVisibleItemPosition =
recyclerView?.layoutManager.findFirstVisibleItemPosition()
if (findFirstVisibleItemPosition <= scrollThreshold) {
scroll(position)
onScrollTrigger()
L.i("锁定滚动至->$position")
}
} else {
val findLastVisibleItemPosition =
recyclerView?.layoutManager.findLastVisibleItemPosition()
if (lastItemPosition - findLastVisibleItemPosition <= scrollThreshold) {
//最后第一个或者最后第2个可见, 智能判断为可以滚动到尾部
scroll(position)
onScrollTrigger()
L.i("锁定滚动至->$position")
}
}
}
}
firstForce = false
}
var attachView: View? = null
override fun attach(view: View) {
detach()
attachView = view
}
override fun detach() {
attachView?.removeCallbacks(this)
}
/**[ViewTreeObserver.OnDrawListener]*/
override fun onDraw() {
initLockStartTime()
onLockScroll()
}
/**[ViewTreeObserver.OnGlobalLayoutListener]*/
override fun onGlobalLayout() {
initLockStartTime()
onLockScroll()
}
open fun initLockStartTime() {
if (_lockStartTime <= 0) {
_lockStartTime = nowTime()
}
}
open fun isLockTimeout(): Boolean {
return if (lockDuration > 0) {
val nowTime = nowTime()
nowTime - _lockStartTime > lockDuration
} else {
false
}
}
open fun onLockScroll() {
attachView?.removeCallbacks(this)
if (enableLock) {
if (isLockTimeout()) {
//锁定超时, 放弃操作
} else {
attachView?.post(this)
}
}
}
open fun onScrollTrigger() {
}
}
/**锁定滚动到最后一个位置*/
inner class LockScrollLayoutListener : LockScrollListener() {
override fun attach(view: View) {
super.attach(view)
view.viewTreeObserver.addOnGlobalLayoutListener(this)
}
override fun detach() {
super.detach()
attachView?.viewTreeObserver?.removeOnGlobalLayoutListener(this)
}
}
/**滚动到0*/
inner class FirstPositionListener : LockScrollListener() {
init {
lockPosition = 0
firstScrollAnim = true
scrollAnim = true
force = true
firstForce = true
}
override fun attach(view: View) {
super.attach(view)
view.viewTreeObserver.addOnDrawListener(this)
}
override fun detach() {
super.detach()
attachView?.viewTreeObserver?.removeOnDrawListener(this)
}
override fun onScrollTrigger() {
super.onScrollTrigger()
if (isLockTimeout() || lockDuration == -1L) {
detach()
}
}
}
private interface IAttachListener {
fun attach(view: View)
fun detach()
}
//滚动参数
internal data class ScrollParams(
var scrollPosition: Int = RecyclerView.NO_POSITION,
var scrollType: Int = SCROLL_TYPE_NORMAL,
var scrollAnim: Boolean = true,
var scrollOffset: Int = 0
)
使用方法
复制源码
到工程即可, 就一个类文件
.
1.初始化
val scrollHelper = ScrollHelper()
scrollHelper.attach(recyclerView)
2.操作方法
每次触发滚动时, 可配置的参数:
/**触发滚动是否伴随了adapter的addItem*/
var isFromAddItem = false
/**滚动是否需要动画*/
var isScrollAnim = false
/**滚动类别*/
var scrollType = SCROLL_TYPE_NORMAL
/**额外的偏移距离*/
var scrollOffset: Int = 0
/**滚动类别: 默认不特殊处理. 滚动到item显示了就完事*/
const val SCROLL_TYPE_NORMAL = 0
/**滚动类别: 将item滚动到第一个位置*/
const val SCROLL_TYPE_TOP = 1
/**滚动类别: 将item滚动到最后一个位置*/
const val SCROLL_TYPE_BOTTOM = 2
/**滚动类别: 将item滚动到居中位置*/
const val SCROLL_TYPE_CENTER = 3
//滚动到指定位置
ScrollHelper#scroll(position)
//滚动到底部
ScrollHelper#scrollToLast()
//滚动到顶部
ScrollHelper#scrollToFirst()
3.锁定滚动
锁定滚动配置参数:
/**激活滚动动画*/
var scrollAnim: Boolean = true
/**激活第一个滚动的动画*/
var firstScrollAnim: Boolean = false
/**不检查界面 情况, 强制滚动到最后的位置. 关闭后. 会智能判断*/
var force: Boolean = false
/**第一次时, 是否强制滚动*/
var firstForce: Boolean = true
/**滚动阈值, 倒数第几个可见时, 就允许滚动*/
var scrollThreshold = 2
/**锁定需要滚动的position, -1就是最后一个*/
var lockPosition = RecyclerView.NO_POSITION
/**是否激活功能*/
var enableLock = true
/**锁定时长, 毫秒*/
var lockDuration: Long = -1
//锁定滚动
ScrollHelper#lockPosition()