Jetpack Compose中的Modifier

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose中的Modifier相关的知识,希望对你有一定的参考价值。

Modifier的基本使用

Modifier修饰符是Jetpack Compose中用来修饰组件的,提供常用的属性,写布局时几乎所有Composable组件的大部分属性都可以用Modifier 来修饰。官方在开发Compose UI时,最初尝试过将所有属性全部以函数参数的形式提供,但是那样太多了,他们也尝试过像Flutter那样的方式,将属性也作为一个组件进行嵌套,但这样又很容易让人感到困惑,所以才诞生了Modifier,将大部分组件常用属性封装成Modifier的形式来提供,哪个组件需要就在哪个组件上应用。我认为Modifier是Compose中最优秀的设计点之一。

@Composable
fun ModifierExample() 
    Box(modifier = Modifier.size(200.dp))  // size同时指定宽高大小
        Box(Modifier.fillMaxSize()  // 填满父空间
            .background(Color.Red))
        Box(Modifier.fillMaxHeight() // 高度填满父空间
            .width(60.dp) 
            .background(Color.Blue))
        Box(Modifier.fillMaxWidth() // 宽度填满父空间
            .height(60.dp)
            .background(Color.Green)
            .align(Alignment.Center))
        Column(Modifier.clickable   // 点击事件 
                .padding(15.dp) // 外间距
                .fillMaxWidth()
                .background(MaterialTheme.colorScheme.primary) // 背景
            	.border(2.dp, Color.Red, RoundedCornerShape(2.dp)) // 边框
            	.padding(8.dp) // 内间距
        ) 
            Text(
                text = "从基线到顶部保持特定距离",
                modifier = Modifier.paddingFromBaseline(top = 35.dp))
            Text(
                text = "offset设置偏移量", 
                modifier = Modifier.offset(x = 14.dp) // 正offset会将元素向右移
            )
         
    

部分Modifier属性只能在特定组件的作用域范围内才能使用,避免了像传统xml布局中的属性那样对自身没有用的属性也能被写出来造成污染。例如 Modifier.matchParentSize() 只有在 Box 组件范围内才能使用:

 Box(modifier = Modifier.size(200.dp)) 
        Text(
            text = "aaa",
            modifier = Modifier
            .align(Alignment.Center)
            .matchParentSize() // matchParentSize 仅在 BoxScope 中可用
        )

观察源码发现 Modifier.matchParentSize()Modifier.align() 被定义在了BoxScope接口的内部,所以只能在Boxlambda中使用,该lambda函数的类型是 @Composable BoxScope.() -> Unit,可见其定义了ReceiverBoxScope

interface BoxScope 
    @Stable
    fun Modifier.align(alignment: Alignment): Modifier
    @Stable
    fun Modifier.matchParentSize(): Modifier

可以在 RowColumn 中使用Modifier.weight,类比传统线性布局中的layout_weight属性,并且仅可在 RowScopeColumnScope 中使用。

@Composable
fun ArtistCard() 
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .size(150.dp)
    ) 
        Image(
            painter = painterResource(id = R.drawable.ic_sky),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.weight(2f) // 占比2/3
        )
        Column(
            modifier = Modifier.weight(1f) // 占比1/3
        ) 
            Text(text = "Hello", style = MaterialTheme.typography.titleSmall)
            Text(text = "Compose", style = MaterialTheme.typography.bodyMedium)
        
    

点击事件相关的Modifier属性:

Column
        Box(Modifier
            .clickable  println("clickable") 
            .size(30.dp)
            .background(Color.Red))
        Box(Modifier
            .size(50.dp)
            .background(Color.Blue)
            .combinedClickable(
                onLongClick =  println("onLongClick") ,
                onDoubleClick =  println("onDoubleClick") ,
                onClick =  println("onClick") 
            ))
        Box(Modifier
            .size(50.dp)
            .background(Color.Green)
            .pointerInput(Unit) 
                detectTapGestures(
                    onDoubleTap =  ,
                    onLongPress =  ,
                    onPress =  ,
                    onTap = )
                detectDragGestures(
                    onDragStart =  ,
                    onDragEnd =  ,
                    onDragCancel =  ,
                    onDrag =  change, dragAmount -> 
                )
            )
    

Modifier的复用

可以通过定义扩展函数复用常用的Modifier属性配置:

fun Modifier.redCircle(): Modifier = clip(CircleShape).background(Color.Red)

