• 注册
  • Android博客 Android博客 关注:0 内容:1410

    玩会儿Compose,原神主题列表

  • 查看作者
  • 打赏作者
  • 当前位置: 职业司 > Android开发 > Android博客 > 正文
    • Android博客
    • 「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

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

      整体设计参考DisneyCompose

      效果图:

      玩会儿Compose,原神主题列表

      玩会儿Compose,原神主题列表

      数据源

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

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

      玩会儿Compose,原神主题列表

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

      首页UI绘制

      整体结构

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

      玩会儿Compose,原神主题列表

      网格布局

      因为整体分成两列,所以选择的是网格布局,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)
      })
      }
      复制代码

      玩会儿Compose,原神主题列表

      数据填充

      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

      请登录之后再进行评论

      登录

      手机阅读天地(APP)

      • 微信公众号
      • 微信小程序
      • 安卓APP
      手机浏览,惊喜多多
      匿名树洞,说我想说!
      问答悬赏,VIP可见!
      密码可见,回复可见!
      即时聊天、群聊互动!
      宠物孵化,赠送礼物!
      动态像框,专属头衔!
      挑战/抽奖,金币送不停!
      赶紧体会下,不会让你失望!
    • 实时动态
    • 签到
    • 做任务
    • 发表内容
    • 偏好设置
    • 到底部
    • 帖子间隔 侧栏位置:
    • 还没有账号?点这里立即注册