之前寫過一篇Android–>RecyclerView 顯示底部,滾動底部(無動畫)文章.
當時是爲了滿足需求, 沒想太多順手寫的. 雖然功能上能滿足, 但是代碼上還是有點low
.
這幾天, 我的徒弟傻豆
在寫一個IM
項目, 需要滾動到底部
. 於是我重寫了一個ScrollHelper
滾動操作類.
特性
- 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()
羣內有各(pian)種(ni)各(jin)樣(qun)
的大佬,等你來撩.
聯繫作者
請使用
QQ掃碼
加羣, 小夥伴們都在等着你哦!
關注
我的公衆號
, 每天都能一起玩耍哦!