之前写过一篇Android–>RecyclerView 显示底部,滚动底部(无动画)文章.
当时是为了满足需求, 没想太多顺手写的. 虽然功能上能满足, 但是代码上还是有点low
.
这几天, 我的徒弟傻豆
在写一个IM
项目, 需要滚动到底部
. 于是我重写了一个ScrollHelper
滚动操作类.
文章目录
- 特性
- 需求分析
- 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 -> {
}
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
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) {
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)
}
override fun onDraw() {
initLockStartTime()
onLockScroll()
}
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)
}
}
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.操作方法
每次触发滚动时, 可配置的参数:
var isFromAddItem = false
var isScrollAnim = false
var scrollType = SCROLL_TYPE_NORMAL
var scrollOffset: Int = 0
const val SCROLL_TYPE_NORMAL = 0
const val SCROLL_TYPE_TOP = 1
const val SCROLL_TYPE_BOTTOM = 2
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
var lockPosition = RecyclerView.NO_POSITION
var enableLock = true
var lockDuration: Long = -1
ScrollHelper#lockPosition()
砖厂地址
群内有各(pian)种(ni)各(jin)样(qun)
的大佬,等你来撩.
联系作者
点此快速加群
请使用QQ扫码
加群, 小伙伴们都在等着你哦!
关注我的公众号
, 每天都能一起玩耍哦!
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)