Android Sunflower中的Jetpack Compose:布局系统详解

Android Sunflower中的Jetpack Compose:布局系统详解

【免费下载链接】sunflower A gardening app illustrating Android development best practices with migrating a View-based app to Jetpack Compose. 【免费下载链接】sunflower 项目地址: https://gitcode.com/gh_mirrors/su/sunflower

概述

Jetpack Compose是Android官方推荐的现代UI工具包,它采用声明式编程模型,让构建Android界面变得更加简单和直观。本文将以Android Sunflower应用为例,详细解析Jetpack Compose的布局系统,帮助开发者更好地理解和应用Compose进行UI开发。

Sunflower是一个展示Android开发最佳实践的园艺应用,它展示了如何将基于View的应用迁移到Jetpack Compose。通过分析README.mddocs/MigrationJourney.md,我们了解到Sunflower已完成从View到Compose的全面迁移,包括5个主要屏幕和导航系统的重构。

Sunflower应用截图

布局基础

布局组件层次结构

Jetpack Compose的布局系统基于组件的层次结构,通过组合不同的布局组件来构建复杂的UI界面。在Sunflower应用中,我们可以看到这种层次结构的应用。

@Composable
fun SunflowerApp() {
    val navController = rememberNavController()
    SunFlowerNavHost(
        navController = navController
    )
}

@Composable
fun SunFlowerNavHost(
    navController: NavHostController
) {
    val activity = (LocalContext.current as Activity)
    NavHost(navController = navController, startDestination = Screen.Home.route) {
        composable(route = Screen.Home.route) {
            HomeScreen(
                onPlantClick = {
                    navController.navigate(
                        Screen.PlantDetail.createRoute(
                            plantId = it.plantId
                        )
                    )
                }
            )
        }
        // 其他屏幕的组合...
    }
}

上述代码来自app/src/main/java/com/google/samples/apps/sunflower/compose/SunflowerApp.kt,展示了Sunflower应用的根布局结构。SunflowerApp是应用的入口点,它创建了一个NavHost来管理不同屏幕之间的导航。

常用布局组件

Jetpack Compose提供了多种布局组件,用于构建不同结构的UI界面。Sunflower应用中广泛使用了这些组件:

  1. Column:垂直排列子组件
  2. Row:水平排列子组件
  3. Box:重叠排列子组件
  4. LazyVerticalGrid:高效展示网格数据
  5. ConstraintLayout:复杂约束布局

下面我们将详细介绍这些布局组件在Sunflower应用中的应用。

主要布局组件详解

1. Column和Row

Column和Row是最基本的布局组件,分别用于垂直和水平排列子组件。在Sunflower的植物详情页面中,我们可以看到Column的应用:

@Composable
private fun PlantInformation(
    name: String,
    wateringInterval: Int,
    description: String,
    hasValidUnsplashKey: Boolean,
    onNamePosition: (Float) -> Unit,
    toolbarState: ToolbarState,
    onGalleryClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier.padding(Dimens.PaddingLarge)) {
        Text(
            text = name,
            style = MaterialTheme.typography.displaySmall,
            modifier = Modifier
                .padding(
                    start = Dimens.PaddingSmall,
                    end = Dimens.PaddingSmall,
                    bottom = Dimens.PaddingNormal
                )
                .align(Alignment.CenterHorizontally)
                .onGloballyPositioned { onNamePosition(it.positionInWindow().y) }
                .visible { toolbarState == ToolbarState.HIDDEN }
        )
        // 其他子组件...
    }
}

这段代码来自app/src/main/java/com/google/samples/apps/sunflower/compose/plantdetail/PlantDetailView.kt,展示了如何使用Column垂直排列植物信息。

2. LazyVerticalGrid

LazyVerticalGrid用于高效展示网格数据,它只会渲染当前可见区域的项目,从而提高性能。在Sunflower的植物列表和花园页面中都使用了LazyVerticalGrid:

@Composable
fun PlantListScreen(
    plants: List<Plant>,
    modifier: Modifier = Modifier,
    onPlantClick: (Plant) -> Unit = {},
) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        modifier = modifier.testTag("plant_list")
            .imePadding(),
        contentPadding = PaddingValues(
            horizontal = dimensionResource(id = R.dimen.card_side_margin),
            vertical = dimensionResource(id = R.dimen.header_margin)
        )
    ) {
        items(
            items = plants,
            key = { it.plantId }
        ) { plant ->
            PlantListItem(plant = plant) {
                onPlantClick(plant)
            }
        }
    }
}

