优化安卓传统ViewPager2和Compose的LazyList的滑动冲突

608 阅读3分钟

安卓经典老坑,在发布之初,安卓传统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轻松实现。

最终的优化效果如下

GIF 2024-12-1 23-05-32.gif

原理在开头部分已经说了,如果你不想自己实现,只想简单的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)  
    }  
}