👋第二章:使用声明式UI创建屏幕并探索组合原则
移动应用程序需要一个用户界面(UI)来进行用户交互。
例如,创建UI的旧方法在Android中是必不可少的。
这意味着应用程序UI的单独原型使用独特的可扩展标记语言(XML)布局,而不是用于构建逻辑的相同语言。
然而,在现代Android开发中,有一种趋势是停止使用命令式编程,并开始使用声明式方式制作UI,这意味着开发人员根据接收到的数据设计UI。
这种设计范例使用一种编程语言来创建整个应用程序。
公平地说,对于新开发人员来说,在构建UI时决定学习什么似乎很困难:是使用旧的创建视图的方法,还是选择新的Jetpack Compose。
但是,假设您在Jetpack Compose时代之前构建了一个Android应用程序。
在这种情况下,您可能已经知道使用XML有点乏味,尤其是在代码库很复杂的情况下。
然而,使用Jetpack Compose作为您的首选使工作更容易。
此外,它通过确保开发人员使用更少的代码来简化UI开发,因为他们利用了直观的Kotlin api。
因此,新开发人员在创建视图时使用Jetpack Compose而不是XML是合乎逻辑的。
但是,了解这两者是有益的,因为许多应用程序仍然使用XML布局,您可能需要维护视图,但使用Jetpack Compose构建新的视图。
在本章中,我们将通过尝试使用列、行、框、惰性列等实现小示例来了解Jetpack Compose的基础知识。
在本章中,我们将介绍以下内容:
- 在Jetpack Compose中实现Android视图
- 在Jetpack Compose中实现一个可滚动列表
- 使用Jetpack Compose实现第一个带有视图页的选项卡布局
- 在Compose中实现动画
- 在Jetpack Compose中实现可访问性
- 使用Jetpack Compose实现声明式图形
⚽️1. 技术要求
为了能够查看所有指南,您需要分别运行所有预览功能。因此,寻找@Preview可组合函数来查看创建的UI。
⚽️2. 在Jetpack Compose中实现Android视图
在每个Android应用程序中,拥有一个UI元素是非常重要的。Android中的视图是一个简单的UI构建块。
视图确保用户可以通过点击或其他动作与应用程序进行交互。
本指南将介绍不同的Compose UI元素,并了解如何构建它们。
⚾️2.1 准备
在这个指南中,我们将创建一个项目,我们将在整个章节中重复使用,所以让我们继续并按照第1章的步骤,开始与现代Android开发技能,如何创建你的第一个Android项目。
创建一个项目,并将其命名为Compose Basics。
此外,我们将主要使用预览部分来查看我们创建的UI元素。
⚾️2.2 如何实现
创建项目后,按照以下步骤构建几个Compose UI元素:
- 在我们的项目中,让我们继续创建一个新包,并将其称为components。这是我们将添加创建的所有组件的地方。
- 创建一个Kotlin文件,并将其命名为UIComponents.kt;在UIComponent中,创建一个可组合的函数,命名为EditTextExample(),并调用OutlinedTextField()函数;这将提示你导入所需的导入,即androidx.composer.material.OutlinedTextField:
@Composable fun EditTextExample() { OutlinedTextField() }
- 当您深入研究OutlineTextField时,您将注意到该函数接受多个输入,当您需要自定义自己的可组合函数时,这非常有用。
- 对于我们的示例,我们不会对我们创建的UI做太多的操作,而只是看看我们如何创建它们。
- 现在,为了完全创建我们的OutlinedTextField()基于我们看到它接受的输入类型,我们可以给它一个文本和颜色,我们可以使用Modifier()修饰它;也就是说,通过给它特定的指令,比如fillMaxWidth(),它设置了最大宽度。
当我们说fill时,我们只是指定它应该被完全填充。
我们将.padding(top)设置为16 .dp,它在dp中的内容的每个边缘上应用额外的空间。
它还有一个值,该值是要在OutlinedTextField中输入的值,还有一个onValueChange lambda侦听输入更改。 - 我们还在聚焦和不聚焦时为OutlinedText赋予一些边框颜色,以反映不同的状态。
因此,如果你开始输入,框的颜色将变为蓝色,如下代码所示:
@OptIn(ExperimentalMaterial3Api::class) @Preview @Composable fun EditTextExample() { OutlinedTextField( value = "", onValueChange = {}, label = { Text(stringResource(id = R.string.sample)) }, modifier = Modifier .fillMaxWidth() .padding(top = 16.dp), colors = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = Color.Blue, unfocusedBorderColor = Color.Black ) ) }
- 我们还有另一种类型的TextField,它没有轮廓,如果你比较OutlinedTextField作为输入的内容,你会注意到它们非常相似:
@OptIn(ExperimentalMaterial3Api::class) @Composable fun NotOutlinedEditTextExample() { TextField( value = "", onValueChange = {}, label = { Text(stringResource(id = R.string.sample)) }, modifier = Modifier .fillMaxWidth() .padding(top = 8.dp, bottom = 16.dp), colors = TextFieldDefaults.outlinedTextFieldColors( focusedBorderColor = Color.Blue, unfocusedBorderColor = Color.Black ) ) }
- 您可以通过在@Preview可组合函数中添加Compose函数来运行应用程序。
在我们的示例中,我们可以创建UIElementPreview(),这是一个用于显示UI的预览函数。
在下图中,顶视图是OutlinedTextField,而第二个视图是普通的TextField。
- 现在,让我们来看看按钮的例子。我们将看看用不同的方法创建不同形状的按钮。
如果您将鼠标悬停在Button()可组合函数上,您将看到它接受的输入,如下图所示。
在第二个示例中,我们将尝试创建一个带有图标的按钮。
此外,我们将添加文本,这在创建按钮时是至关重要的,因为我们需要向用户指定单击按钮后将执行什么操作或执行什么操作。
- 因此,继续在同一个Kotlin文件中创建一个Compose函数,并将其命名为ButtonWithIcon(),然后导入Button()可组合函数。
- 在它里面,你需要导入一个带有painterResource输入、内容描述、Modifier和色调的Icon()。
我们还需要Text(),它将给我们的按钮一个名称。对于我们的例子,我们不会使用浅色:
@Composable fun ButtonWithIcon() { Button(onClick = {}) { Icon( painterResource(id = R.drawable.ic_baseline_shopping_bag_24), contentDescription = stringResource(id = R.string.shop), modifier = Modifier.size(20.dp) ) Text(text = stringResource(id = R.string.buy), Modifier.padding(start = 10.dp)) } }
- 让我们继续创建一个新的可组合函数,并将其命名为CornerCutShapeButton();在这个例子中,我们将尝试创建一个有捷径的按钮:
@Composable fun CornerCutShapeButton() { Button(onClick = {}, shape = CutCornerShape(10)) { Text(text = stringResource(id = R.string.cornerButton)) } }
- 让我们继续创建一个新的可组合函数,并将其命名为RoundCornerShapeButton();在这个例子中,我们将尝试创建一个圆角按钮:
@Composable fun RoundCornerShapeButton() { Button(onClick = {}, shape = RoundedCornerShape(10.dp)) { Text(text = stringResource(id = R.string.rounded)) } }
- 让我们继续创建一个新的可组合函数,并将其命名为ElevatedButtonExample();在这个例子中,我们将尝试创建一个带有elevation的按钮:
@Composable fun ElevatedButtonExample() { Button( onClick = {}, elevation = ButtonDefaults.elevation( defaultElevation = 10.dp, pressedElevation = 15.dp, disabledElevation = 0.dp ) ) { Text(text = stringResource(id = R.string.elevated)) } }
- 当您运行应用程序时,您应该有一个类似于下图的图像;
TextField之后的第一个按钮是ButtonWithIcon(),第二个是CornerCutShapeButton(),第三个是RoundCornerShapeButton(),最后,我们有ElevatedButtonExample()
- 现在,让我们看最后一个例子,因为我们将在整个书中使用不同的视图和样式,并将在这个过程中学习更多。
现在,让我们看一下图像视图;Image()可组合函数接受多个输入,如下图所示。
- 在我们的例子中,Image()将只有一个painter,它是不可空的,这意味着你需要为这个可组合函数提供一个图像,一个可访问性的内容描述,和一个修饰符:
@Composable fun ImageViewExample() { Image( painterResource(id = R.drawable.android), contentDescription = stringResource(id = R.string.image), modifier = Modifier .size(200.dp) ) }
- 您还可以尝试使用其他东西,例如添加RadioButton()和CheckBox()元素并自定义它们。
当您运行您的应用程序时,您应该得到类似于下图的结果。
⚾️2.3 如何工作
每个可组合函数都用@Composable注释。该注释告诉Compose编译器,所提供的编译器打算将所提供的数据转换为UI。
同样重要的是要注意,每个可组合的函数名都需要是名词,而不是动词或形容词,谷歌提供了这些指导方针。
您创建的任何可组合函数都可以接受参数,使应用程序逻辑能够描述或修改您的UI。
我们提到了Compose编译器,这意味着编译器是任何特殊的程序,它接受我们编写的代码,检查它,并将其翻译成计算机可以理解的东西-或机器语言。
在Icon()中,painterresource指定我们要添加到按钮上的图标,内容描述有助于实现可访问性,修饰符用于修饰图标。
我们可以通过添加@Preview注释和showBackground = true来预览我们构建的UI元素:
@Preview(showBackground = true)
@Preview功能强大,我们将在以后的章节中介绍如何更好地利用它。
⚽️3. 在Jetpack Compose中实现一个可滚动列表
在构建Android应用程序时,我们都同意的一件事是你必须知道如何构建一个RecyclerView来显示你的数据。
有了我们新的、现代的Android应用程序构建方式,如果我们需要使用RecyclerView,我们可以使用LazyColumn,这是类似的。
在本指南中,我们将查看行、列和LazyColumn,并使用我们的虚拟数据构建一个可滚动列表。
此外,我们将在这个过程中学习一些Kotlin。
⚾️3.1 准备
我们将继续使用Compose Basics项目来构建可滚动列表;因此,要开始,您需要完成前面的配方。
⚾️3.2 如何实现
按照以下步骤构建第一个可滚动列表:
- 让我们继续构建第一个可滚动列表,但首先,我们需要创建虚拟数据,这是我们希望显示在列表上的项。因此,创建一个名为favoritecity的包,我们的可滚动示例将位于其中。
- 在favoritecity包中,创建一个新的数据类,并将其命名为City;这将是我们的虚拟数据源——数据类City()。
- 让我们对City数据类建模。在添加了带注释的值之后,确保添加了必要的导入:
data class City( @StringRes val nameResourceId: Int, @DrawableRes val imageResourceId: Int )
- 现在,在我们的虚拟数据中,我们需要创建一个Kotlin类,并将这个类称为CityDataSource。
在这个类中,我们将创建一个名为loadCities()的函数,它将返回list 的列表,我们将在可滚动列表中显示该列表。
检查技术要求部分的所有需要的导入,以获得所有的代码和图像:
class CityDataSource { fun loadCities(): List<City> { return listOf( City(R.string.spain, R.drawable.spain), City(R.string.new_york, R.drawable.newyork), City(R.string.tokyo, R.drawable.tokyo), City(R.string.switzerland, R.drawable.switzerland), City(R.string.singapore, R.drawable.singapore), City(R.string.paris, R.drawable.paris), ) } }
- 现在,我们有了虚拟数据,是时候将其显示在可滚动列表上了。
让我们在组件包中创建一个新的Kotlin文件,并将其命名为CityComponents。
在CityComponents中,我们将创建@Preview函数:
@Preview(showBackground = true) @Composable private fun CityCardPreview() { CityApp() }
- 在@Preview函数中,我们有另一个可组合函数CityApp();在这个函数中,我们将调用CityList可组合函数,该函数将列表作为参数。
此外,在这个可组合函数中,我们将调用LazyColumn,项目将是CityCard(城市)。
有关LazyColumn和items的进一步解释,请参阅它的工作原理一节:
@Composable fun CityList(cityList: List<City>) { LazyColumn { items(cityList) { cities -> CityCard(cities) } } }
- 最后,让我们构造CityCard(城市)可组合函数:
@Composable fun CityCard(city: City) { Card(modifier = Modifier.padding(10.dp), elevation = CardDefaults.cardElevation(4.dp)) { Column { Image( painter = painterResource(city.imageResourceId), contentDescription = stringResource(R.string.city_images), modifier = Modifier .fillMaxWidth() .height(154.dp), contentScale = ContentScale.Crop ) Text( text = LocalContext.current.getString(city.nameResourceId), modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.headlineLarge ) } } }
- 当运行CityCardPreview可组合函数时,应该有一个可滚动的列表,如下图所示。
⚾️3.3 如何工作
在Kotlin中,列表有两种类型,不可变和可变。
不可变列表是不能修改的项,而可变列表是列表中可以修改的项。
要定义列表,我们可以说列表是元素的一般有序集合,这些元素可以是整数、字符串、图像等形式,这主要取决于我们希望列表包含的数据类型。
例如,在我们的示例中,我们有一个字符串和图像来帮助通过名称和图像识别我们最喜欢的城市。
在我们的City数据类中,我们使用@StringRes和@DrawableRes是为了方便地直接从res文件夹中为Drawable和String提取数据,它们也表示图像和字符串的ID。
我们创建了CityList,并用可组合函数对其进行注释,并将城市对象列表声明为函数中的参数。
Jetpack Compose中的可滚动列表是使用LazyColumn生成的。
LazyColumn和Column之间的主要区别在于,当使用Column时,您只能显示小项,而Compose会一次加载所有项。
此外,列只能保存固定的可组合函数,而LazyColumn,顾名思义,可以根据需要加载内容,因此可以在需要时加载更多项。
此外,LazyColumn还内置了滚动功能,这使得开发人员的工作更轻松。
我们还创建了一个可组合函数CityCard,在其中从Compose导入Card()元素。
一张卡片包含关于单个对象的内容和动作;例如,在我们的示例中,我们的卡片具有图像和城市名称。
Compose中的Card()元素在其参数中有以下输入:
@Composable @ComposableInferredTarget public fun Card( modifier: Modifier, shape: Shape, colors: CardColors, elevation: CardElevation, border: BorderStroke?, content: @Composable() (ColumnScope.() -> Unit) ): Unit
这意味着你可以很容易地塑造你的卡到最适合;我们的卡片有padding和elevation, scope有column。
在本专栏中,我们有一个图像和文本,这有助于描述图像以获得更多上下文。
⚾️3.4 参考
在Compose中有更多关于列表和网格的知识要学习;您可以使用此链接了解更多信息:https://developer.android.com/jetpack/compose/lists。
⚽️4. 使用Jetpack Compose实现第一个带有视图页的选项卡布局
在Android开发中,在页面之间使用幻灯片是非常常见的,这是一个重要的用例,甚至当你试图以标签和轮播方式显示特定数据时。
在这个指南中,我们将在Compose中构建一个简单的水平分页器,并看看我们如何利用新知识来构建更好、更现代的Android应用程序。
⚾️4.1 准备
在本例中,我们将构建一个水平分页器,它在被选中时改变颜色,以显示已选中的状态。
为了更好地理解状态,我们将在第3章处理Jetpack Compose中的UI状态和使用Hilt中查看状态。打开Compose Basics项目开始。
⚾️4.2 如何实现
遵循以下步骤来构建您的选项卡轮播:
- 将以下页面依赖项添加到build.gradle(Module:app):
implementation("androidx.compose.material3:material3") implementation "com.google.accompanist:accompanist-pager:0.32.0" implementation "com.google.accompanist:accompanist-pager-indicators:0.32.0"
Jetpack Compose提供了accompanist,这是一组库,旨在支持开发人员通常需要的功能——例如,在我们的例子中,页面调度器。
- 在与前面的食谱相同的项目中,让我们创建一个名为pagerexample的包;在其中创建一个名为CityTabExample的Kotlin文件;在这个文件中,创建一个可组合的函数,并将其命名为CityTabCarousel:
@Composable fun CityTabCarousel(){}
- 现在,让我们继续构建CityTabCarousel;在我们的例子中,我们将创建一个虚拟的页面列表,其中包含我们之前项目中的城市:
@RequiresApi(Build.VERSION_CODES.M) @OptIn(ExperimentalPagerApi::class) @Composable fun CityTabCarousel( pages: MutableList<String> = arrayListOf( "Spain", "New York", "Tokyo", "Switzerland", "Singapore", "Paris" ) ) {}
- 我们需要根据状态改变按钮的颜色,为此;我们需要使用LocalContext,它提供了我们可以使用的上下文。
我们还需要创建一个var pagerState = memorberPagerState(),它将记住我们的分页状态,最后,当单击时,我们将需要移动到分页中的下一个城市,这将非常有用。
因此,在CityTabCarousel可组合函数中添加以下代码:
val context = LocalContext.current var pagerState = rememberPagerState() val coroutineScope = rememberCoroutineScope()
- 现在,让我们创建Column元素并添加ScrollableTabRow()可组合函数:
Column { ScrollableTabRow( selectedTabIndex = pagerState.currentPage, indicator = { tabPositions -> TabRowDefaults.Indicator( Modifier .pagerTabIndicatorOffset(pagerState, tabPositions) .fillMaxHeight(0f) ) }, edgePadding = 0.dp, backgroundColor = Color(context.resources.getColor(R.color.white, null)), ) { pages.forEachIndexed { index, title -> val isSelected = pagerState.currentPage == index TabHeader( title, isSelected, onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } }, ) } } HorizontalPager( count = pages.size, state = pagerState, modifier = Modifier .fillMaxWidth() .fillMaxHeight() .background(Color.White) ) { page -> Text( text = "Display City Name: ${pages[page]}", modifier = Modifier.fillMaxWidth(), style = TextStyle( textAlign = TextAlign.Center ) ) } }
- 为HorizontalPager添加Text()和TabHeader():
HorizontalPager( count = pages.size, state = pagerState, modifier = Modifier .fillMaxWidth() .fillMaxHeight() .background(Color.White) ) { page -> Text( text = "Display City Name: ${pages[page]}", modifier = Modifier.fillMaxWidth(), style = TextStyle( textAlign = TextAlign.Center ) ) }
- 请按照技术需求部分提供的链接下载此配方的完整代码,以添加所需的所有代码。
最后,运行@Preview函数,您的应用程序应该如下图所示。
⚾️4.3 如何工作
Accompanist 带有一些重要的库,例如,系统UI控制器,AppCompact撰写主题适配器,材料主题适配器,Pager, Drawable Painter和Flow Layouts,仅举几个例子。
我们在CityTabCarousel函数的Column中使用的ScrollableTabRow()包含一行选项卡,并帮助在当前聚焦或选中的选项卡下方显示指示器。
此外,顾名思义,它支持滚动,您不必实现进一步的滚动工具。
它还将标签偏移量放在起始边缘,您可以快速滚动屏幕外的标签,正如您运行@Preview功能并使用它时所看到的那样。
当我们在Compose中调用remember()时,这意味着我们在整个重组中保持任何值的一致性。
Compose提供了这个函数来帮助我们在内存中存储单个对象。
当我们触发应用程序运行时,记住()存储初始值。
顾名思义,它只是保留值并返回存储的值,以便可组合函数可以使用它。
此外,无论何时存储的值发生变化,您都可以更新它,并且remember()函数将保留它。
下次我们在应用程序中触发另一次运行并发生重组时,remember()函数将提供最新的存储值。
您还会注意到我们的MutableList在每个位置都被索引,我们这样做是为了检查哪个被选中。
在这个Lambda中,我们调用TabHeader并显示所选的选项卡页面。
forEachIndexed对每个元素执行给定的操作,提供元素的顺序索引。
我们还确保当用户单击特定选项卡时,我们在正确的页面上:
onClick = { coroutineScope.launch { pagerState. animateScrollToPage(index) } }
HorizontalPager是一个水平滚动布局,允许我们的用户从左到右在项目之间翻转。
它接受几个输入,但是我们为它提供计数、状态和修饰符,以便在我们的用例中修饰它。
在Lambda中,我们在我们的示例中显示文本,显示我们在哪个页面,这有助于导航,如下所示:
@Deprecated @Composable @ComposableInferredTarget public fun HorizontalPager( count: Int, modifier: Modifier, state: PagerState, reverseLayout: Boolean, itemSpacing: Dp, contentPadding: PaddingValues, verticalAlignment: Alignment.Vertical, flingBehavior: FlingBehavior, key: ((Int) -> Any)?, userScrollEnabled: Boolean, content: @Composable() (PagerScope.(Int) -> Unit) ): Unit
TabHeader可组合函数有一个Box();Jetpack Compose中的框将始终调整大小以适合内容,这受到指定的约束。
在我们的示例中,我们用selectable修饰符修饰Box,它将组件配置为可选的,作为互斥组的一部分,允许每个项在任何给定时间只被选中一次。
👬 交友小贴士:
博主Github,Gitee同名账号,Follow 一下就可以一起愉快的玩耍了,更多精彩文章请持续关注。
专栏推荐