这段代码来自app/src/main/java/com/google/samples/apps/sunflower/compose/plantlist/PlantListScreen.kt,它创建了一个2列的网格布局来展示植物列表。

3. ConstraintLayout

ConstraintLayout用于构建复杂的布局,它允许您通过创建组件之间的约束关系来精确定位和调整UI元素。在Sunflower的植物详情页面中,ConstraintLayout被用来实现复杂的布局效果:

@Composable
private fun PlantDetailsContent(
    scrollState: ScrollState,
    toolbarState: ToolbarState,
    plant: Plant,
    isPlanted: Boolean,
    hasValidUnsplashKey: Boolean,
    imageHeight: Dp,
    onNamePosition: (Float) -> Unit,
    onFabClick: () -> Unit,
    onGalleryClick: () -> Unit,
    contentAlpha: () -> Float,
) {
    Column(Modifier.verticalScroll(scrollState)) {
        ConstraintLayout {
            val (image, fab, info) = createRefs()

            PlantImage(
                imageUrl = plant.imageUrl,
                imageHeight = imageHeight,
                modifier = Modifier
                    .constrainAs(image) { top.linkTo(parent.top) }
                    .alpha(contentAlpha())
            )

            if (!isPlanted) {
                val fabEndMargin = Dimens.PaddingSmall
                PlantFab(
                    onFabClick = onFabClick,
                    modifier = Modifier
                        .constrainAs(fab) {
                            centerAround(image.bottom)
                            absoluteRight.linkTo(
                                parent.absoluteRight,
                                margin = fabEndMargin
                            )
                        }
                        .alpha(contentAlpha())
                )
            }

            PlantInformation(
                name = plant.name,
                wateringInterval = plant.wateringInterval,
                description = plant.description,
                hasValidUnsplashKey = hasValidUnsplashKey,
                onNamePosition = { onNamePosition(it) },
                toolbarState = toolbarState,
                onGalleryClick = onGalleryClick,
                modifier = Modifier.constrainAs(info) {
                    top.linkTo(image.bottom)
                }
            )
        }
    }
}

这段代码来自app/src/main/java/com/google/samples/apps/sunflower/compose/plantdetail/PlantDetailView.kt,展示了如何使用ConstraintLayout来定位植物图片、FAB按钮和植物信息。

响应式布局

Jetpack Compose提供了多种方式来创建响应式布局,以适应不同屏幕尺寸和方向。在Sunflower应用中,我们可以看到响应式布局的应用。

尺寸单位

Compose使用与设备无关的像素(dp)作为尺寸单位,确保在不同屏幕密度的设备上显示一致。Sunflower应用通过Dimens对象来集中管理尺寸:

object Dimens {
    val PaddingSmall: Dp
        @Composable get() = dimensionResource(R.dimen.margin_small)

    val PaddingNormal: Dp
        @Composable get() = dimensionResource(R.dimen.margin_normal)

    val PaddingLarge: Dp = 24.dp

    val PlantDetailAppBarHeight: Dp
        @Composable get() = dimensionResource(R.dimen.plant_detail_app_bar_height)

    val ToolbarIconPadding = 12.dp

    val ToolbarIconSize = 32.dp
}

这段代码来自app/src/main/java/com/google/samples/apps/sunflower/compose/Dimens.kt,它定义了应用中使用的各种尺寸。这些尺寸的值在XML文件中定义:

<resources>
    <dimen name="margin_normal">16dp</dimen>
    <dimen name="margin_small">8dp</dimen>
    <dimen name="plant_detail_app_bar_height">278dp</dimen>
    <dimen name="plant_item_image_height">95dp</dimen>
    <!-- 其他尺寸定义 -->
</resources>

这段XML代码来自app/src/main/res/values/dimens.xml

布局修饰符

Compose提供了丰富的修饰符(Modifier)来调整布局。Sunflower应用中自定义了一个visible修饰符:

fun Modifier.visible(isVisible: () -> Boolean) = this.then(VisibleModifier(isVisible))

private data class VisibleModifier(
    private val isVisible: () -> Boolean
) : LayoutModifier {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        val placeable = measurable.measure(constraints)
        return layout(placeable.width, placeable.height) {
            if (isVisible()) {
                placeable.place(0, 0)
            }
        }
    }
}

这段代码来自app/src/main/java/com/google/samples/apps/sunflower/compose/Modifiers.kt,它创建了一个可以控制组件可见性的修饰符。

高级布局技巧

1. 滚动效果