使用:

	Column 
        Box(Modifier.size(80.dp).redCircle()) 
    

可以提取和复用同一修饰符实例,并将其传递给可组合项,避免在每一帧重组中创建大量对象:

val reusableModifier = Modifier
    .padding(12.dp)
    .background(Color.Gray)
    
@Composable
fun LoadingWheelAnimation() 
    val animatedState = animateFloatAsState(...)

    LoadingWheel(
        // No allocation, as we're just reusing the same instance
        modifier = reusableModifier,
        animatedState = animatedState.value
    )

提取和复用未限定作用域的修饰符
修饰符可以不限定作用域,也可以将作用域限定为特定可组合项。对于未限定作用域的修饰符,可以从任何可组合项之外提取它们作为简单变量:

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)
    
@Composable
fun AuthorField() 
    HeaderText(
        // ...
        modifier = reusableModifier
    )
    SubtitleText(
        // ...
        modifier = reusableModifier
    )

与延迟布局结合使用时,这尤为有用。在大多数情况下,建议对所有潜在的重要项目使用完全相同的修饰符:

val reusableItemModifier = Modifier
    .padding(bottom = 12.dp)
    .size(216.dp)
    .clip(CircleShape)
    
@Composable
private fun AuthorList(authors: List<Author>) 
    LazyColumn 
        items(authors) 
            AsyncImage(
                // ...
                modifier = reusableItemModifier,
            )
        
    

提取和复用限定作用域的修饰符
在处理作用域限定为特定可组合项的修饰符时,您可以将其提取到尽可能高的级别,并在适当的情况下重复使用:

Column(...) 
    val reusableItemModifier = Modifier
        .padding(bottom = 12.dp)
        .align(Alignment.CenterHorizontally)
        .weight(1f)
    Text1(
        modifier = reusableItemModifier,
        // ...
    )
    Text2(
        modifier = reusableItemModifier
        // ...
    )
    // ...

注意:只能将提取的限定作用域的修饰符传递给限定相同作用域的直接子项
例如:

Column(modifier = Modifier.fillMaxWidth()) 
    // Weight modifier is scoped to the Column composable
    val reusableItemModifier =  Modifier.weight(1f)
    // Weight 可以在这里正常应用因为 Text 是 Column 的一个直接子项
    Text(modifier = reusableItemModifier
        // ...
    )
    Box 
         // Weight 在这里不起作用,因为当前 Text 不是 Column 的直接子项
        Text(modifier = reusableItemModifier
            // ...
        )
    

延长提取Modifier链
您可以通过调用 .then() 函数进一步链接或附加提取的Modifier链:

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

// Append to your reusableModifier
reusableModifier.clickable 

// Append your reusableModifier
otherModifier.then(reusableModifier)

Modifier的分类

Modifier有很多属性,这些属性属于不同类型的Modifier,每种类型的Modifier负责处理一类的功能,就常用的属性而言可以分成LayoutModifierDrawModifier,如size、padding等背后的实现是基于LayoutModifier,而background、border等背后的实现是基于DrawModifier

Modifier的分类如下:

Modifier的自定义

可以利用 Modifier.composed 自定义有状态的 Modifier,例如:

// 显示360度旋转动画
fun Modifier.rotating(duration: Int): Modifier = composed 
    val transition = rememberInfiniteTransition()
    val angleRatio by transition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(duration)
        ))
    graphicsLayer 
        rotationZ = 360f * angleRatio
    

// 点击的时候添加一个边框
fun Modifier.addBorderOnClicked() = composed 
    var width by remember  mutableStateOf(0.dp) 
    when(width) 
        0.dp -> Modifier
        else -> Modifier.border(width, Color.Red)
    .then(
        Modifier
        .padding(5.dp)
        .clickable  width = 1.dp 
    )

使用:

    Column 
        Box(Modifier.size(80.dp).background(Color.Blue).rotating(300))
        Text("aaa", Modifier.addBorderOnClicked())
    

composed… 会使用 工厂函数 创建一个新的 Modifier 对象 , 它会在重组的时候被调用,例如:

val modifier = Modifier.composed  // composed 中必须返回一个Modifier
        var padding by remember  mutableStateOf(8.dp) 
        Modifier
            .padding(padding)
            .clickable  padding = 0.dp   // 点击的时候将padding改成0dp

