安卓经典老坑,在发布之初,安卓传统View的ViewPager、ViewPager2和Compose的LazyList就存在滑动冲突,而现在已经快到2025年了,这个问题仍然存在,估计Google是不打算处理这个问题了。 如果可以的话,这个滑动冲突最简单的处理方法应该是把ViewPager2换成Compose的Pager,毕竟在传统View和Compose之间做太多的处理来解决兼容问题肯定不如纯Compose来的好,但是如果你不想改项目里的老代码,那么可以继续往下看。
我使用的处理方法很简单,使用一个传统的AndroidView包裹LazyList,在这个传统的安卓View中用安卓传统的触摸冲突解决方法进行优化。 将原本的UI层级由
ViewPager2 -> ComposeView -> LazyList
转变为
ViewPager2 -> ComposeView -> LazyListWrapper -> LazyList
在自定义的LazyListWrapper中迭代找到自己的父ScrollingView,而后在dispatchTouchEvent中根据被包裹的LazyList是否还能滑动判断是否需要调用requestDisallowInterceptTouchEvent就大功告成。至于判断LazyList是否还能滑动这个条件,则可以通过LazyListState轻松实现。
最终的优化效果如下
原理在开头部分已经说了,如果你不想自己实现,只想简单的copy一下,那么也可以直接复制我的代码,我的实现适用于ViewPager和所有的ScrollingView的子类(也包括以RecyclerView实现功能的ViewPager2)。
github DEMO地址:github.com/U-WHY/LazyL…
使用
记得在LazyList中将wrapperListState作为state参数传入
viewPager2Wrapper.adapter = object : FragmentStateAdapter(this) {
override fun getItemCount(): Int = yourSize
override fun createFragment(position: Int): Fragment = ItemFragment()
}
class ItemFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(inflater.context).apply {
setContent {
Column {
LazyListWrapper {
LazyRow(
verticalAlignment = Alignment.CenterVertically,
// TODO: Use wrapperListState
state = wrapperListState
) {
// your item
...
}
}
}
}
}
}
}
源码
package com.uwhy.helper.lazylist
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewParent
import android.widget.FrameLayout
import androidx.annotation.IntDef
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.UiComposable
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ScrollingView
import androidx.viewpager.widget.ViewPager
import com.uwhy.helper.lazylist.LazyListWrapperLayout.OrientationMode.Companion.HORIZONTAL
import com.uwhy.helper.lazylist.LazyListWrapperLayout.OrientationMode.Companion.UNSET
import com.uwhy.helper.lazylist.LazyListWrapperLayout.OrientationMode.Companion.VERTICAL
import kotlin.math.absoluteValue
@Stable
data class LazyListWrapperScope(val wrapperListState: LazyListState)
/**
* remember use wrapperListState in LazyList
*/
@Composable
fun LazyListWrapper(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
content: @Composable @UiComposable LazyListWrapperScope.() -> Unit
) {
AndroidView(
factory = { context -> LazyListWrapperLayout(context) },
modifier = modifier
) { view ->
(view as? LazyListWrapperLayout)?.let { layout ->
layout.content.setContent {
val scope = LazyListWrapperScope(state)
view.listState = state
scope.content()
}
} }}
/**
* @author uwhy
*/
private class LazyListWrapperLayout : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
@Retention(AnnotationRetention.SOURCE)
@IntDef(HORIZONTAL, VERTICAL, UNSET)
annotation class OrientationMode {
companion object {
const val HORIZONTAL: Int = 0
const val VERTICAL: Int = 1
const val UNSET: Int = -1
}
}
@OrientationMode
private var orientation: Int = UNSET
var listState: LazyListState? = null
val content = ComposeView(context = context)
private var traditionalParent: ViewParent? = null
init {
addView(content, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
searchTraditionalParent()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
traditionalParent = null
orientation = UNSET
}
private fun searchTraditionalParent() {
var tempParent: ViewParent = parent
while (tempParent.parent != null && tempParent.parent is View) {
tempParent = tempParent.parent
if (tempParent is AbstractComposeView) continue
if (tempParent is ScrollingView || tempParent is ViewPager) {
orientation = getScrollingViewOrientation(tempParent as View)
traditionalParent = tempParent
return
}
}
}
private fun getScrollingViewOrientation(view: View): Int = when (view) {
is ScrollingView -> if (view.computeHorizontalScrollRange() > 0) HORIZONTAL else if (view.computeVerticalScrollRange() > 0) VERTICAL else UNSET
is ViewPager -> HORIZONTAL
else -> UNSET
}
override fun canScrollHorizontally(direction: Int): Boolean = when (direction) {
-1 -> listState?.canScrollBackward ?: false
1 -> listState?.canScrollForward ?: false
else -> false
}
override fun canScrollVertically(direction: Int): Boolean = when (direction) {
-1 -> listState?.canScrollBackward ?: false
1 -> listState?.canScrollForward ?: false
else -> false
}
private var initialX = 0f
private var initialY = 0f
override fun dispatchTouchEvent(e: MotionEvent?): Boolean {
e ?: return super.dispatchTouchEvent(e)
val scrollParent = traditionalParent ?: return super.dispatchTouchEvent(e)
if (orientation == UNSET) {
val scrollOrientation = getScrollingViewOrientation(scrollParent as View)
if (scrollOrientation == UNSET) return super.dispatchTouchEvent(e)
orientation = scrollOrientation
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
scrollParent.requestDisallowInterceptTouchEvent(true)
return super.dispatchTouchEvent(e)
}
val dx = e.x - initialX
val dy = e.y - initialY
val absoluteDx = dx.absoluteValue
val absoluteDy = dy.absoluteValue
if (orientation == HORIZONTAL && absoluteDx > 0 && absoluteDx > absoluteDy) {
if (canScrollHorizontally(if (dx < 0) 1 else -1))
scrollParent.requestDisallowInterceptTouchEvent(true)
else
scrollParent.requestDisallowInterceptTouchEvent(false)
} else if (orientation == VERTICAL && absoluteDy > 0 && absoluteDx < absoluteDy) {
if (canScrollVertically(if (dy < 0) 1 else -1))
scrollParent.requestDisallowInterceptTouchEvent(true)
else
scrollParent.requestDisallowInterceptTouchEvent(false)
}
return super.dispatchTouchEvent(e)
}
}