在Sunflower的植物详情页面中,实现了一个类似CollapsingToolbarLayout的效果:

@Composable
fun PlantDetails(
    plant: Plant,
    isPlanted: Boolean,
    hasValidUnsplashKey: Boolean,
    callbacks: PlantDetailsCallbacks,
    modifier: Modifier = Modifier
) {
    // PlantDetails owns the scrollerPosition to simulate CollapsingToolbarLayout's behavior
    val scrollState = rememberScrollState()
    var plantScroller by remember {
        mutableStateOf(PlantDetailsScroller(scrollState, Float.MIN_VALUE))
    }
    val transitionState =
        remember(plantScroller) { plantScroller.toolbarTransitionState }
    val toolbarState = plantScroller.getToolbarState(LocalDensity.current)

    // Transition that fades in/out the header with the image and the Toolbar
    val transition = updateTransition(transitionState, label = "")
    val toolbarAlpha = transition.animateFloat(
        transitionSpec = { spring(stiffness = Spring.StiffnessLow) }, label = ""
    ) { toolbarTransitionState ->
        if (toolbarTransitionState == ToolbarState.HIDDEN) 0f else 1f
    }
    val contentAlpha = transition.animateFloat(
        transitionSpec = { spring(stiffness = Spring.StiffnessLow) }, label = ""
    ) { toolbarTransitionState ->
        if (toolbarTransitionState == ToolbarState.HIDDEN) 1f else 0f
    }

    Box(modifier.fillMaxSize()) {
        PlantDetailsContent(
            scrollState = scrollState,
            toolbarState = toolbarState,
            onNamePosition = { newNamePosition ->
                // Comparing to Float.MIN_VALUE as we are just interested on the original
                // position of name on the screen
                if (plantScroller.namePosition == Float.MIN_VALUE) {
                    plantScroller =
                        plantScroller.copy(namePosition = newNamePosition)
                }
            },
            plant = plant,
            isPlanted = isPlanted,
            hasValidUnsplashKey = hasValidUnsplashKey,
            imageHeight = with(LocalDensity.current) {
                val candidateHeight =
                    Dimens.PlantDetailAppBarHeight
                // FIXME: Remove this workaround when https://github.com/bumptech/glide/issues/4952
                // is released
                maxOf(candidateHeight, 1.dp)
            },
            onFabClick = callbacks.onFabClick,
            onGalleryClick = { callbacks.onGalleryClick(plant) },
            contentAlpha = { contentAlpha.value }
        )
        PlantToolbar(
            toolbarState, plant.name, callbacks,
            toolbarAlpha = { toolbarAlpha.value },
            contentAlpha = { contentAlpha.value }
        )
    }
}

这段代码来自app/src/main/java/com/google/samples/apps/sunflower/compose/plantdetail/PlantDetailView.kt,它实现了滚动时工具栏的淡入淡出效果。

2. 空状态处理

当花园中没有植物时,Sunflower应用会显示一个空状态界面:

@Composable
private fun EmptyGarden(onAddPlantClick: () -> Unit, modifier: Modifier = Modifier) {
    // Calls reportFullyDrawn when this composable is composed.
    ReportDrawn()

    Column(
        modifier,
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(id = R.string.garden_empty),
            style = MaterialTheme.typography.headlineSmall
        )
        Button(
            shape = MaterialTheme.shapes.medium,
            onClick = onAddPlantClick
        ) {
            Text(
                text = stringResource(id = R.string.add_plant),
                style = MaterialTheme.typography.titleSmall
            )
        }
    }
}

这段代码来自app/src/main/java/com/google/samples/apps/sunflower/compose/garden/GardenScreen.kt,它展示了如何使用Column来居中排列空状态的文本和按钮。

总结

Jetpack Compose提供了强大而灵活的布局系统,使Android UI开发变得更加简单和高效。通过Sunflower应用的实例,我们了解了Compose布局的基本概念、常用组件和高级技巧。

Compose的布局系统具有以下优势:

  1. 声明式API,使布局代码更加直观和易于维护
  2. 丰富的布局组件,满足各种UI需求
  3. 高效的渲染机制,提高应用性能
  4. 强大的自定义能力,可以创建复杂的UI效果

希望本文能帮助您更好地理解和应用Jetpack Compose的布局系统,为您的Android应用开发带来更多可能性。

参考资料

【免费下载链接】sunflower A gardening app illustrating Android development best practices with migrating a View-based app to Jetpack Compose. 【免费下载链接】sunflower 项目地址: https://gitcode.com/gh_mirrors/su/sunflower

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值