Android Compose自推出正式版本后,google 就一直推荐使用Compose来开发。正好疫情期间,作为一个 Android 摸鱼达人,就来摸索一下Compose的开发。说实话开发了2天感觉对Android 开发人员来说变化是巨大的,但是作为从业者我们还必须学习和学会,才能不被甩开。
学习使用 Compose 我们需要坐什么呢?
1.使用 Kotlin
2.使用Android Studio 最新版本开发工具
3.去官网查看教程,跟着google 一步一个脚印的学习->官方文档速转通道
- Jetpack Compose 使用入门
https://developer.android.google.cn/jetpack/compose/documentation - Compose 教程
https://developer.android.google.cn/courses/pathways/compose - Compose 示例
https://github.com/android/compose-samples - Codelab: 在 Jetpack Compose 中使用状态
https://developer.android.google.cn/codelabs/jetpack-compose-state
4.找其他开发者的文档,更详细更效率的学习
- https://jetpackcompose.cn/docs文档和开源项目
- https://github.com/Gurupreet/ComposeCookBook开源项目
下载工具
使用 Android Studio Bumblebee 大黄蜂版本,如果不想影响老版本 Android Studio 的运行项目,可以下载 zip 的版本。AS尽可能升级到最新版,最少也要Arctic Fox,老版本无法实现 Compose 的预览和开发
官方下载地址:https://developer.android.google.cn/studio
快速下载地址:https://www.androiddevtools.cn/
下载后创建 Compose 项目,如果你电脑配置的 java为11,那么就没问题。如果出现问题如下:
Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8.
You can try some of the following options:
- changing the IDE settings.
- changing the JAVA_HOME environment variable.
- changing
org.gradle.java.home
ingradle.properties
.
Gradle settings
解决方案为:
1.更新 java 到11,完美。虽然你根本用不到11
2.到系统设置中Preferences -> Build Tools ->Gradle , 选择 Gradle JDK 为11
Compose项目构成
在项目可以正式运行后,我们可以先打开MainActivity,继承ComponentActivity()包含了setContent()方法,如果去除setContent内的代码运行可以看到一个空页面。
然后我们添加一个文本控件Text,对应原生的TextView。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello world!")
}
}
}
项目运行起来后我们看下整个项目的代码是怎么样的。
Compose 的引入
buildscript {
ext {
compose_version = '1.0.1'
}
}
//compose 核心
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
另外 google 提供了非常多的compose 库,具体查看可以到https://developer.android.google.cn/jetpack/androidx/explorer查看。
初识组件
Compose 内部的组件非常少,但是合理的搭配可以做到比安卓原生更好的开发体验。大体如下:
分类 | Compose | 原生 |
---|---|---|
文本 | Text | TextView |
文本 | TextField | Edittext |
图片 | Icon | ImageView |
图片 | Image | ImageView |
Button | Button | Button |
Button | IconButton | Button |
Button | IconToggleButton | CheckBox,Switch |
Button | Switch | Switch |
Button | RadioButton | RadioButton |
Button | Checkbox | CheckBox |
布局 | Box | FrameLayout |
布局 | Column | LinearLayout |
布局 | Row | LinearLayout |
布局 | Scaffold | 无 |
布局 | Constraintlayout | Constraintlayout |
列表 | LazyColumn | RecyclerView,ListView |
列表 | LazyRow | RecyclerView,ListView |
间距类 | Spacer | Space(估计很多人都没用过) |
我们直接在setContent添加个 Text 和 Image 控件,下面代码就是最简单的写法。
Text、Image应该是我们最常用的控件,特别是 Text 可实现的功能特别多,具体可以去看后续文章或者看下其他博客。
setContent {
Text("Hello world!")
Image(
painter = painterResource(R.drawable.share_calc),
contentDescription = null
)
}
上面的展示代码会使内容重叠,需要使用 Row 或者 Column。
按照下面展示,我们添加了Column垂直布局,控件就往下绘制不重叠了。
Row 、Column、Box
Column 垂直布局 -- 对用 LinearLayout 的android:orientation="vertical";Row 水平布局 -- 对用 LinearLayout 的android:orientation="horizontal";Box接近FrameLayout,具体使用可以看下图的例子.
Compose 中的三个基本标准布局元素是 Column、Row 和 Box 可组合项。
Column(){
Text("Hello world!")
Image(
painter = painterResource(R.drawable.share_calc),
contentDescription = null
)
}
Row(){
Text(" Row W")
Image(
painter = painterResource(R.drawable.share_calc),
contentDescription = null
)
}
Box(
modifier = Modifier
.background(Color.Cyan)
.size(180.dp)
) {
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.size(60.dp, 60.dp)
.background(Color.Red)
) {
}
Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.size(60.dp, 60.dp)
.background(Color.Blue)
) {
}
}
Scaffold脚手架和Constraintlayout布局
Scaffold脚手架我们先看下源码,可以看到包含了topBar、bottomBar、floatingActionButton等控件的页面。这是个固定布局,当我们的首页符合该页面固定样式时,可以直接使用该布局。
Constraintlayout布局和Android 原生一样
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
isFloatingActionButtonDocked: Boolean = false,
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
content: @Composable (PaddingValues) -> Unit
)
TextField文本输入控件
@Composable
fun InputText() {
val input = remember { mutableStateOf("") }
TextField(value = input.value, onValueChange = { input.value = it })
}
Icon 和 Image图片类
Row {
Icon(painter = painterResource(R.drawable.database_set), contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Image(
painter = painterResource(R.drawable.share_calc),
contentDescription = null
)
}
Button 类
Compose Button多种多样,具体的大家可以先看 UI 展示,就能明白在具体场景下使用什么 button。估计是参考的H5前端的按钮样式
Button 就是带背景色的按钮,OutlinedButton是带边框绘制的,TextButton是无背景色的按钮。其他都可以参考原生的样式
Row() {
//普通按钮
Button(onClick = { /*TODO*/ }) {
Text(text = "Button")
}
Spacer(modifier = Modifier.width(8.dp))
//带边框绘制的 button
OutlinedButton(onClick = { /*TODO*/ }) {
Text(text = "OutlinedButton")
}
Spacer(modifier = Modifier.width(8.dp))
//纯文字按钮
TextButton(onClick = {}) {
Text("TextButton")
}
}
Row() {
//带Icon的按钮
//圆形水波纹和 Checkbox,Switch 水波纹效果一致,
//android 可以看下设置android:background="?attr/selectableItemBackgroundBorderless"("?attr/actionBarItemBackground")的效果
IconButton(modifier = Modifier.width(120.dp), onClick = { /*TODO*/ }) {
Row() {
Icon(
painter = painterResource(id = R.drawable.database_set),
contentDescription = null
)
Spacer(modifier = Modifier.width(8.dp))
Text("IconButton")
}
}
//图标切换 button,类似于switch,但是更接近于CheckBox
//圆形水波纹和Checkbox,Switch 水波纹效果一致,
//android 可以看下设置android:background="?attr/selectableItemBackgroundBorderless"("?attr/actionBarItemBackground")的效果
IconToggleButton(checked = false, onCheckedChange = {}) {
Text(text = "IconToggleButton")
}
//悬浮按钮
ExtendedFloatingActionButton(text = { Text(text = "FAB") }, onClick = { })
}
RadioButton\Checkbox和原生对应,但是没 text 属性,如果需要显示文本需要和 Text 控件组合使用
RadioDefault()
RadioText()
RadioButtonGroup()
Row() {
CheckboxDefault()
SwitchDefault()
}
@Composable
fun RadioDefault() {
val isSelected = remember { mutableStateOf(false) }
RadioButton(selected = isSelected.value, onClick = {
isSelected.value = !isSelected.value
Log.e(TAG, "默认Checkbox,同RadioButton不带text文本控件")
})
}
@Composable
fun RadioText() {
val isSelected = remember { mutableStateOf(false) }
// 如果需要 text 需要和 Text 组合使用,click处理在row上
Row(modifier = Modifier.clickable {
isSelected.value = !isSelected.value
}) {
RadioButton(
selected = isSelected.value, onClick = null,
colors = RadioButtonDefaults.colors(
selectedColor = MaterialTheme.colors.primary,
unselectedColor = MaterialTheme.colors.error,
disabledColor = MaterialTheme.colors.secondary
)
)
Text("选项②")
}
}
@Composable
fun RadioButtonGroup() {
val options = listOf("选项③", "选项④", "选项⑤", "选项⑥")
val selectedButton = remember { mutableStateOf(options.first()) }
//RadioButton 不带 text 文本控件,
Row() {
options.forEach {
val isSelected = it == selectedButton.value
Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable {
selectedButton.value = it
}) {
RadioButton(
selected = isSelected, onClick = null,
)
Text(it, textAlign = TextAlign.Justify)
}
}
}
}
@Composable
fun CheckboxDefault() {
val isSelected = remember { mutableStateOf(false) }
Checkbox(checked = isSelected.value, onCheckedChange = {
isSelected.value = it
Log.e(TAG, "默认Checkbox,同RadioButton不带text文本控件")
})
}
@Composable
fun SwitchDefault() {
val isSelected = remember { mutableStateOf(false) }
Switch(checked = isSelected.value, onCheckedChange = {
isSelected.value = it
Log.e(TAG, "默认Checkbox,同RadioButton不带text文本控件")
})
}
定义一个带 Text 的 RadioButton
@Composable
fun RadioText() {
val isSelected = remember { mutableStateOf(false) }
// 如果需要 text 需要和 Text 组合使用,click处理在row上
Row(modifier = Modifier.clickable {
isSelected.value = !isSelected.value
}) {
RadioButton(
selected = isSelected.value, onClick = null,
colors = RadioButtonDefaults.colors(
selectedColor = MaterialTheme.colors.primary,
unselectedColor = MaterialTheme.colors.error,
disabledColor = MaterialTheme.colors.secondary
)
)
Text("选项②")
}
}
Compose 无RadioGroup 组件,如果需要实现RadioGroup也需要组合使用
定义 RadioGroup 组件,在循环中改变状态
@Composable
fun RadioButtonGroup() {
val options = listOf("选项③", "选项④", "选项⑤", "选项⑥")
val selectedButton = remember { mutableStateOf(options.first()) }
//RadioButton 不带 text 文本控件,
Row() {
options.forEach {
val isSelected = it == selectedButton.value
Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable {
selectedButton.value = it
}) {
RadioButton(
selected = isSelected, onClick = null,
)
Text(it, textAlign = TextAlign.Justify)
}
}
}
}
同理Checkbox和Switch控件就不细说了。对应的参数改成了checked和onCheckedChange,也是没有 text 属性,需要组合使用。
@Composable
fun CheckboxDefault() {
val isSelected = remember { mutableStateOf(false) }
Checkbox(checked = isSelected.value, onCheckedChange = {
isSelected.value = it
Log.e(TAG, "默认Checkbox,同RadioButton不带text文本控件")
})
}
@Composable
fun SwitchDefault() {
val isSelected = remember { mutableStateOf(false) }
Switch(checked = isSelected.value, onCheckedChange = {
isSelected.value = it
Log.e(TAG, "默认Checkbox,同RadioButton不带text文本控件")
})
}
LazyColumn、LazyRow
LazyColumn 和 LazyRow 相当于 Android View 中的 RecyclerView。它们只会渲染屏幕上可见的内容,从而在渲染大型列表时提升效率。
注意:LazyColumn 不会像 RecyclerView 一样回收其子级。它会在您滚动它时发出新的可组合项,并保持高效运行,因为与实例化 Android Views 相比,发出可组合项的成本相对较低。
@Composable
fun RecyclerView(names: List = List(1000) { "$it" }) {
LazyColumn() {
items(items = names) { item ->
MessageCard(item)
}
}
}
@Composable
fun MessageCard(msg: String) {
var isExpanded by remember { mutableStateOf(false) }
val surfaceColor: Color by animateColorAsState(
if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
)
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
isExpanded = !isExpanded
}
.padding(8.dp)
) {
Image(
painter = painterResource(R.drawable.share_calc),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))
// We toggle the isExpanded variable when we click on this Column
Column() {
Text("当前索引:")
Spacer(modifier = Modifier.height(4.dp))
Surface(
shape = MaterialTheme.shapes.medium,
elevation = 1.dp,
// surfaceColor color will be changing gradually from primary to surface
color = surfaceColor,
// animateContentSize will change the Surface size gradually
modifier = Modifier
.animateContentSize()
.padding(1.dp)
) {
Text(
text = "索引为--------> $msg ,这是一个可展开和关闭的 Text 控件:" +
"微凉的晨露 沾湿黑礼服\n" +
"石板路有雾 父在低诉\n" +
"无奈的觉悟 只能更残酷\n" +
"一切都为了通往圣堂的路",
modifier = Modifier.padding(all = 4.dp),
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.body2
)
}
}
}
}
这边定义了一个内容为1000行的 List,使用LazyColumn包裹,同时在 MessageCard 中对点击坐了处理,点击后放开 Text 的行数限制。具体效果如下
这篇博客只是只是展示了控件的简单使用,快速的了解控件的创建及展示效果。
下一篇我们介绍如何跳转页面。