Android-Compose中的基本布局

目标

构建一个健康应用,这款应用包含两个版块,一个列出了收藏合集,另一个列出了各种体育锻炼。具体如下图所示,包括竖屏和横屏的适配

主要内容

  • 借助修饰符扩充可组合项
  • 通过 Column 和 LazyRow 等标准布局组件定位可组合子项
  • 通过对齐方式和排列方式更改可组合子项在父项中的位置
  • 借助 Scaffold Bottom Navigation 等 Material 可组合项创建详细布局
  • 使用槽位 API 构建灵活的可组合项
  • 为不同的屏幕配置构建布局

结构分析

总体来说可以将应用分为内容导航栏两个部分

在内容部分又可以分为搜索栏、收藏栏以及运动收集网格

搜索栏

@Composable
fun SearchBar(modifier: Modifier=Modifier)
{
    TextField(value = "",
        onValueChange = {},
        modifier= modifier
            .fillMaxWidth()
            .heightIn(min = 56.dp),
        leadingIcon = {
            Icon(painter = painterResource(id = R.drawable.search), contentDescription = null )
        },
        colors = TextFieldDefaults.colors(
            unfocusedLabelColor = MaterialTheme.colorScheme.surface,
            focusedContainerColor = MaterialTheme.colorScheme.surface,
        ),
        placeholder = {
            Text(text = stringResource(R.string.search))
        }
    )
}

图标是在Browse Fonts – Google Fonts这里下载的。总体代码还是比较简单的,这里先不涉及具体的文本处理。

收藏栏

首先要分析每个元素的具体结构。

可以看到,要对图片进行裁剪,并且在图片下面要对齐相应的文字。

使用modifier的clip对图片进行裁剪,并且可能会出现裁剪之后有残缺,又用了ContentScale对图片进行适当缩放来填满clip的形状

@Composable
fun AlignYourBodyElement(
    modifier: Modifier=Modifier,
    @DrawableRes drawable:Int,
    @StringRes text:Int
)
{
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = modifier
    ) {
        Image(painter = painterResource(id = drawable),
            contentDescription =null ,
            modifier= Modifier
                .size(88.dp)
                .clip(CircleShape),
            contentScale = ContentScale.Crop

        )
        Text(text = stringResource(id = text),
            modifier=Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp)
        )
    }
}

这里涉及到了@DrawableRes@StringRes两个修饰符,显式规定了传入参数的类型可以被当作resource处理。类似于 R.string.username 这样的资源引用方式,实际上在程序中是以Int数字类型进行传递的(可以调试查看)。添加注解后,编译器会提前检查资源是否合法存在,防止错误的资源类型传递。

剩下的就是将每个元素进行横向排列

@Composable
fun AlignYourBodyRow(modifier: Modifier=Modifier)
{
    val alignYourBodyData=
        listOf(
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
    )

    LazyRow(
        modifier=modifier,
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(horizontal = 16.dp)
    ){
        items(alignYourBodyData){
            item-> AlignYourBodyElement(drawable = item.drawable, text = item.text)
        }
    }

}

这里使用到了LazyRow(懒加载)而不是Row,原因就在于,LazyRow 只会渲染当前视图中的内容,有助于提高应用的性能。

运动收集网格

data class FavoriteCollectionsData(
    @DrawableRes val drawable: Int,
    @StringRes val text: Int
)
@Composable
fun FavoriteCollectionsGrid(modifier: Modifier=Modifier)
{
    val favoriteCollectionsData= listOf(
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
    )

    LazyHorizontalGrid(
        rows = GridCells.Fixed(2),
        modifier=modifier.height(168.dp),
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        items(favoriteCollectionsData){
            item-> FavoriteCollectionCard(drawable = item.drawable, text = item.text, modifier = Modifier.height(80.dp))
        }
    }
}

LazyHorizontalGrid同样是懒加载的组件,指定了行数row之后会自动排列,然后使用items进行子元素生成

内容插槽

@Composable
fun HomeSection(
    @StringRes title: Int,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Column(modifier=modifier) {
        Text(
            text = stringResource(title),
            style = MaterialTheme.typography.titleMedium,
            modifier = Modifier
                .paddingFromBaseline(top = 40.dp, bottom = 16.dp)
                .padding(horizontal = 16.dp),
        )
        content()
    }
}

将内容传入HomeSection插槽来使用,增强代码的结构性。具体如下,并且要使用verticalScroll添加可滑动效果

