注册

玩会儿Compose,原神主题列表

Jetpack Compose出来有一段时间了,一直都没有去尝试,这次有点想法去玩一玩这个声明性界面工具,就以“原神”为主题写个列表吧。


整体设计参考DisneyCompose


效果图:


image.png


image.png


数据源


因为数据比较简单,也就只包含图片、姓名、描述等。所以在后台数据存储上选择的是Bmob后端云,一个方便前端开发的后端服务平台。


主要数据也是从原神各大网站搜集下来的,新建表结构并且将数据填充,我们简单看一下Bmob的后台。


image.png


数据准备好了,那就开始我们的Compose之旅。


首页UI绘制


整体结构


从上面的项目效果图来看,首页总布局属于是一个网格列表,平分两格,列表中的每个Item上方带有头像,头像下面是角色名称以及角色其他信息。


image.png


网格布局


因为整体分成两列,所以选择的是网格布局,Compose提供了一个实现-LazyVerticalGrid


fun LazyVerticalGrid(
cells: GridCells,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
content: LazyGridScope.() -> Unit
)

LazyVerticalGrid中有几个重要参数先说明一下:



  • GridCells :主要控制如何将单元格构建为列,如GridCells.Fixed(2),表示两列平分。
  • Modifier : 主要用来对列表进行额外的修饰。
  • PaddingValues :主要设置围绕整个内容的padding。
  • LazyListState :用来控制或观察列表状态的状态对象

首页布局是平分两列的网格布局,那相应的代码如下:


LazyVerticalGrid(cells = GridCells.Fixed(2)) {}

单个Item


看过了外部框架,那现在来看每个Item的布局。每个Item为卡片式,外边框为圆角,且带有阴影。内部上方是一张图片Image,图片下方是两行文字Text。那Item具体该怎样布局?


我们先来看看在Compose之前,在xml中是怎么写?例如使用ConstraintLayout布局,顶部放一个ImageView,再来一个TextView layout_constraintTop_toBottomOf ImageView,最后在来个TextViewTopToBottomOf第一个TextView


那使用Compose应该怎么写?


其实在Compose里也存在着ConstraintLayout布局并且具体Api的调用思路与在xml中使用也是一致的。我们就来看看具体操作。


ConstraintLayout() {
Image()
Text()
Text()
}

一共两个元素:ImageText,分别代表着xml里的ImageViewTextView



  • Image:

Image(
painter = rememberCoilPainter(request = item.url),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier
.clickable(onClick = {
val objectId = item.objectId
navController.navigate("detail/$objectId")
})
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.width(180.dp)
.height(160.dp)
.constrainAs(image) {
centerHorizontallyTo(parent)
top.linkTo(parent.top)
})

Image加载的是网络图片,则使用painter加载图片链接,contentScale与xml中的scaleType相似,modifier主要设置图片的样式,点击事件、宽高等。里面有一个需要注意的点constrainAs(image)


constrainAs(image) {
centerHorizontallyTo(parent)
top.linkTo(parent.top)
}

这段代码主要表示Image在父布局中的位置,例如相对父布局,相对其他子控件等,有点xml中layout_constraintTop_toBottomOf内味。下面Text也是相同的道理。



  • Text

Text(text = item.name,
color = Color.Black,
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.constrainAs(title) {
centerHorizontallyTo(parent)
top.linkTo(image.bottom)
}
)

Text的设置主要包含Text内容、文字类型、大小、颜色等。在constrainAs(title)里有一句top.linkTo(image.bottom),这句代码指的就是xml中,TextView layout_constraintTop_toBottomOf ImageView


在Image和Text中发现了一个点,constrainAs(?)中传入了一个值,且设置相对位置时也是以此值为控件的代表。这是在进行相对位置的设定之前,利用createRefs创建多个引用,在ConstraintLayout中作为Modifier.constrainAs的一部分分配给布局。


val (image, title, content) = createRefs()

具体代码:


ConstraintLayout() {
val (image, title, content) = createRefs()
//头像
Image(
//图片地址
painter = rememberCoilPainter(request = item.url),
contentDescription = "",
//图片缩放规则
contentScale = ContentScale.Crop,
modifier = Modifier
.clickable(onClick = {//点击事件
val objectId = item.objectId
navController.navigate("detail/$objectId")
})
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.width(180.dp)
.height(160.dp)
.constrainAs(image) {
centerHorizontallyTo(parent) //水平居中
top.linkTo(parent.top)//位于父布局的顶部
})
//文字
Text(text = item.name,
color = Color.Black,//颜色
style = MaterialTheme.typography.h6,//字体格式
textAlign = TextAlign.Center,
modifier = Modifier
.padding(0.dp, 4.dp, 0.dp, 0.dp)
.constrainAs(title) {
centerHorizontallyTo(parent)//水平居中
top.linkTo(image.bottom)//位于图片的下方
}
)
Text(text = item.from,
color = Color.Black,
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(4.dp)
.constrainAs(content) {
centerHorizontallyTo(parent)
top.linkTo(title.bottom)

})
}

