美文网首页Android进阶之路Android技术知识
Android前沿技术学习——Compose布局原理+配置布局+

Android前沿技术学习——Compose布局原理+配置布局+

作者: 码农的地中海 | 来源:发表于2022-07-11 19:24 被阅读0次

    一、简介

    布局系统的 Jetpack Compose 实现有两个主要目标:一是实现高性能,二是让开发者能够轻松编写自定义布局。在 Compose 中,通过避免多次测量布局子级可实现高性能。如果需要进行多次测量,Compose 具有一个特殊系统,即固有特性测量。

    注意:使用 Android View 系统时,在嵌套某些 View(如 RelativeLayout)时,可能会出现一些性能问题。由于 Compose 可以避免多次测量,因此可以根据需要进行深层次嵌套,而不会影响性能。

    布局主要包括:布局基础知识、Material组件和布局、自定义布局、Compose中使用ConstraintLayout。


    image.png

    二、布局基础

    image.png

    可组合函数

    可组合函数是 Compose 的基本构建块,返回值是 Unit 的函数,用于描述界面中的某一部分,该函数可接收参数。
    组合函数中可包含多个界面元素。
    其基本格式为:

    @Composable
    fun xxx():Unit{
    ....
    }
    

    标准布局元素

    在Compose中标准的布局元素有三个:BoxColumnRow

    image.png

    标准布局元素特性示意图

    Box
    重叠布局,类似Android View系统中的FramLayout布局,元素会重叠显示,其原型定义如下:

    @Composable
    inline fun Box(
        modifier: Modifier = Modifier,                   //修饰符
        contentAlignment: Alignment = Alignment.TopStart,//内容的位置
        propagateMinConstraints: Boolean = false,       //是否应将传入的最小约束传递给内容
        content: @Composable BoxScope.() -> Unit    //内容,即界面元素
    ) {
        ......
    }
    

    例子

    /**
     * 标准布局 - Box
     */
    @Composable
    fun BoxExample(){
        Box (
            modifier = Modifier.size(width = 230.dp,height = 100.dp),
            contentAlignment = Alignment.Center
        ){  //对应content参数 lambda表达上
            Text(text = "Text 1")
            Text(text = "Text 2")
        }
    }
    
    image.png

    预览结果

    Column
    垂直布局,在界面元素垂直方向依次排列。源码中定义如下:

    @Composable
    inline fun Column(
        modifier: Modifier = Modifier,          //修饰符
        verticalArrangement: Arrangement.Vertical = Arrangement.Top,  //内容元素垂直方向分布方式
        horizontalAlignment: Alignment.Horizontal = Alignment.Start,    //内容元素水平方向分布方式
        content: @Composable ColumnScope.() -> Unit                      //内部元素
    ) {
        ......
    }
    

    实例

    /**
     * 标准布局 - Column
     */
    @Composable
    fun ColumnExample(){
        Column (
            modifier = Modifier.size(width = 230.dp,height = 100.dp),
            verticalArrangement = Arrangement.Center,//垂直方向居中
            horizontalAlignment = Alignment.End      //水平方向靠右
        ){  //对应content参数 lambda表达上
            Text(text = "Text 111")
            Text(text = "Text 666666")
        }
    }
    
    image.png

    Column示例预览结果

    Row
    元素水平方向分布,源码中定义:

    @Composable
    inline fun Row(
        modifier: Modifier = Modifier,          //修饰符
        horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,//水平方向排列方式
        verticalAlignment: Alignment.Vertical = Alignment.Top,      //垂直方向元素排列方式
        content: @Composable RowScope.() -> Unit          //内部子元素
    ) {
        ......
    }
    

    示例

    /**
     * 标准布局 - Row
     */
    @Composable
    fun RowExample(){
        Row (
            modifier = Modifier.size(width = 230.dp,height = 100.dp),
            horizontalArrangement = Arrangement.SpaceAround,
            verticalAlignment = Alignment.Bottom
        ){  //对应content参数 lambda表达上
            Text(text = "Text 111")
            Text(text = "Text 666666")
        }
    }
    
    image.png

    Row示例预览

    基本组件

    Jetpack Compose中组件库包括:compose-ui和material。很多常用基本组件都是在material库中,Compose-ui中组件很少:Image、BasicTextField(输入框)。
    示例:

    //添加一个图片
    Image(
          //填充内容
          painter = painterResource(id = message.iconId),
          contentDescription = "logo",
          //尺寸及形状
          modifier= Modifier
              .padding(top = 2.dp)
              .size(40.dp)             //图像尺寸
              .clip(CircleShape)       //形状
              .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)//边框样式
      )
    

    布局模型

    在对标准布局元素和组件有了一定了解后,我们来看下Compose的布局流程。
    首先Compose布局是一个界面树,从树的根节点开始依次要求其子节点对自身进行测量,然后递归完成所有子节点的测量,并将约束条件沿着树向下传递给子节点,并将测量的尺寸和放置指令依次向根节点传递。

    以下面可组合函数为示例:

    @Composable
    fun SearchResult(...) {
      Row(...) {
        Image(...)
        Column(...) {
          Text(...)
          Text(..)
        }
      }
    }
    

    其布局过程如下图示:

    image.png

    image.png

    修饰符

    在Compose布局中修饰符至关重要,您可以使用修饰符来修饰或扩充可组合项。常用的修饰符如下:

    • background 可组合元素的背景色;
    • clickable 使可组合项响应用户点击,并显示波纹效果;
    • padding 设置元素周围留出空间;
    • size 可组合元素的尺寸;
    • clip 可组合元素的形状;
    • border 可组合元素的边框及形状;
    • fillMaxSize可组合元素尺寸按父布局的最大尺寸显示;
    • fillMaxWidth 可组合元素宽度按父布局的最大宽度显示;
    • fillMaxHeight 可组合元素高度按父布局的最大高度显示;
      除以上外,还有widthheightwrapConentHeight等,用到的时候可以尝试看看效果。
      下面展示一个圆形图像的示例
    /**
     * 修饰符使用示例
     */
    @Composable
    fun ModifierExample(){
        //添加一个图片
        Image(
            //填充内容
            painter = painterResource(id = R.mipmap.ic_girl),
            contentDescription = "logo",
            //尺寸及形状
            modifier= Modifier
                .padding(top = 2.dp)
                .background(Color.Blue)//设置蓝色背景
                .size(40.dp)             //图像尺寸
                .clip(CircleShape)       //圆形形状
                .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)//边框样式
        )
    }
    

    效果如下:


    image.png

    修饰符示例

    修饰符扩展
    除了使用Compose提供的修饰符外,我们也可以扩展函数自定义修饰符,具体方式可以参考Jetpack Compose布局(三)一文中讲的扩展布局修饰符

    强调:修饰符的使用是有顺序的
    强调:修饰符的使用是有顺序的
    强调:修饰符的使用是有顺序的
    例如,将上例的background和padding顺序调换下,就会如下图效果,仔细看两图的顶部边距,会发现区别

    image.png

    调换background和padding顺序的效果图

    槽位布局

    Compose中提供了固定槽位的可组合项,以简化界面元素,这些主要在androidx.compose.material:material库中,如:DrawerFloatingActionButtonTopAppBar等。
    Material 组件大量使用槽位 API,这是 Compose 引入的一种模式,它在可组合项之上带来一层自定义设置。这种方法使组件变得更加灵活,因为它们接受可以自行配置的子元素,而不必公开子元素的每个配置参数。槽位会在界面中留出空白区域,让开发者按照自己的意愿来填充。如下图TopAppBar的槽位。

    image.png

    三、Material 组件和布局

    Jetpack Compose 提供了 Material Design 的实现,后者是一个用于创建数字化界面的综合设计系统。Material 组件(按钮、卡片、开关等)和布局(如 Scaffold)可作为可组合函数提供。

    image.png

    Material 组件是用于创建界面的交互式构建块。Compose 提供了许多此类组件,开箱即可使用。如需了解提供了哪些组件,请参阅 Compose Material API 参考文档

    Material 组件会使用应用中 MaterialTheme 提供的值:

    @Composable
    fun MyApp() {
        MaterialTheme {
            // Material Components like Button, Card, Switch, etc.
        }
    }
    

    如需详细了解主题,请参阅“Compose 中的主题”指南

    内容槽

    支持内部内容(文本标签、图标等)的 Material 组件往往会提供“槽”(即接受可组合内容的通用 lambda),而且还会提供尺寸和内边距等公共常量,从而支持设置内部内容的布局,使之符合 Material 规范。

    例如 Button

    Button(
        onClick = { /* ... */ },
        // Uses ButtonDefaults.ContentPadding by default
        contentPadding = PaddingValues(
            start = 20.dp,
            top = 12.dp,
            end = 20.dp,
            bottom = 12.dp
        )
    ) {
        // Inner content including an icon and a text label
        Icon(
            Icons.Filled.Favorite,
            contentDescription = "Favorite",
            modifier = Modifier.size(ButtonDefaults.IconSize)
        )
        Spacer(Modifier.size(ButtonDefaults.IconSpacing))
        Text("Like")
    }
    
    
    image.png

    图 1. 使用 content 槽和默认内边距的 Button(左),以及使用提供自定义 contentPaddingcontent 槽的 Button(右)。

    Button 有一个通用 content 尾随 lambda 槽,该槽使用 RowScope 将内容可组合项的布局设为行。此外,它还有一个 contentPadding 参数,用于将内边距应用于内部内容。您可以使用通过 ButtonDefaults 提供的常量,也可以使用自定义值。

    再比如 ExtendedFloatingActionButton

    ExtendedFloatingActionButton(
        onClick = { /* ... */ },
        icon = {
            Icon(
                Icons.Filled.Favorite,
                contentDescription = "Favorite"
            )
        },
        text = { Text("Like") }
    )
    
    image.png

    图 2. 使用 icon 槽和 text 槽的 ExtendedFloatingActionButton

    ExtendedFloatingActionButton 有两个槽,分别针对 icontext 标签,而没有通用 content lambda。虽然每个槽都支持通用的可组合内容,但该组件会自行判断这些内部内容的布局方式。它会在内部处理内边距、对齐方式和大小。

    Scaffold

    Compose 提供了便捷的布局,用于将 Material 组件组合成常见的屏幕图案。可组合项(例如 Scaffold)提供了适用于各种组件和其他屏幕元素的槽。

    屏幕内容

    Scaffold 有一个通用 content 尾随 lambda 槽。lambda 会收到应该应用于内容根目录(例如,通过 Modifier.padding)的 PaddingValues 实例,以便偏移顶部栏和底部栏(如果存在的话)。

    Scaffold(/* ... */) { contentPadding ->
        // Screen content
        Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
    }
    

    应用栏

    Scaffold顶部应用栏底部应用栏提供了槽。系统将在内部处理可组合项的放置位置。

    image.png

    您可以使用 topBar 槽和 TopAppBar

    Scaffold(
        topBar = {
            TopAppBar { /* Top app bar content */ }
        }
    ) {
        // Screen content
    }
    
    image.png

    您可以使用 bottomBar 槽和 BottomAppBar

    Scaffold(
        bottomBar = {
            BottomAppBar { /* Bottom app bar content */ }
        }
    ) {
        // Screen content
    }
    

    这些槽可用于 BottomNavigation 等其他 Material 组件。 此外,您还可以使用自定义可组合项 - 例如,查看 Owl 示例中的初始配置屏幕

    悬浮操作按钮

    image.png

    Scaffold悬浮操作按钮提供了槽。

    您可以使用 floatingActionButton 槽和 FloatingActionButton

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { /* ... */ }) {
                /* FAB content */
            }
        }
    ) {
        // Screen content
    }
    

    注意floatingActionButton 槽接受任何可组合内容。例如,您可以将展开式悬浮操作按钮ExtendedFloatingActionButton 结合使用。

    系统将在内部处理 FAB 可组合项的底部放置位置。您可以使用 floatingActionButtonPosition 参数来调整水平位置:

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { /* ... */ }) {
                /* FAB content */
            }
        },
        // Defaults to FabPosition.End
        floatingActionButtonPosition = FabPosition.Center
    ) {
        // Screen content
    }
    

    如果您使用的是 Scaffold 可组合项的 bottomBar 槽,则可以使用 isFloatingActionButtonDocked 参数将悬浮操作按钮与底部应用栏重叠:

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { /* ... */ }) {
                /* FAB content */
            }
        },
        // Defaults to false
        isFloatingActionButtonDocked = true,
        bottomBar = {
            BottomAppBar { /* Bottom app bar content */ }
        }
    ) {
        // Screen content
    }
    
    image.png

    图 3. 使用 floatingActionButton 槽和 bottomBar 槽的 ScaffoldisFloatingActionButtonDocked 参数设为 false(顶部)和 true(底部)。

    BottomAppBar 支持带有 cutoutShape 参数的 FAB 刘海屏,它接受任何 Shape。最好提供停靠组件所使用的同一 Shape。例如,FloatingActionButton 使用 MaterialTheme.shapes.small,并将 50% 的边角大小作为其 shape 参数的默认值:

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { /* ... */ }) {
                /* FAB content */
            }
        },
        isFloatingActionButtonDocked = true,
        bottomBar = {
            BottomAppBar(
                // Defaults to null, that is, No cutout
                cutoutShape = MaterialTheme.shapes.small.copy(
                    CornerSize(percent = 50)
                )
            ) {
                /* Bottom app bar content */
            }
        }
    ) {
      // Screen content
    }
    

    [图片上传失败...(image-4686ad-1657537998502)]

    图 4. 具有 BottomAppBar 和停靠 FloatingActionButtonScaffoldBottomAppBar 的自定义 cutoutShapeFloatingActionButton 所使用的 Shape 一致。

    信息提示控件

    image.png

    Scaffold 提供了一种显示信息提示控件的方式。

    这是通过 ScaffoldState 提供的,其中包含一个 SnackbarHostState 属性。您可以使用 rememberScaffoldState 创建一个 ScaffoldState 实例,并通过 scaffoldState 参数将其传递给 ScaffoldSnackbarHostState 可提供对 showSnackbar 函数的访问权限。该挂起函数需要 CoroutineScope(例如,使用 rememberCoroutineScope),并可被调用以响应界面事件,从而在 Scaffold 中显示 Snackbar

    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    Scaffold(
        scaffoldState = scaffoldState,
        floatingActionButton = {
            ExtendedFloatingActionButton(
                text = { Text("Show snackbar") },
                onClick = {
                    scope.launch {
                        scaffoldState.snackbarHostState
                            .showSnackbar("Snackbar")
                    }
                }
            )
        }
    ) {
        // Screen content
    }
    

    您可以提供可选操作,并调整 Snackbar 的时长。snackbarHostState.showSnackbar 函数可接受额外的 actionLabelduration 参数,并返回 SnackbarResult

    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    Scaffold(
        scaffoldState = scaffoldState,
        floatingActionButton = {
            ExtendedFloatingActionButton(
                text = { Text("Show snackbar") },
                onClick = {
                    scope.launch {
                        val result = scaffoldState.snackbarHostState
                            .showSnackbar(
                                message = "Snackbar",
                                actionLabel = "Action",
                                // Defaults to SnackbarDuration.Short
                                duration = SnackbarDuration.Indefinite
                            )
                        when (result) {
                            SnackbarResult.ActionPerformed -> {
                                /* Handle snackbar action performed */
                            }
                            SnackbarResult.Dismissed -> {
                                /* Handle snackbar dismissed */
                            }
                        }
                    }
                }
            )
        }
    ) {
        // Screen content
    }
    

    您可以使用 snackbarHost 参数提供自定义 Snackbar。如需了解详情,请参阅 SnackbarHost API reference docs

    抽屉式导航栏

    image.png

    Scaffold模态抽屉式导航栏提供了槽。系统将在内部处理可组合项的可拖动动作条和布局。

    您可以使用 drawerContent 槽,该槽使用 ColumnScope 将抽屉式导航栏内容可组合项的布局设为列:

    Scaffold(
        drawerContent = {
            Text("Drawer title", modifier = Modifier.padding(16.dp))
            Divider()
            // Drawer items
        }
    ) {
        // Screen content
    }
    

    Scaffold 接受一些额外的抽屉式导航栏参数。例如,您可以使用 drawerGesturesEnabled 参数来切换抽屉式导航栏是否响应拖动:

    Scaffold(
        drawerContent = {
            // Drawer content
        },
        // Defaults to true
        drawerGesturesEnabled = false
    ) {
        // Screen content
    }
    

    您可以通过 ScaffoldState 完成以编程方式打开和关闭抽屉式导航栏的操作,其中包含一个 DrawerState 属性,该属性应使用 scaffoldState 参数传递给 ScaffoldDrawerState 可提供对 openclose 函数的访问权限,以及对与当前抽屉式导航栏状态相关的属性的访问权限。这些挂起函数需要 CoroutineScope(例如,使用 rememberCoroutineScope),并且可被调用以响应界面事件。

    val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()
    Scaffold(
        scaffoldState = scaffoldState,
        drawerContent = {
            // Drawer content
        },
        floatingActionButton = {
            ExtendedFloatingActionButton(
                text = { Text("Open or close drawer") },
                onClick = {
                    scope.launch {
                        scaffoldState.drawerState.apply {
                            if (isClosed) open() else close()
                        }
                    }
                }
            )
        }
    ) {
        // Screen content
    }
    

    模态抽屉式导航栏

    如果您想实现不含 Scaffold 的模态抽屉式导航栏,可以使用 ModalDrawer 可组合项。它接受与 Scaffold 类似的抽屉式导航栏参数。

    val drawerState = rememberDrawerState(DrawerValue.Closed)
    ModalDrawer(
        drawerState = drawerState,
        drawerContent = {
            // Drawer content
        }
    ) {
        // Screen content
    }
    
    image.png

    如果您要实现底部抽屉式导航栏,可以使用 BottomDrawer 可组合项:

    val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
    BottomDrawer(
        drawerState = drawerState,
        drawerContent = {
            // Drawer content
        }
    ) {
        // Screen content
    }
    

    底部动作条

    image.png

    如果您要实现标准底部动作条,可以使用 BottomSheetScaffold 可组合项。它接受与 Scaffold 类似的参数,例如 topBarfloatingActionButtonsnackbarHost。其中包含额外的参数,这些参数可提供底部动作条的显示方式。

    您可以使用 sheetContent 槽,该槽使用 ColumnScope 将动作条内容可组合项的布局设为列:

    BottomSheetScaffold(
        sheetContent = {
            // Sheet content
        }
    ) {
        // Screen content
    }
    

    BottomSheetScaffold 接受一些额外的动作条参数。例如,您可以使用 sheetPeekHeight 参数设置动作条的窥视高度。此外,您还可以使用 sheetGesturesEnabled 参数来切换抽屉式导航栏是否响应拖动。

    BottomSheetScaffold(
        sheetContent = {
            // Sheet content
        },
        // Defaults to BottomSheetScaffoldDefaults.SheetPeekHeight
        sheetPeekHeight = 128.dp,
        // Defaults to true
        sheetGesturesEnabled = false
    
    ) {
        // Screen content
    }
    

    您可以通过 BottomSheetScaffoldState 完成以编程方式展开和收起动作条的操作,其中包含一个 BottomSheetState 属性。您可以使用 rememberBottomSheetScaffoldState 创建一个 BottomSheetScaffoldState 实例,并通过 scaffoldState 参数将其传递给 BottomSheetScaffoldBottomSheetState 可提供对 expandcollapse 函数的访问权限,以及对与当前动作条状态相关的属性的访问权限。这些挂起函数需要 CoroutineScope(例如,使用 rememberCoroutineScope),并且可被调用以响应界面事件。

    val scaffoldState = rememberBottomSheetScaffoldState()
    val scope = rememberCoroutineScope()
    BottomSheetScaffold(
        scaffoldState = scaffoldState,
        sheetContent = {
            // Sheet content
        },
        floatingActionButton = {
            ExtendedFloatingActionButton(
                text = { Text("Expand or collapse sheet") },
                onClick = {
                    scope.launch {
                        scaffoldState.bottomSheetState.apply {
                            if (isCollapsed) expand() else collapse()
                        }
                    }
                }
            )
        }
    ) {
        // Screen content
    }
    
    image.png

    如果您要实现模态底部动作条,可以使用 ModalBottomSheetLayout 可组合项:

    val sheetState = rememberModalBottomSheetState(
        ModalBottomSheetValue.Hidden
    )
    ModalBottomSheetLayout(
        sheetState = sheetState,
        sheetContent = {
            // Sheet content
        }
    ) {
        // Screen content
    }
    
    

    背景幕

    image.png

    如果您要实现背景幕,可以使用 BackdropScaffold 可组合项。

    BackdropScaffold(
        appBar = {
            // Top app bar
        },
        backLayerContent = {
            // Back layer content
        },
        frontLayerContent = {
            // Front layer content
        }
    )
    

    BackdropScaffold 接受一些额外的背景幕参数。例如,您可以使用 peekHeightheaderHeight 参数来设置后层的窥视高度和前层的最小非活动高度。此外,您还可以使用 gesturesEnabled 参数来切换背景幕是否响应拖动。

    BackdropScaffold(
        appBar = {
            // Top app bar
        },
        backLayerContent = {
            // Back layer content
        },
        frontLayerContent = {
            // Front layer content
        },
        // Defaults to BackdropScaffoldDefaults.PeekHeight
        peekHeight = 40.dp,
        // Defaults to BackdropScaffoldDefaults.HeaderHeight
        headerHeight = 60.dp,
        // Defaults to true
        gesturesEnabled = false
    )
    

    您可以通过 BackdropScaffoldState 完成以编程方式显示和隐藏背景幕的操作。您可以使用 rememberBackdropScaffoldState 创建一个 BackdropScaffoldState 实例,并通过 scaffoldState 参数将其传递给 BackdropScaffoldBackdropScaffoldState 可提供对 revealconceal 函数的访问权限,以及对与当前背景幕状态相关的属性的访问权限。这些挂起函数需要 CoroutineScope(例如,使用 rememberCoroutineScope),并且可被调用以响应界面事件。

    val scaffoldState = rememberBackdropScaffoldState(
        BackdropValue.Concealed
    )
    val scope = rememberCoroutineScope()
    BackdropScaffold(
        scaffoldState = scaffoldState,
        appBar = {
            TopAppBar(
                title = { Text("Backdrop") },
                navigationIcon = {
                    if (scaffoldState.isConcealed) {
                        IconButton(
                            onClick = {
                                scope.launch { scaffoldState.reveal() }
                            }
                        ) {
                            Icon(
                                Icons.Default.Menu,
                                contentDescription = "Menu"
                            )
                        }
                    } else {
                        IconButton(
                            onClick = {
                                scope.launch { scaffoldState.conceal() }
                            }
                        ) {
                            Icon(
                                Icons.Default.Close,
                                contentDescription = "Close"
                            )
                        }
                    }
                },
                elevation = 0.dp,
                backgroundColor = Color.Transparent
            )
        },
        backLayerContent = {
            // Back layer content
        },
        frontLayerContent = {
            // Front layer content
        }
    )
    

    四、自定义布局

    在实际开发中会出于各种原因进行自定义布局,所以这里简单记录下使用Compose进行自定义布局的方式

    二、使用布局修饰符

    本段参考代码是google的代码

    我们可以使用layout修饰符来元素的测量和布局方式,大概方式如下:

    fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
      ...
    })
    

    不过实际应用中通常使用以下写法:

    fun Modifier.customLayoutModifier(...) =
        this.layout { measurable, constraints ->
            ...
        })
    

    比如想控制显示的Text顶部到第一行基线的位置,示例如下:

    fun Modifier.firstBaselineToTop(
        firstBaselineToTop: Dp
    ) = layout { measurable, constraints ->
        // Measure the composable
        val placeable = measurable.measure(constraints)
    
        // 检查是否包含基线,如果不包含则会引发异常
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
        val firstBaseline = placeable[FirstBaseline]
    
        // Height of the composable with padding - first baseline
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        val height = placeable.height + placeableY
        layout(placeable.width, height) {
            // Where the composable gets placed
            placeable.placeRelative(0, placeableY)
        }
    }
    

    三、创建自定义布局

    layout 修饰符仅更改调用可组合项。如需测量和布置多个可组合项,请改用 Layout 可组合项。此可组合项允许您手动测量和布置子项。ColumnRow 等所有较高级别的布局都使用 Layout 可组合项构建而成。大都数自定义布局遵循以下方式:

    @Composable
    fun MyBasicColumn(
        modifier: Modifier = Modifier,
        content: @Composable () -> Unit
    ) {
        Layout(
            modifier = modifier,
            children = content
        ) { measurables, constraints ->
            // measure and position children given constraints logic here
        }
    }
    

    比如我们自定义一个Column布局,示例如下:

    @Composable
    fun MyBasicColumn(
        modifier: Modifier = Modifier,
        content: @Composable () -> Unit
    ) {
        Layout(
            modifier = modifier,
            content = content
        ) { measurables, constraints ->
            // Don't constrain child views further, measure them with given constraints
            // List of measured children
            val placeables = measurables.map { measurable ->
                // Measure each children
                measurable.measure(constraints)
            }
    
            // Set the size of the layout as big as it can
            layout(constraints.maxWidth, constraints.maxHeight) {
                // Track the y co-ord we have placed children up to
                var yPosition = 0
    
                // Place children in the parent layout
                placeables.forEach { placeable ->
                    // Position item on the screen
                    placeable.placeRelative(x = 0, y = yPosition)
    
                    // Record the y co-ord placed up to
                    yPosition += placeable.height
                }
            }
        }
    }
    

    四、固有特性测量

    一般来说,在自定义布局中使用默认测量方式就可以了,但是有时候可能并不能满足需求。因此要指定自定义 Layout 的固有特性测量,则在创建该布局时替换 MeasurePolicy的 minIntrinsicWidthminIntrinsicHeightmaxIntrinsicWidthmaxIntrinsicHeight

    代码结构如下:

    @Composable
    fun MyCustomComposable(
        modifier: Modifier = Modifier,
        content: @Composable () -> Unit
    ) {
        return object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                // Measure and layout here
            }
    
            override fun IntrinsicMeasureScope.minIntrinsicWidth(
                measurables: List<IntrinsicMeasurable>,
                height: Int
            ) = {
                // Logic here
            }
    
            // Other intrinsics related methods have a default value,
            // you can override only the methods that you need.
        }
    }
    

    而在创建自定义 layout 修饰符时,替换 LayoutModifier 界面中的相关方法。

    fun Modifier.myCustomModifier(/* ... */) = this.then(object : LayoutModifier {
    
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
            // Measure and layout here
        }
    
        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurable: IntrinsicMeasurable,
            height: Int
        ): Int = {
            // Logic here
        }
    
        // Other intrinsics related methods have a default value,
        // you can override only the methods that you need.
    })
    

    五、分析修饰符

    这里分析下Modifier.padding的原理,代码如下(以下代码源自Google,不过已经修改为更易懂的方式):

    // How to create a modifier
    @Stable
    fun Modifier.padding(all: Dp) =
        this.then(
            PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)
        )
    
    // Implementation detail
    private class PaddingModifier(
        val start: Dp = 0.dp,
        val top: Dp = 0.dp,
        val end: Dp = 0.dp,
        val bottom: Dp = 0.dp,
        val rtlAware: Boolean,
    ) : LayoutModifier {
    
        override fun MeasureScope.measure(
            measurable: Measurable,
            constraints: Constraints
        ): MeasureResult {
    
            val horizontal = start.roundToPx() + end.roundToPx() //获取padding的横向长度
            val vertical = top.roundToPx() + bottom.roundToPx() //获取padding的垂直长度
    
            // val placeable = measurable.measure(constraints.offset(horizontal, vertical)) //偏移horizontal、vertical距离后进行测量,偏移只会更改内容位置,不会影响测量大小,因为下面已经进行偏移了,所以可以不用这么麻烦
            val placeable = measurable.measure(constraints)
            val width = constraints.constrainWidth(placeable.width + horizontal)
            val height = constraints.constrainHeight(placeable.height + vertical)
            return layout(width, height) {//定义父布局宽高
                if (rtlAware) {
                    placeable.placeRelative(start.roundToPx(), top.roundToPx()) //将组件在现有位置上进行移动,该移动是在布局里面,所以并不会超出布局宽高
                } else {
                    placeable.place(start.roundToPx(), top.roundToPx())
                }
            }
        }
    }
    

    这里面有一个有意思的问题,就是调用placeable.placeRelative偏移后为什么不会超出设置的宽高?

    这里解释下Placeable,文档上解释的意思是:Placeable对应于可以由其父布局定位的子布局。大多数PlaceableMeasurable.measure调用的结果。Placeable不应该在测量调用之间存储。其中placeable.width是父布局所需要留出的宽度,placeable.height是父布局所需要留出的高度。而placeable.measuredWidth才是控件真正的测量宽度,placeable.measuredHeight是控件真正的测量高度。因此调用placeable.placeRelative函数并不会导致组件超出布局。

    五、ConstraintLayout使用

    ConstraintLayout VS RelativeLayout

    相信当我们进行布局的时候,使用最多的应该是LinearLayout与RelativeLayout。而对于复杂一点的布局来说,他们之间的嵌套使用就最正常不过了。所以为了减少不必要的嵌套布局,Google特意开发的ConstraintLayout。它同时支持LinearLayout与RelativeLayout的所用特性。同时它完全通过约束来减少布局的嵌套。意思就是基本上最外层只需要一个ConstraintLayout节点就可以了。下面先从RelativeLayout开始,看它是如何来实现RelativeLayout的特性。

    这里我列举一些RelativeLayout的特性:

    android:layout_alignStart="@id/view"
    android:layout_alignLeft="@id/view"
    android:layout_alignEnd="@id/view"
    android:layout_alignRight="@id/view"
    android:layout_alignTop="@id/view"
    android:layout_alignBaseline="@id/view"
    android:layout_alignBottom="@id/view"
     
    android:layout_toStartOf="@id/view"
    android:layout_toLeftOf="@id/view"
    android:layout_toEndOf="@id/view"
    android:layout_toRightOf="@id/view"
    android:layout_above="@id/view"
    android:layout_below="@id/view"
     
    android:layout_alignParentStart="true"
    android:layout_alignParentLeft="true"
    android:layout_alignParentEnd="true"
    android:layout_alignParentRight="true"
    android:layout_alignParentTop="true"
    android:layout_alignParentBottom="true"
     
    android:layout_centerInParent="true"
    android:layout_centerHorizontal="true"
    android:layout_centerVertical="true"
    

    相信上面的特性大家再熟悉不过了。对于layout_align*的属性在ConstraintLayout中可以通过以下方式替代。

    app:layout_constraintStart_toStartOf="@id/view"
    app:layout_constraintLeft_toLeftOf="@id/view"
    app:layout_constraintEnd_toEndOf="@id/view"
    app:layout_constraintRight_toRightOf="@id/view"
    app:layout_constraintTop_toTopOf="@id/view"
    app:layout_constraintBaseline_toBaselineOf="@id/view"
    app:layout_constraintBottom_toBottomOf="@id/view"
    

    而对于layout_to*的属性ConstraintLayout也有与之完美替代的特性:

    app:layout_constraintStart_toEndOf="@id/view"
    app:layout_constraintLeft_toRightOf="@id/view"
    app:layout_constraintEnd_toStartOf="@id/view"
    app:layout_constraintRight_toLeftOf="@id/view"
    app:layout_constraintTop_toBottomOf="@id/view"
    app:layout_constraintBottom_toTopOf="@id/view"
    

    接下来是layout_alignParent*的替代实现:

    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    

    这里与之前的layout_align*基本类似,只不过它的所以约束的对象不同而已,为了实现对父布局的依赖,这里统一都是parent。

    最后是layout_center*属性,本质与上面的基本类似。下面直接通过实例来展示

     <Button
            android:id="@+id/center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="center"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    

    通过上面的代码相信不难理解,要想实现水平居中只需设置left与right分别约束parent,而要想实现竖直居中则只需设置top与bottom分别约束parent。

    为了巩固上面的特性,我这里写了一个示例代码与效果图,方便大家理解

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
     
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="left" />
     
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="right"
            app:layout_constraintRight_toRightOf="parent" />
     
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="bottom"
            app:layout_constraintBottom_toBottomOf="parent" />
     
        <Button
            android:id="@+id/center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="center"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="center top"
            app:layout_constraintBottom_toTopOf="@+id/center"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent" />
     
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="center bottom"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/center" />
     
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="center left"
            app:layout_constraintRight_toLeftOf="@+id/center"
            app:layout_constraintTop_toTopOf="@+id/center" />
     
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="center right"
            app:layout_constraintLeft_toRightOf="@+id/center"
            app:layout_constraintTop_toTopOf="@+id/center" />
    
    </android.support.constraint.ConstraintLayout>
    

    [图片上传失败...(image-c6830e-1657537998502)]

    点击查看源码

    在ConstraintLayout中没有match_parent,而与之替代的是match_constraint,在使用中通过0dp来代表。一旦你使用了match_parent那么它的约束将会失效。

    ConstraintLayout VS LinearLayout

    为了能够达到LinearLayout的效果,ConstraintLayout引入了Chain Style.通过以下代码来设置:

    app:layout_constraintHorizontal_chainStyle=""
    app:layout_constraintVertical_chainStyle=""
    

    它主要有三个属性分别为:

    1. spread_inside:两边不留空间,中间间距平分
    2. spread:完全均分
    3. packed:完全不留间距

    解释的有点生硬,下面通过一张官方图来更直观的了解

    [图片上传失败...(image-925df6-1657537998502)]

    需要注意的是:要达到上的的chain效果,他们之间必须完全相互约束,同时chain style的设置都是以第一个view为基点。同时默认chain style为spread。

    同样的下面是一个示例代码与效果图

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
     
        <Button
            android:id="@+id/first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="first"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/second" />
     
        <Button
            android:id="@+id/second"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="second"
            app:layout_constraintLeft_toRightOf="@+id/first"
            app:layout_constraintRight_toLeftOf="@+id/third"
            app:layout_constraintTop_toTopOf="@+id/first" />
     
        <Button
            android:id="@+id/third"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="third"
            app:layout_constraintLeft_toRightOf="@+id/second"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/first" />
     
        <Button
            android:id="@+id/match_first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="first"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/match_second"
            app:layout_constraintTop_toBottomOf="@+id/first" />
     
        <Button
            android:id="@+id/match_second"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="second"
            app:layout_constraintHorizontal_weight="3"
            app:layout_constraintLeft_toRightOf="@+id/match_first"
            app:layout_constraintRight_toLeftOf="@+id/match_third"
            app:layout_constraintTop_toTopOf="@+id/match_first" />
     
        <Button
            android:id="@+id/match_third"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="third"
            app:layout_constraintHorizontal_weight="4"
            app:layout_constraintLeft_toRightOf="@+id/match_second"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/match_first" />
     
        <Button
            android:id="@+id/spread_first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="first"
            app:layout_constraintHorizontal_chainStyle="spread_inside"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/spread_second"
            app:layout_constraintTop_toBottomOf="@+id/match_first" />
     
        <Button
            android:id="@+id/spread_second"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="second"
            app:layout_constraintLeft_toRightOf="@+id/spread_first"
            app:layout_constraintRight_toLeftOf="@+id/spread_third"
            app:layout_constraintTop_toTopOf="@+id/spread_first" />
     
        <Button
            android:id="@+id/spread_third"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="third"
            app:layout_constraintLeft_toRightOf="@+id/spread_second"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/spread_first" />
     
        <Button
            android:id="@+id/packed_first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="first"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/packed_second"
            app:layout_constraintTop_toBottomOf="@+id/spread_first" />
     
        <Button
            android:id="@+id/packed_second"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="second"
            app:layout_constraintLeft_toRightOf="@+id/packed_first"
            app:layout_constraintRight_toLeftOf="@+id/packed_third"
            app:layout_constraintTop_toTopOf="@+id/packed_first" />
     
        <Button
            android:id="@+id/packed_third"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="third"
            app:layout_constraintLeft_toRightOf="@+id/packed_second"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/packed_first" />
     
        <Button
            android:id="@+id/bias_first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="first"
            app:layout_constraintHorizontal_bias="0.2"
            app:layout_constraintHorizontal_chainStyle="packed"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@+id/bias_second"
            app:layout_constraintTop_toBottomOf="@+id/packed_first" />
     
        <Button
            android:id="@+id/bias_second"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="second"
            app:layout_constraintLeft_toRightOf="@+id/bias_first"
            app:layout_constraintRight_toLeftOf="@+id/bias_third"
            app:layout_constraintTop_toTopOf="@+id/bias_first" />
     
        <Button
            android:id="@+id/bias_third"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="third"
            app:layout_constraintLeft_toRightOf="@+id/bias_second"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="@+id/bias_first" />
     
    </android.support.constraint.ConstraintLayout>
    

    [图片上传失败...(image-51cf6a-1657537998502)]

    点击查看源码

    通过效果图,我们会发现第二行他们占的比例不同,返回看代码发现其实与LinearLayout类似,使用了app:layout_constraintHorizontal_weight=""权重属性。当然要使得其生效必须layout_width与layout_height其中一个为0dp。
    我们再来看第4、5行,如果chain style的值为packed,那么它默认是居中排列的,如果想改变的话可以通过设置app:layout_constraintHorizontal_bias="0.2"app:layout_constraintVertical_bias="0.2"

    margin & goneMargin

    margin

    ConstraintLayout的margin与原来的使用区别主要为两点:其一android:layout_margin*效果不变,但它的值不能为负值;其二相应方向的margin设置必须要有约束,否则不生效。

        <TextView
            android:id="@+id/tv1"
            ...
            android:layout_marginRight="100dp"        
            app:layout_constraintLeft_toLeftOf="parent"/>
     
        <TextView
            android:layout_marginLeft="10dp"
            ...       
            app:layout_constraintBaseline_toBaselineOf="@+id/tv1"
            app:layout_constraintLeft_toRightOf="@+id/tv1" />
    

    首先我们来看第二个TextView,它的marginLeft会生效,因为它有left方向的约束:app:layout_constraintLeft_toRightOf="@+id/tv1";而对于第一个TextView,它的marginRight不会生效,因为它的right方向上没有约束,所以如果要生效可以加入:app:layout_constraintRight_toRightOf="parent约束。

    这些都是相对于原来布局margin使用的区别,如果你觉得不习惯可以使用padding代替,这也是ConstraintLayout所推荐的方式。

    goneMargin

    在ConstraintLayout中还增加了另外一种goneMargin,它的作用主要是:一旦某个方向上的约束view不可以见,这时如果设置了该属性,该方向将自动增加margin值。即目标必须不可见。

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <TextView
            android:id="@+id/tv1"
            android:layout_marginTop="100dp"
            android:text="tv1"
            ...
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent" />
     
        <TextView
            android:layout_marginLeft="10dp"
            android:text="tv2"
            ....
            app:layout_constraintBaseline_toBaselineOf="@+id/tv1"
            app:layout_constraintLeft_toRightOf="@+id/tv1" />
     
        <TextView
            android:id="@+id/tv3"
            ...
            android:text="tv3"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     
        <TextView
            android:id="@+id/tv4"
            android:layout_marginLeft="10dp"
            android:text="tv4"
            ...
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toRightOf="@+id/tv3"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_goneMarginLeft="100dp" />
     
        <TextView
            android:id="@+id/tv5"
            ...
            android:layout_marginBottom="10dp"
            android:text="tv5"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.33" />
     
    </android.support.constraint.ConstraintLayout>
    

    [图片上传失败...(image-37141-1657537998502)]

    点击查看源码

    Circle

    在ConstraintLayout中你不仅可以对任意view进行水平与竖直方向的约束,同时你还可以居于约束view的中心点进行不同角度方向的约束。主要属性有如下三种:

    1. layout_constraintCircle: 代表约束的view的id
    2. layout_constraintCircleAngle: 代表约束的角度
    3. layout_constraintCircleRadius: 代表约束的半径大小

    [图片上传失败...(image-8ea85b-1657537998502)]

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
        xmlns:app="http://schemas.android.com/apk/res-auto"
     ...
     >
     
        <TextView
            android:id="@+id/tv1"
            android:text="tv1"
            ...
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
     
        <TextView
            android:text="tv2"
            ...
            app:layout_constraintCircle="@id/tv1"
            app:layout_constraintCircleAngle="45"
            app:layout_constraintCircleRadius="100dp" />
     
    </android.support.constraint.ConstraintLayout>
    

    [图片上传失败...(image-1ffecf-1657537998502)]

    点击查看源码

    GuideLine

    GuideLine也是ConstraintLayout特有的属性,它相当于一个参考线,且它的布局中不会展示。

    GuidLine有水平与竖直方法的设置:

    android:orientation="horizontal|vertical"
    

    主要设置属性为:

    1. layout_constraintGuide_begin: 代表距离GuideLine左边或者底部的距离
    2. layout_constraintGuide_end: 代表距离GuideLine右边或者底部的距离
    3. layout_constraintGuide_percent:代表GuideLine相对于parent的位置百分比
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 
        xmlns:app="http://schemas.android.com/apk/res-auto"
       ...
        >
     
        <android.support.constraint.Guideline
            android:id="@+id/vertical_guide_line"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintGuide_begin="150dp" />
     
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="Button"
            app:layout_constraintLeft_toLeftOf="@+id/vertical_guide_line"
            app:layout_constraintTop_toTopOf="parent" />
     
        <android.support.constraint.Guideline
            android:id="@+id/horizontal_guide_line"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_end="150dp" />
     
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button1"
            app:layout_constraintRight_toLeftOf="@+id/vertical_guide_line"
            app:layout_constraintTop_toTopOf="@+id/horizontal_guide_line" />
     
        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button2"
            app:layout_constraintBottom_toBottomOf="@+id/horizontal_guide_line"
            app:layout_constraintLeft_toRightOf="@+id/vertical_guide_line" />
     
        <android.support.constraint.Guideline
            android:id="@+id/percent_guide_Line"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.3" />
     
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="margin top of parent 30%"
            app:layout_constraintTop_toBottomOf="@+id/percent_guide_Line" />
     
    </android.support.constraint.ConstraintLayout>
    

    [图片上传失败...(image-3a7643-1657537998502)]

    点击查看源码

    Barrier & Group

    Barrier

    Barrier与GuideLine有点类似,也是布局不可见的,不同的是它就约束对象的,看如下图:

    [站外图片上传中...(image-d11b4-1657537998502)]

    如果有这么一种需求:tv3始终在tv1与tv2的最近右边,但tv1与tv2的宽度的不看预期的,即可能tv1更长或者tv2更长。这时Barrier就能很好的解决这问题。

        <!--barrier-->
        <TextView
            android:id="@+id/tv1"
            android:layout_width="150dp"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimary"
            android:padding="5dp"
            android:text="tv1,固定宽度"
            android:textColor="@android:color/white" />
     
        <TextView
            android:id="@+id/tv2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:background="@color/colorPrimary"
            android:padding="5dp"
            android:text="tv2,宽度不固定"
            android:textColor="@android:color/white"
            app:layout_constraintTop_toBottomOf="@+id/tv1" />
     
        <android.support.constraint.Barrier
            android:id="@+id/barrier"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:barrierDirection="end"
            app:constraint_referenced_ids="tv1,tv2" />
     
        <TextView
            android:id="@+id/tv3"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:background="@color/colorPrimary"
            android:padding="5dp"
            android:text="@string/barrier_tv3"
            android:textColor="@android:color/white"
            app:layout_constraintLeft_toRightOf="@+id/barrier"
            app:layout_constraintRight_toRightOf="parent" />
    

    在Barrier中有两个属性,分别为:

    1. barrierDirection:控制其方向
    2. constraint_referenced_ids:约束的view的参考id

    Group

    细心的你可能会发现,既然ConstraintLayout中能减少布局的嵌套,那么对于同一模块的UI对其进行整体操作,是否只能逐一进行操作(显隐操作)?ConstraintLayout给出了它的答案,就是Group。它的作用就是对多个view进行分组操作,当然在布局中也是不可见的。主要属性是:

    constraint_referenced_ids: 约束的view的参考id
    

    就拿上面Barrier的示例来说。

        <!--group 通过设置关联id来控制它们的显隐-->
        <android.support.constraint.Group
            android:id="@+id/group"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:visibility="visible"
            app:constraint_referenced_ids="tv1,tv2" />
    

    如果加了如上代码,那么对group进行VISIBLE操作时,对同时作用于tv1与tv2。

    点击查看源码

    other

    下面来说一下使用ConstraintLayout时,一些需要注意的点。这样可以帮助你在使用做少走弯路。

    wrap_content

    如果你的View中对宽高使用了wrap_content,那么你要时刻注意,它的约束可能并不会很好的生效。例如如下实例:

        <!--当为wrap_content对其进行强制约束-->
        <!--app:layout_constrainedWidth=”true|false”-->
        <!--app:layout_constrainedHeight=”true|false”-->
        <!--android:minWidth-->
        <!--android:minHeight-->
        <!--android:maxWidth-->
        <!--android:maxHeight-->
        <TextView
            android:id="@+id/tv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimary"
            android:padding="10dp"
            android:text="tv1"
            android:textColor="@android:color/white" />
     
        <TextView
            android:id="@+id/tv2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:background="@color/colorPrimary"
            android:padding="10dp"
            android:text="@string/other_tv2"
            android:textColor="@android:color/white"
            app:layout_constrainedWidth="true"
            app:layout_constraintLeft_toRightOf="@+id/tv1"
            app:layout_constraintRight_toRightOf="parent" />
    

    [图片上传失败...(image-161102-1657537998502)]

    如果将app:layout_constrainedWidth="true"这行代码删除,那么你将会看到如下效果

    [图片上传失败...(image-3a8c3a-1657537998502)]

    在代码注释中已经说明,在使用wrap_content时,还可以使用minWith等属性。它们之间的优先级为 min/max > constraintWith/Height

    MATCH_CONSTRAIN

    当使用了MATCH_CONSTRAIN,即0dp来展示宽高时,可以通过如下方式来进行约束相应宽高值。

        <!--当为MATCH_CONSTRAINT 可以通过以下设置来约束宽高-->
        <!--layout_constraintWidth_min and layout_constraintHeight_min-->
        <!--layout_constraintWidth_max and layout_constraintHeight_max-->
        <!--layout_constraintWidth_percent and layout_constraintHeight_percent-->
        <!--android:minWidth-->
        <!--android:minHeight-->
        <!--android:maxWidth-->
        <!--android:maxHeight-->
        <TextView
            android:id="@+id/tv3"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:background="@color/colorPrimary"
            android:padding="10dp"
            android:text="tv3"
            android:textColor="@android:color/white"
            app:layout_constraintTop_toBottomOf="@+id/tv1"
            app:layout_constraintWidth_percent="0.5" />
    

    [图片上传失败...(image-4b16-1657537998503)]

    如果将app:layout_constraintWidth_percent="0.5"去掉的话,你将看到如下效果:

    [站外图片上传中...(image-229af4-1657537998503)]

    注意它们之间的优先级为parent > min/max > constraint_min/max

    Ratio

    如果你的需要是对View进行固定宽高比展示时,那么Ratio的这个特性将非常适合你。你只需设置它的layout_constraintDimensionRatio属性即可。

    如果layout_width与layout_height其中一个为MATCH_CONSTRAIN(0dp), MATCH_CONSTRAIN(0dp)的值将根据layout_constraintDimensionRatio所设的值来计算(w:h)

        <TextView
            android:id="@+id/tv4"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:background="@color/colorPrimary"
            android:padding="10dp"
            android:text="tv4"
            android:textColor="@android:color/white"
            app:layout_constraintDimensionRatio="2:1"
            app:layout_constraintTop_toBottomOf="@+id/tv3" />
    

    [图片上传失败...(image-649252-1657537998503)]

    如果layout_width与layout_height都为MATCH_CONSTRAIN(0dp), 那么可以对layout_constraintDimensionRatio添加前缀W/H(H,w:h,来对其进行宽高方向的约束,从而形成比例。

        <TextView
            android:id="@+id/tv5"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="50dp"
            android:background="@color/colorPrimary"
            android:padding="10dp"
            android:text="tv5"
            android:textColor="@android:color/white"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintDimensionRatio="H,3:1"
            app:layout_constraintTop_toBottomOf="@+id/tv4" />
    

    [站外图片上传中...(image-f31983-1657537998503)]

    根据效果图展示的宽高比为1:3。即对应了H,3:1。

    六、文末

    由于布局这块涉及内容较多 ,文章篇幅很长。全文全方面讲解了Compose布局的各方面知识。更多Android前言技术进阶,我自荐一套完整的Android的资料,以及一些视频课讲解》》》

    相关文章

      网友评论

        本文标题:Android前沿技术学习——Compose布局原理+配置布局+

        本文链接:https://www.haomeiwen.com/subject/dgvnbrtx.html