前言
本章主要介绍下 Compose 的声明式 UI 以及初级写法;
什么是声明式UI
传统UI
传统 UI 方式来声明UI
<androidx.appcompat.widget.LinearLayoutCompat android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <androidx.appcompat.widget.AppCompatImageView android:layout_width="match_parent" android:scaleType="centerCrop" android:layout_height="300dp" />
</androidx.appcompat.widget.LinearLayoutCompat>
是通过 xml 来进行显示的,显示文字的方式是使用 TextView,它内部显示文字的方式有两种,一种是在 xml 中直接设置,通过下面这种方式设置
android:text="@string/app_name"
这种方式是通过初始值在 xml 中进行预设置的;
还有一种是在代码中直接调用 setText 进行设置
TextView textView = findViewById(R.id.text);
textView.setText("xxxx");
代码中是通过 setText 后续对值进行手动更新的;
这种需要手动对界面更新的方式 就不是声明式,属于传统式
声明式UI
指的是:在写界面的时候,只需要对界面进行一次性的声明,而不用在各种条件对界面元素进行跟条件有关的更新;
声明式 UI 自动更新界面,不需要手动更新,而传统式需要手动更新界面;
例如我们写一个 Compose 的 Text
Text( text = "Hello $name!", modifier = modifier
)
我们只需要把 name 作为参数传给这个 Text ,这个文字控件就会显示 name 的值;而声明式 UI 它不仅会用 name 来初始化显示 name 的值,当 name 的值发生变化的时候,Text 所显示的内容也会跟着发生改变,比如 name 的初始值是 Mars,界面第一时间显示的就是 Mars,过了一会这个 name 的值变成了 老A 了,那么界面就会跟着变成 老A,而不需要再次调用 setText 进行更新;这就是声明式 UI;并不是说界面是用声明式写出来的,而是说只需要声明就够了,不需要任何的手动更新;
传统式再次调用 setText 才能更新,这个调用不能省略,否则不会发生更新;
Compose 怎么写?
一种是直接创建一个 Compose 项目,一种是项目中增加配置,让它支持 Compose;
我的 Android Stuido 版本,可以直接创建 Compose 项目,并且这个版本的 AS 已经默认创建的就是 Compose 项目了;
点击 Next 创建完成;
完成之后,我们在 MainActivity 中可以看到一个大概的构建逻辑:
区别与传统的方式,我们通过一个 setContent 函数开启了我们的 Compose 之旅;
文字 Text 的声明
setContent { Text("老A")
}
运行我们的程序,然后就可以在界面上看到 老A
Text 函数还有很多其他的用法,大家根据 API 直接去探索使用即可
图片 Image 的声明
传统 ImageView 显示图片的方式有两种,一种是位图,也就是 Bitmap;一种是矢量图,也就是 VectorDrawable;
但是在 Compose 中位图提供了一个新的 API,ImageBitmap;矢量图也提供了一个新的 API,ImageVector;
到这里的时候,可能很多人就会有疑问了,为什么要区别开来?
因为 Compose 的初衷是独立于 Android 平台,这个 [独立于平台] 说的就是不依赖于最新的 Android 系统才能使用;
例如我们熟悉的 RecyclerView,ViewPager2,AppCompat,协调者布局,ViewModel 等等一系列 Jetpack 组件,任何一个库出了新版本就能直接发布,开发者就可以直接使用,不需要等升级到最新的 Android 系统版本之后才能使用;
Compose 不仅独立与最新版本的 Android ,还独立于 Android 平台;Compose 提供的所有 API 全都不带有 Android 的痕迹;
虽然 Compose 不带有 Android 痕迹了,但是它底层还是使用的 Android 的 drawText()、drawTextRun()(可以看下这个链接,针对这两个 api 的介绍)、Canvas 来进行的绘制;
更精确的说:是上层暴露给开发者的 API 是独立于 Android 平台的;
声明一个图片 使用 Image,如果想使用 drawable 文件夹下的图片,使用 painterResource 来传递一个资源 id;
Image(painterResource( id = R.drawable.ic_launcher_foreground), contentDescription = resources.getString(R.string.app_name)
)
painterResource 这个函数内部会创建一个 ImageBitmap 或者 ImageVector 对象,取决于传递进去的时候位图还是矢量图,不过这就跟外部调用的没有关系了;painter 跟 Android 中的 drawable 比较类似,我们也可以不使用 painterResource 直接传递一个 ImageBitmap、ImageVector 也是可以的;
运行效果如上;
网络图片的加载,就需要依赖第三方库了,这里推荐一个图片库 Coil (Android 官方推荐 Coroutine Image Loader)这个库是面向 kotlin 和协程的,同时它还不是面向 View 系统的,
使用的话,直接引入依赖即可
implementation("io.coil-kt:coil-compose:2.5.0")
然后还是使用 Image 函数
Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp)
)
只不过 painterResource 替换成了 rememberAsyncImagePainter,就可以传递一个网络图片url,并进行加载;
设置图片尺寸,使用 Modifier 关键字;
别忘了添加网络权限
<uses-permission android:name="android.permission.INTERNET"/>
运行效果如下:
早期的 Coil 并没有支持 Jetpack Compose,如果你使用的 Coil 版本比较低,可以使用另外一个库 Accompanist
Image 的底层也是最终调用的 Android 的原生 API drawBitmap,如果是纯色,就是 Canvas 的 drawColor 等 API;
图标 Icon 的声明
在 Compose 中,图标的声明使用 Icon 函数;
Column {Icon( imageVector = Icons.Filled.Favorite, contentDescription = "")
}
运行效果如下:
Icon 的用法基本上和 Image 上一样;
按钮 Button 的声明
在Compose当中,Button 和 Text 之间并没有什么关系。它们是两个独立的控件,并且通常它们还需要配合在一起使用才行;
先来看下不配合 Text 的效果
Button(onClick = {}) { }
运行效果如下:
按钮是没有文案的,需要给 Button 设置一个文案
Button(onClick = {}) { Text(text = "我是老A")
}
运行效果如下:
点击事件的话,就直接在 onClick = { } 中处理即可
Button(onClick = { Toast.makeText(LocalContext.current as Context, "", Toast.LENGTH_LONG).show()
}) { Text(text = "我是老A")
}
输入框 TextFiled 的声明
在 Compose 中,使用 TextFiled 代替了 EditText
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = {Text(text = "用户名")})
运行效果如下:
可以看到,实现了 EditeText 的效果,还是 Material 风格的;
TextField 函数还有很多其他的用法,大家根据 API 直接去探索使用即可;
使用 Layout 的 Compose 平替
Android 中常用的布局有 FrameLayout、 LinearLayout、RelativeLayout、ConstraintLayout、ScrollView、RecyclerView、ViewPager 等等
Compose 中的平替控件
FrameLayout -> Box()
Box {Text(text = "老A"color = Color(0XFF886600) )Image(rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp))
}
运行效果如下:
可以看到,和上面截图排列一样,也就是说,不使用布局控件,默认就是 FrameLayout排列;
LinearLayout -> Column()、Row() 表示纵向和横向;
Column {Text(text = "老A"color = Color(0XFF886600) )Image(rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp))
}
运行效果如下:
Row {Text(text = "老A"color = Color.Blue)Image(rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp))
}
运行效果如下:
RelativeLayout -> Box()
RelativeLayout 也是使用 Box 平替,但是通过 Modifier 来修改相对位置;
Box(modifier = Modifier.border(2.dp, Color.Black).size(200.dp)){ Text( text= "老A", color = Color.Blue ) Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp).align(Alignment.CenterEnd) )
}
运行效果如下:
ConstraintLayout -> ConstraintLayout for Compose
因为约束布局非常好用,所以官方为我们迁移到了 Compose 平台,我们可以直接使用这个控件;这里并不是套壳支持,而是把它们的逻辑移植到了 Compose 里面;
implementation("androidx.constraintlayout:constraintlayout-compose:+")
ConstraintLayout(modifier = Modifier .border(2.dp, Color.Black) .size(400.dp) .padding(10.dp)) { val (portraitImageRef, usernameTextRef, desTextRef) = remember { createRefs() } Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = "", modifier = Modifier.size(150.dp).constrainAs(portraitImageRef){ top.linkTo(parent.top) bottom.linkTo(parent.bottom) start.linkTo(parent.start) }) Text( text = "我是老A", fontSize = 16.sp, maxLines = 1, textAlign = TextAlign.Left, modifier = Modifier.constrainAs(usernameTextRef) { top.linkTo(portraitImageRef.top) start.linkTo(portraitImageRef.end, 10.dp) }) Text( text = "这是我的个人描述", fontSize = 12.sp, maxLines = 1, color = Color.Red, fontWeight = FontWeight.Light, modifier = Modifier.constrainAs(desTextRef) { top.linkTo(usernameTextRef.bottom, 5.dp) start.linkTo(portraitImageRef.end, 10.dp) })
}
运行效果如下:
使用 RecyclerView(竖向) 的 Compose 平替 -> LazyColumn
val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn { // 没有 adapter,没有 ViewHolder items(names.size) { Text(text = names[it]) }
}
运行效果如下:
items 是遍历,item 是设置单个列表项,而且可以使用 item 来添加header、footer,也可以设置不同的 itemType,也就是不同的 ViewHolder;
val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn { item { Text(text = "header") } items(names.size) { Text(text = names[it]) } item { Text(text = "footer") }
}
运行效果如下:
设置不同的 itemType,
val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul")
LazyColumn { item { Text(text = "header") } items(names.size) { Text(text = names[it]) } item { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp) ) } items(names.size) { Text(text = names[it]) } item { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp) ) }
}
运行效果如下:
可以看到,直接就可以滑动了,也就是实现了 Rechyclerview 的效果;
使用 RecyclerView(横向) 的 Compose 平替 -> LazyRow
val names = listOf("Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul", "Jordan", "Kobe", "James", "Wade", "Paul")
LazyRow { item { Text(text = "header") } items(names.size) { Text(text = names[it]) } item { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp) ) } items(names.size) { Text(text = names[it]) } item { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp) ) }
}
运行效果如下,横向滑动的列表:
使用 ScrollView(竖向) 的 Compose 平替 -> Modifier.verticalScroll
Android 中还有一个滑动组件:ScrollView,它的内部不是动态的,它内部的内容都是预先编排好的,而不是让它自己算的,它内部的布局也是在第一时间就加载好的,而不是滑动到什么地方才加载,如果 ScrollView 加载一个很长很长的布局,有可能会内存溢出的;
而在 Compose 中的平替是没有 ScrollView 的概念的,你只需要给你需要滑动的组件加上 Modifier.verticalScroll() 函数就可以实现竖向滑动;
Column(Modifier.requiredSize(200.dp) .background(Color.Blue) .verticalScroll(rememberScrollState())) { repeat(20) { Text( text= "老A", color = Color.Red ) }
}
运行效果如下:
使用 ScrollView(横向) 的 Compose 平替 -> Modifier.horizontalScroll
Row(Modifier.requiredSize(200.dp) .background(Color.Blue) .horizontalScroll(rememberScrollState())) { repeat(20) { Text( text= "老A", color = Color.Red ) }
}
运行效果如下:
使用 ViewPager(竖向) 的 Compose 平替 -> VerticalPager
VerticalPager(10) { page -> Text( text = "Page: $page", modifier = Modifier.fillMaxWidth() )
}
运行效果如下:
使用 ViewPager(横向) 的 Compose 平替 -> HorizontalPager
HorizontalPager(10) { page -> Text( text = "Page: $page", modifier = Modifier.fillMaxWidth() )
}
这里就不运行显示了,大家可以自己运行看下效果
Modifier
padding & margin
现在我们知道怎么去写布局了,其中线性布局是我们开法中用的最多的布局了,但是我们一般并不只是在那放一个线性布局就OK了,而是我们需要做很多很多的细节设置,比如我们需要去设置一些边距,margin、padding 等等,而在 Compose 中需要使用另外一种方式来设置边距,它就是 Modifier;
Modifier 是 Compose 中的一个很重要的角色,UI 的很多设置都是通过 Modifier 来完成的,通过官方文档,我们可以知道,Modifier 有如下四个作用:
借助修饰符(Modifier),您可以修饰或扩充可组合项。您可以使用修饰符(Modifier)来执行以下操作:
- 更改可组合项的大小、布局、行为和外观;
- 添加信息,如无障碍标签;
- 处理用户输入;
- 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放;
比如,我们想给一个 Row 设置一个 10dp 的 padding,我们可以这么实现
Row(Modifier.padding(8.dp)) {}
但是 Compose 中的 Modifier 是没有 margin 的概念的,我们在 Andriod 中使用 padding 和 margin 来设置内边距和外边距,通常是因为背景色的原因,margin 是外边距,它是不包含在背景之内的,而 padding 它是内边距;
我们在 Compose 中设置背景色的时候,也是通过 Modifier 来执行的,Modifier.background(),但是 Modifier 对调用顺序是有先后要求的,我们可以看下下面的这个例子,以及我们如果和通过 padding 来实现 margin 的效果:
Row( Modifier .background(Color.Red) .padding(10.dp)) { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp) ) Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp)) Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}
运行效果:
可以看到 我是老A 是把背景色包含进来了,而 老A是我 是不包含的,也是 Modifier 的一个特点,设置有先后;
这样,我们就只需要 padding 这一个函数就可以了;
background
我们看到,background 其实是有两个参数的,一个设置背景色,一个是 Shape,也就是形状,也就是说,我们可以给 background 设置一个形状,我们来看下效果
Row( Modifier .background(Color.Red, RoundedCornerShape(10.dp)) // 给背景设置一个圆角 .padding(10.dp)) { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.size(150.dp) ) Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp)) Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}
运行效果如下:
我们的 CardView,可以通过 Modifier 来实现;
clip(shape: Shape)
Modifier 还提供了一个切割函数 clip,可以切各种东西,例如可以切图片,我们看下面这个例子
Row( Modifier .background(Color.Red, RoundedCornerShape(10.dp)) // 给背景设置一个圆角 .padding(10.dp)) { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), // 将图片切成圆的 modifier = Modifier.clip(CircleShape).size(150.dp) ) Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp)) Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}
运行效果如下:
因为图片的左右是透明的,所以看不出来效果;已经把图片切割成圆的了;
layout_width & layout_height 的 Compose 平替 size
layout_width 可以用 width 来替换,layout_height 可以用 height 来替换,如果宽高一样的话,可以直接使用 size
Row( Modifier .background(Color.Red, RoundedCornerShape(10.dp)) .width(400.dp) // 设置宽度 .height(800.dp) // 设置高度 .padding(10.dp)) { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.clip(CircleShape).size(150.dp) ) Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp)) Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}
在 Android 中,我们需要强制给每个控件声明 width 和 height,但是在 Compose 中,这个并不是强制的了,因为每个组件都有一个默认规则,默认规则就是,如果你不写,那么它就相当于是传统 View 的 wrap_content 的了;
如果想使用类似 match_parent 的,则使用 Modifier.fillMaxWidth/ fillMaxHeight/ fillMaxSize 函数;
给 Text 设置颜色和大小
如果我们想给 Text 设置颜色和大小,可能第一时间我们就想到了使用 Modifier,但是,可能没有你想的那么简单,Modifier 并没有提供相关API,我们可以往上看 Text 的基础使用,发现其实 Text 的 API 提供了相关设置;
我们在声明 Text 的时候,直接使用 color 和 fontSize 来设置颜色和大小,而不用通过 Modifier,到这里的时候,可能大家就比较迷糊了,为啥还不统一呢,是不是 Jetpack Compose 的作者脑子没想好呢?然而并不是,因为 Compose 对于 Android 团队来说,是一次重要的革命,Android 团队其实在 Compose 上下了很大的功夫的;判断是 Modifier 还是函数参数,其实很简单:
通用的设置方式,使用 Modifier;专项的设置,使用函数参数;
给控件设置点击监听
Android 中其实任何控件都可以设置监听的,这其实可以看做通用的设置方式,也就是说可以通过 Modifier 来给任意的控件设置一个监听
Row( Modifier .background(Color.Red, RoundedCornerShape(10.dp)) .width(400.dp) .height(800.dp) .padding(10.dp) // 给 Row 设置一个监听 .clickable { Toast.makeText(context, "我是 Row,我被点击了", Toast.LENGTH_LONG).show() }) { Image( rememberAsyncImagePainter("https://img2.baidu.com/it/u=1866466367,1635278877&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=734"), contentDescription = resources.getString(R.string.app_name), modifier = Modifier.clip(CircleShape).size(150.dp) ) Text(text = "我是老A", Modifier.background(Color.Green).padding(10.dp)) Text(text = "老A是我", Modifier.padding(10.dp).background(Color.Green))
}
运行效果如下:
可以看到,点击事件被执行了;
好了,Compose 的初探,就先到这里吧~~
下一节预告
状态订阅和自动更新;