//主体代码
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
    Column(
        modifier= modifier
            .fillMaxHeight()
            .background(color = Color(0xFFdbccb9))
            .verticalScroll(
                rememberScrollState()
            )
            .padding(bottom = 20.dp)
    ) {
        Spacer(modifier = Modifier.height(16.dp))
        SearchBar(Modifier.padding(horizontal = 16.dp))
        HomeSection(title = R.string.username) {
            AlignYourBodyRow()
        }

        HomeSection(title = R.string.username) {
            FavoriteCollectionsGrid()
        }

    }
}

导航栏

由于涉及到横屏竖屏两种情况,导航栏得做两个

//竖屏
NavigationBar(
        modifier = modifier,
    ){
        NavigationBarItem(
            icon = {
                Icon(
                    imageVector = Icons.Default.Home,
                    contentDescription = stringResource(id = R.string.username)
                )
            },
            label = {
                Text(
                    text = stringResource(id = R.string.home)
                )
            },
            selected = true,
            onClick = {}
        )
        NavigationBarItem(
            icon = {
                Icon(
                    imageVector = Icons.Default.AccountCircle,
                    contentDescription = null
                )
            },
            label = {
                Text(
                    text = stringResource(id = R.string.profile)
                )
            },
            selected = false,
            onClick = {}
        )
    }

Jetpack Compose十分贴心,内置了底部导航栏的组件,可以直接使用。

//横屏
@Composable
private fun SootheNavigationRail(modifier: Modifier=Modifier)
{
    NavigationRail(
        modifier=modifier.padding(top = 8.dp, bottom = 8.dp),
        containerColor = MaterialTheme.colorScheme.background,
    ) {


        Column(
            modifier=modifier.fillMaxHeight(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            NavigationRailItem(selected = false, onClick = { /*TODO*/ }, icon = {
                Icon(imageVector = Icons.Default.Home, contentDescription = null )
            },
                label = {
                    Text(text = stringResource(id = R.string.home))
                },

                )

            Spacer(modifier = Modifier.height(8.dp))

            NavigationRailItem(selected = false, onClick = { /*TODO*/ }, icon = {
                Icon(imageVector = Icons.Default.AccountCircle, contentDescription = null)
            },
                label = {
                    Text(text = stringResource(id = R.string.profile))
                }
            )

        }
    }
}

判断设备状态

官方的引用方式:窗口大小类别  |  Jetpack Compose  |  Android Developers (google.cn)

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

    // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

随意选择即可,我这里使用的是高度判断,并且只关心了手机设备

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    when(windowSizeClass.windowHeightSizeClass)
    {
        WindowHeightSizeClass.MEDIUM,WindowHeightSizeClass.EXPANDED->{
            Scaffold(
                bottomBar = {
                    SootheBottomNavigation()
                }
            ) {
                padding ->  HomeScreen(Modifier.padding(padding))
            }
        }

        WindowHeightSizeClass.COMPACT->{
            Row {
                SootheNavigationRail()
                HomeScreen()
            }
        }
        
    }
}

如果是常规的竖屏情况下,可以使用Scaffold组件来进行快速创建包含各种导航栏的界面。

在横屏状态下,导航栏和主体内容应该是成横向排列

总体代码

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyApplicationTheme {
                     MyApp()
            }
        }
    }
}
data class AlignYourBodyData(
    @DrawableRes val drawable:Int,
    @StringRes val text:Int
)

data class FavoriteCollectionsData(
    @DrawableRes val drawable: Int,
    @StringRes val text: Int
)
@Composable
fun FavoriteCollectionsGrid(modifier: Modifier=Modifier)
{
    val favoriteCollectionsData= listOf(
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
        FavoriteCollectionsData(R.drawable.tomcat,R.string.username),
    )

    LazyHorizontalGrid(
        rows = GridCells.Fixed(2),
        modifier=modifier.height(168.dp),
        contentPadding = PaddingValues(horizontal = 16.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        items(favoriteCollectionsData){
            item-> FavoriteCollectionCard(drawable = item.drawable, text = item.text, modifier = Modifier.height(80.dp))
        }
    }
}


@Composable
fun AlignYourBodyRow(modifier: Modifier=Modifier)
{
    val alignYourBodyData=
        listOf(
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
            AlignYourBodyData(R.drawable.tomcat,R.string.username),
    )

    LazyRow(
        modifier=modifier,
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(horizontal = 16.dp)
    ){
        items(alignYourBodyData){
            item-> AlignYourBodyElement(drawable = item.drawable, text = item.text)
        }
    }

}

@Composable
fun AlignYourBodyElement(
    modifier: Modifier=Modifier,
    @DrawableRes drawable:Int,
    @StringRes text:Int
)
{
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = modifier
    ) {
        Image(painter = painterResource(id = drawable),
            contentDescription =null ,
            modifier= Modifier
                .size(88.dp)
                .clip(CircleShape),
            contentScale = ContentScale.Crop

        )
        Text(text = stringResource(id = text),
            modifier=Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp)
        )
    }
}

