玩会儿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

欢迎关注公 z 号:9点大前端,每天9点推荐更多前端、Android、Flutter文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 简介 Jetpack Compose 是 Google 官方 2019 年推出的UI框架,它可简化并加快 Andr...
    TTTqiu阅读 3,731评论 1 3
  • 邂逅FLutter 万物皆是Widget 一般缩进2个空格 文字居中 Widget Center() Materi...
    JackLeeVip阅读 3,115评论 0 4
  • Flutter 学习笔记-基础篇 如果你要获取与该笔记配套的源码,请点击这里[https://github.com...
    蜗牛学开车阅读 1,981评论 2 11
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,083评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,032评论 0 4