專案應用篇-RecyclerView巢狀滑動置頂效果實踐~
都2021了,RecyclerView巢狀滑動置頂應該已經被說爛了吧,但是如果專案中真的需要一個這樣的結構應用到首頁,想找到一個成熟的方案並不容易。這篇文章給出的是已穩定執行大半年的巢狀滑動程式碼。程式碼地址:
專案來源
半年前接到的任務需要將首頁改為天貓或京東的一樣,現在似乎滑動置頂都是標配了,之前在網上看到那麼多這類似的文章,找找應該不難,結果我幾乎找遍了所有的文章與專案基本都不能使用,有卡頓的bug,問作者有沒有應用到專案中也沒有迴應。
本來想像以前的滑動置頂使用CoordinatorLayout+TabLayout+RecyclerView
的形式處理,但是感覺欠妥,用uiautomatorviewer
分析了天貓/京東/網易考拉所有App的首頁都是使用的RecyclerView-ViewPager-RecyclerView
的形式,然後繼續尋找時發現了一個最接近的專案xmuSistone/PersistentRecyclerView
。這應該是我找過的最完善的程式碼,然後應用到實際專案中發現還是有問題:
- 1.在華為裝置上滑動子RecyclerView時會有跳動
- 2.父RecyclerView下拉重新整理使用的是SmartRefreshLayout有issues反應有卡頓
- 3.子RecyclerView載入更多需要處理
- 4.子RecyclerView巢狀橫向的RecyclerView滑動衝突問題
- 5.Android4.4慣性滑動崩潰問題 當然輪子不可能完美貼合專案的需求,於是在上面修改了部分程式碼。
完善
1.在華為裝置上滑動子RecyclerView時會有跳動
這個問題我在好幾個作者寫的滑動置頂程式碼那裡都發現了,其他手機都是沒問題的,原因是華為裝置靈敏度很高,在手指放在ChildRecyclerView時很容易觸發parent.requestDisallowInterceptTouchEvent(false)
將事件丟給ParentRecyclerView,然後導致卡頓。處理方式是在dispatchTouchEven
ParentRecyclerView
。具體程式碼:
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
val x = e.rawX
val y = e.rawY
when (e.action) {
MotionEvent.ACTION_DOWN -> {
//將按下時的座標儲存
downX = x
downY = y
// true 表示讓ParentRecyclerView不要攔截
parent.requestDisallowInterceptTouchEvent(true)
}
MotionEvent.ACTION_MOVE -> {
//獲取到距離差
val dx: Float = x - downX
val dy: Float = y - downY
// 通過距離差判斷方向
val orientation = getOrientation(dx, dy)
val location = intArrayOf(0, 0)
getLocationOnScreen(location)
when (orientation) {
"d" -> if (canScrollVertically(-1)) {
// 可以向下滑動時讓ParentRecyclerView不要攔截
parent.requestDisallowInterceptTouchEvent(true)
} else { //內層RecyclerView下拉到最頂部時
if(dy < 24f){
// 如果滑動的距離小於這個值依然讓Parent不攔截
parent.requestDisallowInterceptTouchEvent(true)
}else{
// 將滑動事件拋給Parent,這樣可以隨著Parent一起滑動
parent.requestDisallowInterceptTouchEvent(false)
}
}
"u" -> {
// 向上滑動時,始終由ChildRecyclerView處理
parent.requestDisallowInterceptTouchEvent(true)
}
}
}
}
return super.dispatchTouchEvent(e)
}
private fun getOrientation(dx: Float, dy: Float): String {
return if (Math.abs(dx) > Math.abs(dy)) {
//X軸移動
if (dx > 0) "r" else "l" //右,左
} else {
//Y軸移動
if (dy > 0) "d" else "u" //下//上
}
}
2.解決下拉重新整理/上拉載入問題(問題2/3)
由於之前花了一番功夫寫了ByRecyclerView
,支援下拉重新整理和上拉載入,其本質上就是Adapter上加一個特殊的viewType
來處理,所帶來的相容性也好很多,於是將BaseRecyclerView
繼承ByRecyclerView
後就達到了要求,給ParentRecyclerView
和ChildRecyclerView
分別加下拉重新整理和上拉載入的監聽就好了:
parentRecyclerView.setOnRefreshListener { }
childRecyclerView.setOnLoadMoreListener { }
3.子RecyclerView裡的item巢狀橫向的RecyclerView滑動衝突問題
這部分相對於巢狀置頂的處理要簡單多了:
override fun dispatchTouchEvent(e: MotionEvent): Boolean {
val x = e.rawX
val y = e.rawY
when (e.action) {
MotionEvent.ACTION_DOWN -> {
downX = x
downY = y
}
MotionEvent.ACTION_MOVE -> {
// 獲取到距離差
val dx: Float = x - downX
val dy: Float = y - downY
// 通過距離差判斷方向
val orientation = getOrientation(dx, dy)
val location = intArrayOf(0, 0)
getLocationOnScreen(location)
when (orientation) {
// 上下滑動時拋給ChildRecyclerView處理
"d" -> parent.requestDisallowInterceptTouchEvent(false)
"u" -> parent.requestDisallowInterceptTouchEvent(false)
"r" -> {
if (canScrollVertically(-1)) {
// 可以向右滑動時,自己處理,可以內部左右滑
parent.requestDisallowInterceptTouchEvent(true)
} else {
// 右滑動到頂時,交給parent處理,使其可以滑到ViewPager下一個的position
parent.requestDisallowInterceptTouchEvent(false)
}
}
"l" -> {
if (canScrollVertically(-1)) {
parent.requestDisallowInterceptTouchEvent(true)
} else {
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
return super.dispatchTouchEvent(e)
}
4.Android 4.4慣性滑動崩潰問題
上到線上後發現,使用Android 4.4手機慣性滑動停止後會必現崩潰,具體原因是onNestedScrollAccepted()
was added in API 21. You can't use it in lower API levels.,因為使用此版本的人數很少,所以粗略處理了一下:
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
try {
if (android.os.Build.VERSION.SDK_INT <= 19) {
return true
}
return super.onNestedPreFling(target, velocityX, velocityY)
} catch (e: Exception) {
return true
}
}
總結
此專案是在PersistentRecyclerView
中發展修改而來,其中解決了部分卡頓相容問題和加了下拉重新整理上拉載入功能,且經過一段時間的大範圍使用,基本趨於穩定,使用起來更加方便了。專案程式碼放在了:
Android高階開發系統進階筆記、最新面試複習筆記PDF,我的GitHub
文末
您的點贊收藏就是對我最大的鼓勵!
歡迎關注我,分享Android乾貨,交流Android技術。
對文章有何見解,或者有何技術問題,歡迎在評論區一起留言討論!