Jetpack Compose - PointerInputModifier,ParentDataModifier,SemanticsModifier(十四)

1,278 阅读5分钟

PointerInputModifier

先来看下在Compose中怎么做 长按事件的监听

Box(
    modifier = Modifier.size(40.dp).background(Color.Blue).combinedClickable(
        onDoubleClick = {
           println("onDoubleClick")
        }, onLongClick = {
            println("onLongClick")
        }
    ){
        println("onClick")
    }
)

Box(
    modifier = Modifier.size(20.dp).background(Color.Blue).pointerInput(Unit) {
        detectTapGestures(onDoubleTap = {
            println("onDoubleTap")
        }, onTap = {
            println("onTap")
        }, onLongPress = {
            println("onLongPress")
        })
    }
)

一共有两种方式来实现,一般来说 大家用第一种写法比较多,第二种写法较少, 第二种写法中的pointerInput 主要是用来 写自定义触摸反馈事件的,

我们可以点进去看一下这个detect方法里面的实现

image.png

注意看上图中红框中的写法,这在compose的自定义触摸反馈事件的写法中是标准写法

有点扯远了,还是回到主题,我们还是从pointerInput这个方法下手

image.png

image.png

看到这里 就是要考虑一下,这个PointerInputModifier是怎么处理的,那么显然 我们还是得回到LayoutNode里面去看

image.png

你会发现这个PointerInput和DrawModifier 是一样的处理逻辑,

换句话说,PointerInputModifer 影响的是 右边的LayoutModifier

当存在多个pointerInput的时候,左边的是右边的 父触摸事件

ParentDataModifier

来看一下 下面这个布局

Row() {

    Box(
        Modifier
            .background(Color.Red)
            .size(50.dp))
    Box(
        Modifier
            .weight(1f).height(50.dp)
            .background(Color.Blue)
    )
    Box(
        Modifier
            .background(Color.Yellow)
            .size(50.dp))

}

image.png

这在传统view中 很好实现,要么相对布局,要么线性布局加weight, 在Compose中 我们没有相对布局,但是有weight 所以在用法上 和传统view 其实是保持了一致

本质上这种Modifier是ParentDataModifier

image.png

他和普通的LayoutModifier 的区别是,普通的LayoutModifier是用来决定自己测量大小的, 而Parent 是用来告诉父view 我要多大的, 就好像这个weight的属性一样,你自己的weight 最终可以多大,是你自己能决定的吗? 当然不是了,具体还要看 和我同级别的 其他view的weight属性值,然后 让我们的父view 来最终决定 我们每个人的大小 所以名字里也带有一个ParentData

除了weight以外,还有一个layoutId的方法 ,背后也是ParentData 在起作用,通常而言,layoutId 用来写自定义Layout的时候 会非常有用

除此之外,我们还可以得知,既然parentData 是给父view 用的,所以 也并不一定每个view 都能支持这个parentData,比如我们上文中的例子 Row,看下代码

image.png

之所以能用这个weight,是因为Row实现了 该parentData,同样的 如果你这个父view 如果没有实现对应的parentData 则不能调用 对应的方法

如果是我们自己的自定义view呢? 该怎么使用parentdata?

其实非常简单,第一步就是 直接获取parentData 然后做强转即可了,

@Composable
fun CustomLayout(modifier: Modifier,content:@Composable ()->Unit){
    Layout(content, modifier){ measureable,constraints->
        measureable.forEach {
            // 通过parentData的方式 来拿到 我们自定义的CustomData
            val data = it.parentData as? CustomData
        }

        layout(100,100){

        }
    }
}

data class CustomData(val weight: Float = 0f, val weightString: String = "")

第二步就是对外暴露 可以使用ParentData的函数


fun Modifier.weightData(weight: Float)=then(object :ParentDataModifier{
    /**
     * Provides a parentData, given the [parentData] already provided through the modifier's chain.
     */
    override fun Density.modifyParentData(parentData: Any?): Any? {
        return  if (parentData ==null){
            CustomData(weight)
        }else{
            (parentData as CustomData).copy(weight = weight)
        }
    }

})

fun Modifier.weightStringData(weightStr: String)=then(object :ParentDataModifier{
    /**
     * Provides a parentData, given the [parentData] already provided through the modifier's chain.
     */
    override fun Density.modifyParentData(parentData: Any?): Any? {
        return if (parentData == null) {
            CustomData(weightString = weightStr)
        } else {
            (parentData as CustomData).copy(weightString = weightStr)
        }
    }

})