image.png


数据填充


UI已经画好了,接下来就是数据展示的事情。还是以ViewModel-LiveData-Repository为整体请求方式。 因为数据都存储到了Bmob后台,就直接使用Bmob的方式查询数据:


private val bmobQuery: BmobQuery<GcDataItem> = BmobQuery()

fun queryRoleData(successLiveData: MutableLiveData<List<GcDataItem>>) {
bmobQuery.findObjects(object : FindListener<GcDataItem>() {
override fun done(list: MutableList<GcDataItem>?, e: BmobException?) {
if (e == null) {
successLiveData.value = list
}
}

})
}

具体的请求方式可参考Bmob的完档,这里就不在赘述。 ViewModel中还是抛出一个LiveData,而UI层相对之前有一些变化。


@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HomePoster(navController: NavController, model: HomeViewModel
= viewModel()) {
model.queryGcData()
val data: List<GcDataItem> by model.getDataLiveData().observeAsState(listOf())

LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(data) {
ItemPoster(navController, item = it)
}
}

}

Compose提供了一个viewModel()方法来获取ViewModel实例,至于怎么拿到数据,Compose提供了LiveData的一个扩展方法 observeAsState(listOf()) 。它的主要作用是用来观察这个LiveData,并通过State表示它的值,每次有新值提交到LiveData时,返回的状态将被更新,从而导致每个状态的重新组合。


拿到List数据后,网格LazyVerticalGrid就开始使用items(data){}添加列表,


 LazyVerticalGrid(cells = GridCells.Fixed(2)) {
items(data) {
ItemPoster(navController, item = it)
}
}

而ItemPoster就是我们设置Item布局的地方,将每个Item的数据传递给ItemPoster,利用Image、Text等控件设置imageUrl、text内容等。


@Composable
fun ItemPoster(navController: NavController, item: GcDataItem) {
Surface(
modifier = Modifier
.padding(4.dp),
color = Color.White,
elevation = 8.dp,
shape = RoundedCornerShape(8.dp)
)
{
ConstraintLayout() {
val (image, title, content) = createRefs()

Image(
//设置图片Url-item.url
painter = rememberCoilPainter(request = item.url),
...)

Text(text = item.name
...)

Text(text = item.from
...)
}

}

跳转


样例中还有一个从列表跳转到详情页的功能,Compose提供了一个跳转组件-navigation。这个navigation与之前管理Fragment的navigation思路也是一致的,利用NavHostController进行不同页面的管理。我们先使用 rememberNavController()方法创建一个NavHostController实例。


val navController = rememberNavController()

接着将navController与NavHost相关联,且设置导航图的起始目的地startDestination


 NavHost(navController = navController, startDestination = "Home") {}

我们将起始目的地暂时先标记为“Home”。 那如何对页面进行管理?这就需要在NavHost中使用composable添加页面,例如该项目有两个页面,一个首页列表页,一个详情页。我们就可以这样写:


 NavHost(
navController = navController, startDestination = "Home"
)
{
composable(
route = "Home",
)
{
HomePoster(navController)
}

composable("detail/{objectId}"){
val objectId = it.arguments?.getString("objectId")
DetailPoster(objectId){
navController.popBackStack()
}
}
}

第一个composable则代表的是列表页,并且将到达目的地的路线route设置为“Home”,其实类似于ARouter框架中在每个Activity上设置Path,做一个标识作用,后面做跳转时也是依据该route进行跳转。


第二个composable则代表的是详情页,同样设置route="detail"


那如何从列表页跳到详情页?只需要在点击事件里使用navController.navigate("detail"),传入想要跳转的route即可。


携带参数跳转


因为详情页需要根据所点击列表Item的Id进行数据查询,点击时要将id传到详情页,这就需要携带参数。 在Compose中,向route添加参数占位符,如"detail/{objectId}",从composable()函数提取 NavArguments。 如下修改详情页:


 composable("detail/{objectId}"){
val objectId = it.arguments?.getString("objectId")
DetailPoster(objectId){
navController.popBackStack()
}
}

跳转时将objectId传到route的占位符中即可。


clickable(onClick = {
val objectId = item.objectId
navController.navigate("detail/$objectId")})

当然,compose navigation还支持launchMode设置、深层链接等,具体可查看官方文档


一点感受


对于用习惯了xml编写UI的我来说,首次上手Compose其实还是蛮不习惯,Compose打破了原有的格局,给了我们一个全新的视角去看待Android,学完后有种“哦,原来UI还可以这么干!!”的感叹。对于Android开发者来说,其实需要这些新的路线去突破自己的固有化思维。


Compose的风格其实和Flutter有点像,估计是出于同一个爸爸的原因。但是Compose没有Flutter的无限套娃,对Android开发者来说还是比较友好的。如果想要学习Flutter,可以用Compose作为过渡。


以上便是本篇内容,感谢阅读,如果对你有帮助,欢迎点赞收藏关注三连走一波?   



项目地址:genshin-compose


0 个评论

要回复文章请先登录注册