安卓Compose实现事件分发

2,377 阅读4分钟

在使用Compose开发过程中,经常会遇到涉及事件分发的场景。例如布局嵌套,以及两个Compose组件重叠。

下面分开来讲:

  • 布局嵌套事件分发

    下面看看在Compose实现一次点击事件监听的写法:

    Modifier.pointerInput(Unit) {
    	awaitPointerEventScope {
    		awaitPointerEvent(PointerEventPass.Initial)
    		Log.d("TAG", "点击")
    	}
    }
    

    awaitPointerEventScope是一个手势事件的作用域,awaitPointerEvent是一个挂起函数,这里传入了一个参数:PointerEventPass,而这个参数是控制事件分发顺序的关键。PointerEventPass是一个枚举类,主要有三个值,Initial, Main, Final。在Compose嵌套布局中,每个组件都设置awaitPointerEvent并且传入PointerEventPass就可以控制分发的顺序了。

    分发顺序:

    PointerEventPass.Initial -> PointerEventPass.Main -> PointerEventPass.Final

    下面写段代码验证一下,

    @Composable
    fun BoxDemo() {
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier
                .fillMaxSize()
                .background(Color.Yellow)
                .pointerInput(Unit) {
                    awaitPointerEventScope {
                        awaitPointerEvent(PointerEventPass.Initial)
                        Log.d("TAG", "第一层")
                    }
                }
        ) {
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .size(400.dp)
                    .background(Color.Blue)
                    .pointerInput(Unit) {
                        awaitPointerEventScope {
                            awaitPointerEvent(PointerEventPass.Final)
                            Log.d("TAG", "第二层")
                        }
                    }
            ) {
                Box(
                    Modifier
                        .size(200.dp)
                        .background(Color.Green)
                        .pointerInput(Unit) {
                            awaitPointerEventScope {
                                awaitPointerEvent(PointerEventPass.Main)
                                Log.d("TAG", "第三层")
                            }
                        }
                )
            }
        }
    }
    

    可以看到有3个Box进行了嵌套,点击之后输出:

    14:20:08.411  D  第一层
    14:20:08.412  D  第三层
    14:20:08.412  D  第二层
    

    这个顺序刚好是对应了:

    PointerEventPass.Initial -> PointerEventPass.Main -> PointerEventPass.Final

    另外说明一下,使用上面的代码只能监听一次点击,因为awaitPointerEventScope只响应一次点击事件,如果需要监听每一次的点击事件,只需要把awaitPointerEventScope替换成awaitEachGesture就可以了。

  • 组件重叠

    如果两个组件出现了重叠,并且只需要响应其中一个组件的点击事件,只需要设置其中一个组件的Modifier.clickable即可监听。又或者,2个组件不完全重叠,只需要响应点击到的组件就行,这个时候两个组件设置Modifier.clickable也都是可以的,只要点击到就能监听到。现在出现了另外几个场景,A组件在底部,B组件在上面,A、B组件都设置了Modifier.clickable,布局方式有:A组件和B组件完全重叠;A组件小于B组件并且被B组件完全覆盖了;B组件覆盖了A组件部分位置。需求是在这几种布局方式中,某些条件下点击B组件的位置需要响应A组件的点击事件。一般情况下,两个组件有重叠区域,并且都设置了Modifier.clickable,那么在上面的组件,会把点击事件消费,从而底部的组件不会有任何事件响应。这个时候需要用到Modifier的一个拓展函数pointerInteropFilter,拦截过滤事件,达到事件分发的作用。

    pointerInteropFilter函数源码如下:

    @ExperimentalComposeUiApi
    fun Modifier.pointerInteropFilter(
        requestDisallowInterceptTouchEvent: (RequestDisallowInterceptTouchEvent)? = null,
        onTouchEvent: (MotionEvent) -> Boolean
    ): Modifier = composed(
        inspectorInfo = debugInspectorInfo {
            name = "pointerInteropFilter"
            properties["requestDisallowInterceptTouchEvent"] = requestDisallowInterceptTouchEvent
            properties["onTouchEvent"] = onTouchEvent
        }
    ) {
        val filter = remember { PointerInteropFilter() }
        filter.onTouchEvent = onTouchEvent
        filter.requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent
        filter
    }
    

    可以看到是一个实验的函数,需要在组件外部添加:@OptIn(ExperimentalComposeUiApi::class)

    如下:

    @OptIn(ExperimentalComposeUiApi::class)
    @Composable
    fun BoxDemo2() { }
    

    参数解释:

    onTouchEvent:

    具有类似于返回类型的 View.onTouchEvent布尔返回类型。如果提供的 onTouchEvent 返回 true,它将继续接收事件流(除非事件流已被截获),如果返回 false,则不会。

    requestDisallowInterceptTouchEvent

    是一个 lambda,您可以选择提供它,以便以后可以调用它(是的,在这种情况下,您调用您提供的 lambda),这类似于调用 ViewParent.requestDisallowInterceptTouchEvent。调用此值时,树中遵守合约的任何关联祖先都会采取相应的行动,不会拦截偶数流

    这里只介绍onTouchEvent参数,这个onTouchEvent和View系的事件分发的onTouchEvent是一样的,返回true,表示消费了事件,不会继续往下分发,false表示继续传递。

    如果两个组件都设置了pointerInteropFilter,并且onTouchEvent都返回true,那么这2个组件不会有任何的事件监听响应。代码如下:

    @OptIn(ExperimentalComposeUiApi::class)
    @Composable
    fun BoxDemo2() {
        Box {
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Yellow)
                    .pointerInteropFilter {
                        true
                    }
                    .clickable {
                        Log.d("TAG", "A组件")
                    }
            ) {
    
            }
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .size(400.dp)
                    .background(Color.Blue)
                    .pointerInteropFilter {
                        true
                    }
                    .clickable {
                        Log.d("TAG", "B组件")
                    }
            ) {
    
            }
        }
    }
    

    那么可以知道,如果组件中设置了pointerInteropFilter,onTouchEvent返回true将会表示本次事件已经被消费了,设置的clickable不会被响应,也不会继续分发点击事件。

    在A组件铺满屏幕,B组件覆盖了A组件400.dp大小的位置的布局中,A、B组件都设置clickable,某些条件下,希望点击B组件那一块的位置,A组件会得到事件响应,B组件不响应,那么B组件可以设置Modifier.pointerInteropFilter { false },如果有其它的条件判断可以返回不同值。

    @OptIn(ExperimentalComposeUiApi::class)
    @Composable
    fun BoxDemo2() {
        Box {
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .fillMaxSize()
                    .background(Color.Yellow)
                    .clickable {
                        Log.d("TAG", "A组件")
                    }
            ) {
    
            }
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .size(400.dp)
                    .background(Color.Blue)
                    .pointerInteropFilter {
                        false
                    }
                    .clickable {
                        Log.d("TAG", "B组件")
                    }
            ) {
    
            }
        }
    }
    

    输出:

    15:18:05.889 D A组件

    点击了B组件,事件没有被消费继续分发到A组件响应。