到这,其实基本上就写完了, 唯一要注意的是,我们现在这种写法 还是会有 api污染的,因为这个weightData和weightStringData 对每个view来说 都可以使用它,虽然大部分场景下这样做 没有任何作用,但总归是不好的,

我们要想办法 将这2个方法 限定在CustomLayout 我们这个自定义view的scope中来使用

@LayoutScopeMarker
object CustomLayoutScope{

    fun Modifier.weightData(weight: Float)=then(object :ParentDataModifier{
        /**
         * Provides a parentData, given the [parentData] already provided through the modifier's chain.
         */
        override fun Density.modifyParentData(parentData: Any?): Any? {
            return  if (parentData ==null){
                CustomData(weight)
            }else{
                (parentData as CustomData).copy(weight = weight)
            }
        }

    })

    fun Modifier.weightStringData(weightStr: String)=then(object :ParentDataModifier{
        /**
         * Provides a parentData, given the [parentData] already provided through the modifier's chain.
         */
        override fun Density.modifyParentData(parentData: Any?): Any? {
            return if (parentData == null) {
                CustomData(weightString = weightStr)
            } else {
                (parentData as CustomData).copy(weightString = weightStr)
            }
        }

    })
}
@Composable
fun CustomLayout(modifier: Modifier,content:@Composable CustomLayoutScope.()->Unit){
    Layout({ CustomLayoutScope.content() }, modifier) { measureable, constraints ->
        measureable.forEach {
            // 通过parentData的方式 来拿到 我们自定义的CustomData
            val data = it.parentData as? CustomData
        }

        layout(100,100){

        }
    }
}

SemanticsModifier

这个其实就是Compose中的 无障碍实现

Box(
    Modifier
        .background(Color.Red)
        .size(50.dp).semantics {
            contentDescription = "红色方块"
        })

注意这个semantics是有参数的:

image.png

当这个参数为true的时候, 代表合并自己的内部子组件,并且不被外部组件合并

OnRemeasuredModifier

在Compose中 我们可以监听 一个组件大小的变化

Text(text = "hello world", Modifier.onSizeChanged {
    println("onSizeChanged :$it")
})

可以稍微跟一下代码:

image.png

image.png

OnSizeChangedModifier 与 OnRemeasureModifier最大的区别是啥? 最大的区别就是OnRemeasureModifier 是在每次测量的时候 都会调用,而OnSizeChangedModifier 是尺寸变化的时候才会被调用, 多数情况下,我们用 OnSizeChangedModifier 即可

对于下面这种写法,onRemeasured 也总是响应 它右边的尺寸变化

Modifier
    .padding(150.dp)
    .then(object : OnRemeasuredModifier {
        /**
         * Called after a layout's contents have been remeasured.
         */
        override fun onRemeasured(size: IntSize) {
            println("onRemeasured :$size")
        }
    }).padding(10.dp)) {
    

OnPlacedModifier

与OnRemeasureModifier 对应的就是OnPlacedModifier ,前者类似于传统view 中的 onmeasure测量回调,而后者则 类似于onLayout的 摆放位置的回调

换句话说,如果想拿到某个组件 具体的位置 ,则可以使用该onPlaced函数

Text(text = "hello world",
    Modifier
        .padding(50.dp)
        .onPlaced {
            println("onPlaced ${it.positionInParent()}  ${it.positionInWindow()}")
        })

image.png

可以看下 LayoutCoordinates 具体有哪些扩展函数,可以拿到哪些位置信息

image.png

OnGloballyPositionedModifier

这个Modifier 其实和 前面的 OnPlacedModifier 差不多

Text(text = "hello world",
    Modifier
        .padding(50.dp)
        .onGloballyPositioned {
            println("onPlaced ${it.positionInParent()}  ${it.positionInWindow()}")
        }
        .onPlaced {
            println("onPlaced ${it.positionInParent()}  ${it.positionInWindow()}")
        })

用法上,甚至都是一样的,唯一的区别是 OnGloballyPositionedModifier 触发的时机 比 OnPlacedModifier 要多不少,这个api 较重

举个例子,我们思考如下界面,一个list组件A, 里面有多个item可以滑动,每个item 都是一个组件b, 每个组件b里面 有头像,有昵称 分别是组件c和d

在这个list 组件A 滑动的时候 ,组件c和d的onPlaced 是不会触发的,因为 组件c和d 相对于他们的父组件b 没有发生任何变化, 但是滑动的时候 c和d的 绝对位置 是在不停的变化,所以会触发 onGloballyPositioned的回调, 这个区别搞清楚就没啥难的了