Jetpack Compose - 修饰符的顺序

36

文档说修饰符是从左边开始应用的。 但从这个例子来看,它们似乎是从右边开始应用的: 首先是边框,然后是内边距,因为文本和边框之间没有空格。

Text("Hi there!", Modifier.padding(10.dp).border(2.dp, Color.Magenta))

输入图片描述


1
但是如果我看一下截图,我看到有10个像素的内边距,然后是2个像素的边框,听起来是从左到右的。 - Hakanai
5个回答

104

有一个Jetpack Compose中的布局代码实验室,其中包含底层的布局修饰符步骤,解释了修改器顺序,请参见“顺序很重要”部分。

当链接修改器时,顺序很重要,因为它们从左到右应用于修改的可组合项,这意味着左侧的修改器的测量和布局将影响右侧的修改器。可组合项的最终大小取决于传递的所有修改器参数。首先,修改器会从左到右更新约束,然后,它们会从右到左返回大小

为了更好地理解,我建议先了解Compose中的布局是如何工作的。简而言之,padding()是一个LayoutModifer,它接收一些约束条件,根据该约束条件的投影测量其子大小,并将子放置在某些坐标上。

让我们看一个例子:

Box(
  modifier = Modifier
    .border(1.dp, Color.Red)
    .size(32.dp)
    .padding(8.dp)
    .border(1.dp, Color.Blue)
)

结果如下:

enter image description here

但是让我们交换.size().padding()

Box(
  modifier = Modifier
    .border(1.dp, Color.Red)
    .padding(8.dp)
    .size(32.dp)
    .border(1.dp, Color.Blue)
)

现在我们有了不同的结果:

enter image description here

我希望这个示例能帮助你理解修饰符是如何应用的。
人们可以期望红色边框应该是最靠近框的,因为它是首先添加的,所以顺序似乎是相反的,但这样的顺序也有优点。让我们来看看这个组合:
@Composable
fun MyFancyButton(modifier: Modifier = Modifier) {
  Text(
    text = "Ok",
    modifier = modifier
      .clickable(onClick = { /*do something*/ })
      .background(Color.Blue, RoundedCornerShape(4.dp))
      .padding(8.dp)
  )
}

通过将modifier移动到参数中,可组合项允许其父级添加其他修饰符,例如额外的边距。因为最后添加的修饰符最接近按钮、边框和内部填充,所以不会影响它们。


4
哇,不仅订单很重要,而且可以多次调用border()来创建多个边框,这很有趣。 - ericn
@Valeriy 很好的解释!我从这个答案中理解的比最近的谷歌视频还要多。你有没有写过关于这个问题的博客呢? :D - Sarthak Mittal

14
在Android Compose中,Image从外层向中心的Composable构建。 这意味着最先定义的绿色边框是外边框,最后定义的红色边框是内边框。 这非常令人困惑,因为在代码中最靠近Text Composable的Green Modifier在结果中离它最远。 这与SwiftUI相反,在SwiftUI中,Modifier在代码和结果图像中都以相同的顺序出现。 在代码中最接近Composable的Modifier也会在结果图像中最接近它。 如果你想象一下结果图像是从你的Composable所在的中心开始构建的(就像在SwiftUI中),那么Modifier的应用顺序与它们给定的顺序相反(从底部向上)。 因此,如果你有一个带有两个边框修饰符的Text Composable - 在代码中离Text Composable最远的边框修饰符(底部的红色修饰符) - 将成为结果图像中距Text Composable最近的修饰符 修饰符从外向内层应用 - 将.border(2.dp, Color.Green) 应用于最外层 - .padding(50.dp) 向内移动 - 将.border(2.dp, Color.Red) 应用于最内层
package com.example.myapplication

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.dp

class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      Text("Hi there!",
        Modifier
          .border(2.dp, Color.Green)
          .padding(50.dp)
          .border(2.dp, Color.Red)
      )
    }
  }
}

enter image description here