Column 
        Box(Modifier.background(Color.Red)) 
            Text("aaaaa", modifier)
        
        Box(Modifier.background(Color.Blue)) 
            Text("bbbbbbbbb", modifier)
         

composed与普通Modifier属性的区别是其状态是独享的在重组运行时才生效,因为其factory参数是一个Composable函数 @Composable Modifier.() -> Modifier,所以在…中可以使用remember,可以把它当成一个Composable组件。例如上面代码运行后点击其中一个Box的padding变成0dp,但是此时另一个Box的padding不会发生变化,作为对比可以运行如下代码:

	// 这样写下面两个组件会共享这个padding, 点击的时候会同时paddinng变成0
    var padding by remember  mutableStateOf(8.dp) 
    val modifier = Modifier.padding(padding).clickable  padding = 0.dp 
    Column 
        Box(Modifier.background(Color.Red)) 
            Text("aaaaa", modifier)
        
        Box(Modifier.background(Color.Blue)) 
            Text("bbbbbbbbb", modifier)
         
    

composed的主要作用还是为了重用Modifier,延时使用

还可以利用 Modifier.layout() 自定义一些布局相关的属性,如组件的位置偏移、大小限制、或者padding等。

例如:

// 自定义类似Modifier.offset()类似的效果
fun Modifier.myOffset(x : Dp = 0.dp, y : Dp = 0.dp) = layout  measurable, constraints ->
    val placeable = measurable.measure(constraints)
    layout(placeable.width, placeable.height) 
        placeable.placeRelative(x.roundToPx(), y.roundToPx()) //设置偏移 支持RTL
        // placeable.place(0, 0) // 不支持RTL使用这个即可
    

// 使用:
@Composable
fun LayoutModifierExample() 
    Box(Modifier.background(Color.Red)) 
        Text(text = "Offset", Modifier.myOffset(5.dp))
    

// 自定义和Modifier.padding()类似的效果
fun Modifier.myPadding(myPadding : Dp) = layout  measurable, constraints ->
    val padding = myPadding.roundToPx()
    val placeable = measurable.measure(constraints.copy(
        maxWidth = constraints.maxWidth - padding * 2,
        maxHeight = constraints.maxHeight - padding * 2
    ))
    val width =  placeable.width + padding * 2
    val height = placeable.height + padding * 2
    layout(width, height) 
        placeable.placeRelative(padding, padding)
    

// 使用:
@Composable
fun LayoutModifierExample3() 
    Box(Modifier.background(Color.Green)) 
        Text(text = "padding", Modifier.myPadding(10.dp))
    

// 自定义和Modifier.paddingFromBaseline()类似的效果
fun Modifier.paddingBaslineToTop(padding : Dp = 0.dp) = layout  measurable, constraints ->
    val placeable = measurable.measure(constraints)
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline] // 基线高度
    val paddingTop = padding.roundToPx() - firstBaseline // [设置的基线到顶部的距离] - [基线的高度]
    // 仅改变高度为加上paddingTop
    layout(placeable.width, placeable.height + paddingTop) 
        placeable.placeRelative(0, paddingTop) // y坐标向下偏移paddingTop
    

// 使用:
@Composable
fun LayoutModifierExample4() 
    Box(Modifier.background(Color.Green)) 
        Text(text = "paddingFromBaseline", Modifier.paddingBaslineToTop(25.dp))
    

类似的我们也可以尝试模仿DrawModifier的相关属性自己写出类似的东西。

利用modifierElementOf进行自定义,例如:

@OptIn(ExperimentalComposeUiApi::class)
class Circle(var color: Color) : DrawModifierNode, Modifier.Node() 
    override fun ContentDrawScope.draw() 
        drawCircle(color)
    

@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.circle(color: Color) = this then modifierElementOf(
    key = color,
    create =  Circle(color) ,
    update =  it.color = color ,
    definitions = 
        name = "circle"
        properties["color"] = color
    
)
@Preview
@Composable
fun ModifierElementOfExample() 
    Box(Modifier.size(100.dp).circle(Color.Red))

@ExperimentalComposeUiApi
class VerticalOffset(var padding: Dp) : LayoutModifierNode, Modifier.Node() 
    override fun MeasureScope.measure(
        measurableJetpack Compose 从入门到入门

Jetpack Compose 从入门到入门

Jetpack Compose - Modifier入门篇

Jetpack Compose中的列表

Jetpack Compose 从入门到入门

Jetpack Compose 从入门到入门