@Composable
fun SearchBar(modifier: Modifier=Modifier)
{
    TextField(value = "",
        onValueChange = {},
        modifier= modifier
            .fillMaxWidth()
            .heightIn(min = 56.dp),
        leadingIcon = {
            Icon(painter = painterResource(id = R.drawable.search), contentDescription = null )
        },
        colors = TextFieldDefaults.colors(
            unfocusedLabelColor = MaterialTheme.colorScheme.surface,
            focusedContainerColor = MaterialTheme.colorScheme.surface,
        ),
        placeholder = {
            Text(text = stringResource(R.string.search))
        }
    )
}

@Composable
fun FavoriteCollectionCard(
    modifier: Modifier=Modifier,
    @DrawableRes drawable: Int,
    @StringRes text:Int
)
{
    Surface(
        shape = MaterialTheme.shapes.medium,
        modifier = modifier,
        color = MaterialTheme.colorScheme.surfaceVariant
    ) {
        Row (
            verticalAlignment = Alignment.CenterVertically,
            modifier=modifier.width(255.dp)
        ){
            Image(painter = painterResource(id = drawable),
                contentDescription =null,
                modifier=Modifier.size(80.dp),
                contentScale = ContentScale.Crop
            )
            Text(text = stringResource(id = text))

        }
    }
}
@Composable
fun HomeSection(
    @StringRes title: Int,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Column(modifier=modifier) {
        Text(
            text = stringResource(title),
            style = MaterialTheme.typography.titleMedium,
            modifier = Modifier
                .paddingFromBaseline(top = 40.dp, bottom = 16.dp)
                .padding(horizontal = 16.dp),
        )
        content()
    }
}
@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
    Column(
        modifier= modifier
            .fillMaxHeight()
            .background(color = Color(0xFFdbccb9))
            .verticalScroll(
                rememberScrollState()
            )
            .padding(bottom = 20.dp)
    ) {
        Spacer(modifier = Modifier.height(16.dp))
        SearchBar(Modifier.padding(horizontal = 16.dp))
        HomeSection(title = R.string.username) {
            AlignYourBodyRow()
        }

        HomeSection(title = R.string.username) {
            FavoriteCollectionsGrid()
        }

    }
}

@Composable
private fun SootheBottomNavigation(modifier: Modifier=Modifier){
    NavigationBar(
        modifier = modifier,
    ){
        NavigationBarItem(
            icon = {
                Icon(
                    imageVector = Icons.Default.Home,
                    contentDescription = stringResource(id = R.string.username)
                )
            },
            label = {
                Text(
                    text = stringResource(id = R.string.home)
                )
            },
            selected = true,
            onClick = {}
        )
        NavigationBarItem(
            icon = {
                Icon(
                    imageVector = Icons.Default.AccountCircle,
                    contentDescription = null
                )
            },
            label = {
                Text(
                    text = stringResource(id = R.string.profile)
                )
            },
            selected = false,
            onClick = {}
        )
    }
}

@Composable
private fun SootheNavigationRail(modifier: Modifier=Modifier)
{
    NavigationRail(
        modifier=modifier.padding(top = 8.dp, bottom = 8.dp),
        containerColor = MaterialTheme.colorScheme.background,
    ) {


        Column(
            modifier=modifier.fillMaxHeight(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            NavigationRailItem(selected = false, onClick = { /*TODO*/ }, icon = {
                Icon(imageVector = Icons.Default.Home, contentDescription = null )
            },
                label = {
                    Text(text = stringResource(id = R.string.home))
                },

                )

            Spacer(modifier = Modifier.height(8.dp))

            NavigationRailItem(selected = false, onClick = { /*TODO*/ }, icon = {
                Icon(imageVector = Icons.Default.AccountCircle, contentDescription = null)
            },
                label = {
                    Text(text = stringResource(id = R.string.profile))
                }
            )

        }
    }
}


@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    
    when(windowSizeClass.windowHeightSizeClass)
    {
        WindowHeightSizeClass.MEDIUM,WindowHeightSizeClass.EXPANDED->{
            Scaffold(
                bottomBar = {
                    SootheBottomNavigation()
                }
            ) {
                padding ->  HomeScreen(Modifier.padding(padding))
            }
        }

        WindowHeightSizeClass.COMPACT->{
            Row {
                SootheNavigationRail()
                HomeScreen()
            }
        }
        
    }
}
@Preview(showBackground = true)
@Composable
fun MyPreview() {
    MyApplicationTheme {
        MyApp()
    }
}

运行效果

在竖屏情况下预览

在横屏状态下预览

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