1
这是一个不直观的API。你期望的是首先将边框应用于内容,然后再应用第二个边框到填充的内容。 - Farid

6

修改器(Modifier)允许我们自定义组合的外观。 使用它,您可以:

  • 更改组合的外观、大小、偏移量、填充或边缘
  • 添加交互,如使元素可点击、可滚动、可拖动或可缩放
  • 更改其比例、在屏幕中的位置,同时其布局完全不同,或者更改形状以改变其触摸区域。

根据您放置这些修改器的顺序,您的组合的视觉和行为结构会被塑造。

大多数的修改器从上到下或从左到右应用。 Modifier.pointerInput()是一个例外,它默认情况下从右往左或从下往上应用。

Modifier.padding()

Jetpack Compose 中的 Modifier.padding() 根据顺序作为填充或边距。

Modifier.padding(10.dp).size(200.dp) 在设置大小之前添加空白,你将得到一个带有200.dp大小的组合。

Modifier.size(200.dp).padding(10.dp) 在设置 10.dp 的内边距后,添加了填充,使得你的组合宽度和高度都是180.dp

Box(
    Modifier
        .border(2.dp, Color.Green)
        .padding(10.dp)
        .border(2.dp, Color.Red)
        .size(200.dp)
)

Box(
    Modifier
        .border(2.dp, Color.Cyan)
        .size(200.dp)
        .padding(10.dp)
        .border(2.dp, Color.Magenta)
)

enter image description here

而且填充修饰符是累加的。Modifier.padding(20.dp).padding(20.dp)相当于40.dp。

enter image description here

Box(
    Modifier
        .border(2.dp, Color.Green)
        .padding(20.dp)
        .border(2.dp, Color.Red)
        .size(200.dp)
)

Box(
    Modifier
        .border(2.dp, Color.Green)
        .padding(20.dp)
        .padding(20.dp)
        .border(2.dp, Color.Red)
        .size(200.dp)
)

Modifier.shadow()

另一个修改器可以根据应用的顺序改变 Composable 的外观。如果要将阴影应用于 Composable 的外部,则应在背景或其他修饰符之前应用它。如果您在 Modifier.background 之后应用它,则可以获得外部阴影。

Box(
    Modifier
        .shadow(5.dp, RoundedCornerShape(8.dp))
        .background(Color.White)
        .size(100.dp)
)

Box(
    Modifier
        .background(Color.White)
        .size(100.dp)
        .shadow(5.dp, RoundedCornerShape(8.dp))
) 

enter image description here

Modifier.clip()

此修饰符还根据其放置顺序剪裁其他修饰符。使用此修饰符的好处是,如果您将其放置在 Modifier.clickable{} 之前,可以更改或剪切可组合项的可点击区域。使用此修饰符和 Shape,可以创建圆形、三角形或钻石圆形区域或在布局前/后创建这些图形。

它是底层的 Modifier.graphicsLayer{} ,您可以查看我关于 此处, 此处此处 的详细答案。它可以帮助您使用比例、形状、剪切、平移和其他很酷的属性创建复杂的布局。

enter image description here

Modifier.offset()

这个修饰符用于在布局后更改 Composable 的位置,与 Modifier.padding 改变此修饰符的值不会改变 Composable 相对于其兄弟 Composable 的位置不同。但是,根据您设置 Modifier.offset 的位置,可以更改 Composable 的触摸区域,并且它有两个变体。一个采用 lambda defers state read,建议使用谷歌的这个,而不是采用值。

我使用了带有值的示例。您可以看到,如果首先应用偏移量,则随着滑块更改值,跟随偏移量的第一个修饰符将移动。在第二个示例中,由于在 Modifier.offset{} 之前应用了 Modifier.clickable{},因此 Composable 的触摸区域没有更改

var offset by remember {
    mutableStateOf(0f)
}
Box(
    Modifier
        .offset(x = offset.dp)
        .clickable {}
        .background(Color.Red)
        .size(100.dp)
)

Box(
    Modifier
        .clickable {}
        .offset(x = offset.dp)
        .background(Color.Red)
        .size(100.dp)
)

Slider(value = offset, onValueChange = { offset = it }, valueRange = 0f..200f)

enter image description here

Modifier.pointerInput(keys)

这个修饰符是手势和触摸事件的基础。使用它可以调用拖动、轻击、按压、双击、缩放、旋转和许多手势。在这个答案中,解释了如何使用它来构建View系统的onTouchEvent对应项。

与上面的修饰符不同,它默认从底部向上传播,除非你消耗PointerInputChange。在Compose手势系统中,消耗连续事件会取消下一个接收它的事件。因此,当你缩放图像时,可以防止出现滚动等手势。

Modifier
   .pointerInput() // Second one that is invoked
   .pointerInput() // First one that is invoked

4

首个padding就像该元素的margin一样。

比较这些组合,您将看到差异。

@Composable
fun Example() {
    // Default
    Box(modifier = Modifier.background(Color.Cyan), alignment = Alignment.Center){
        Text("Hi there!", Modifier.border(2.dp, Color.Magenta))
    }
    Divider()
    // 10dp margin
    Box(modifier = Modifier.background(Color.Cyan), alignment = Alignment.Center){
        Text("Hi there!", Modifier.padding(10.dp).border(2.dp, Color.Magenta))
    }
    Divider()
    // 10dp margin and 10dp padding
    Box(modifier = Modifier.background(Color.Cyan), alignment = Alignment.Center){
        Text("Hi there!", Modifier.padding(10.dp).border(2.dp, Color.Magenta).padding(10.dp))
    }
}

Render of given Example


3
“使用 then 可以组合修饰元素。顺序很重要;首先出现的修饰元素将首先应用。” @这里 “它先应用于外层,具有 10.dp 的填充,然后是带有 color.Magenta 的边框等等(“从左到右”)。80.dp 填充最后应用于内层。”
@Composable
fun test() {
    Text("Hi there!",
            Modifier.background(color = Color.Green)
                    .padding(10.dp)
                    .border(2.dp, Color.Magenta)
                    .padding(30.dp)
                    .border(2.dp, Color.Red)
                    .padding(80.dp)
    )
}

enter image description here


.padding(10.dp) 在代码中与 Text 最接近,但在图像中最远。这是不合逻辑的。.padding(80.dp) 在代码中与 Text 最远,但在图像中最接近 Text。这是不合逻辑的。但从这个结论可以得出,修饰符实际上是按相反的顺序应用的:在代码中最远的先被应用。 - ivoronline
1
所以当我说修饰符是应用在相反的方向上时,我的意思是最靠近代码文本的修饰符在生成的图像中距离更远。图像与代码方向相反 - 因此修饰符是应用在相反的方向上。在代码中,品红色边框在红色边框之前被定义,并且更接近文本。但在生成的图像中,情况正好相反:红色边框更接近文本,尽管它是在品红色边框之后定义的。 - ivoronline
3
在SwiftUI中的等效代码产生了相反的结果。在两种代码和图像中,品红色边框会更靠近文本。这是因为每个修饰符都返回一个新的修改视图。因此,修饰符实际上是按照它们定义的方向使用的。你首先从文本视图开始。然后应用品红色修饰符,它返回一个带有品红色边框的新视图。然后对该视图应用填充和红色边框,使红色边框成为外部。在代码和结果图像中,一切都很合乎逻辑,并且自然地流向同一个方向。 - ivoronline
@ivoronline 我曾经使用过SwiftUI,所有的API都很直观,因为它是由理智的人创建的,而不是谷歌员工。 - Farid
首先出现的修饰符元素将首先被应用。你可能是想表达“首先出现的修饰符元素将最后被应用”,因为我们直觉上是从内层到外层进行操作。但是谷歌通过这些API破坏了这种直觉。 - Farid
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接