2

【带源码】我又用Jetpack Compose做了个示例App,是怎样的体验?

 2 years ago
source link: https://rengwuxian.com/jetpack-compose-weiguan-app/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【带源码】我又用Jetpack Compose做了个示例App,是怎样的体验?

下面是视频内容的脚本整理稿。如果你看了视频,那下面的文稿就不用看了,直接翻到底部就行。

大家好,我是扔物线朱凯。

去年我发过几个视频讲 Compose,有做 Compose 的介绍的,有讲原理的,但唯独没有实际代码的讲解。倒是有一节我的 Compose 课程的试听课我也放了出来,但是那个是课程,是三个小时的,有人嫌长,嫌啰嗦。

我真是……

所以今天,我就给大家来一个 Compose 代码的演示。

这个,是我用 Compose 写的一个叫做「围观」的虚构的 App,「虚构」的意思就是里面的功能是我编的,因为我只是要把界面和交互给做出来。这界面看着复杂,但是用 Compose 写起来其实特别简单。今天我就用最流畅的方式给大家讲解一下它的代码,让大家看看 Compose 的界面代码到底长什么样。

我先简单介绍一下这个软件的「功能」。「功能」两个字得加个引号,因为都是假的嘛。

首先顶部是一些基本信息,例如当前用户是谁、通知按钮,以及一个搜索框。
修- 功能.png
然后中间就是围观的内容。
修-内容.png
最上面你可以选择你要围观谁——实际上是选不了的啊,我没写这个功能,所以我就不点了;往下是这个你围观的人他喜欢的东西,比如你看这是在围观我对吧,我就喜欢这么几个东西。你也可以点击其中一项,来查看详情。
修-详情.png
详情页的顶部是一个预览图,图的下方就是各种详细信息。

然后关掉详情页之后,下方是这个被围观的人最近的行动轨迹。
修-行动轨迹.png
在页面的最下方,是一个切换标签页的导航条,不过这个条的导航功能我也没做,只做了点击效果。

就这么简单的一个虚构的示例 App,叫做《围观》。那么接下来我们就来看看它的代码是怎么写的。

首先,由于它的界面是用 Compose 写的,所以我要把 setContentView() 删掉,换成 Compose 专用的 setContent {}

setContentView(R.layout.activity_main)

...

setContent {
  
}
Markdown

然后就可以写内容了。

鉴于这是个上下结构的分栏布局,所以二话不说,我先把底部的导航条给做了。用一个 Row() 来做横向布局,里面用 4 个连续的 Icon() 来显示 4 个图标:

setContent {
  NavBar()
}

->
  
@Composable
private fun NavBar() {
  Row {
    Icon(painterResource(R.drawable.icon1), "图标")
    Icon(painterResource(R.drawable.icon2), "图标")
    Icon(painterResource(R.drawable.icon3), "图标")
    Icon(painterResource(R.drawable.icon4), "图标")
  }
}
Markdown

很难看,没关系,我去调整一下这些图标。我把图标抽到单独的函数里:

再调整一下图标的尺寸:

再归整一下它们的位置布局:

就一下子变得整齐了。

然后我再给它们涂上色:

哎,就又好看了很多。

这时候为了让这些图标有点击的波纹效果,我再把它外面包上一层 Button():

好,这样底栏就完成了。

代码的细节我在这个视频里就不深入拆解了,今天主要是快速演示、快速感受,让你感受一下用 Compose 写界面大概是怎么一回事。要看细节的可以去看这个项目的源码,扫码加我的助教,让她发给你就行,暗号「围观」。或者看我的 Compose 课程的试听课也行,那个也很细,也可以加助教让助教发你。

导航条完成之后,就可以做上面的部分了。那么在做这部分之前,我要先用一个 Column() 来做纵向布局来把整个界面上下分开,下面是导航条,上面就是我接下来要写的部分:

Column {
    
    NavBar()
}
Markdown

由于这一部分的内部也是纵向的布局,所以我再套一层 Column()

Column {
    Column {
    }
    NavBar()
}
Markdown

然后加个 fillMaxWidth() 让它横向撑满;再给它个 weight(),让它纵向撑满剩余空间,也就是把导航条踩到底:

Column {
  Column(Modifier
      .fillMaxWidth()
      .weight(1f)
      .background(Color(0xfffafafa))) {
  }
  NavBar()
}
Markdown

好,接下来就要填充上面这一大块空白了。

从上到下地做,所以第一步是顶部的基本信息的横条。

我可以先用一个 Row() 来给出横向的布局:

Column {
  Column(
    Modifier
      .fillMaxWidth()
      .weight(1f)
      .background(Color(0xfffcfcfc))
  ) {
    Row {
      
    }
  }
  NavBar()
}
Markdown

然后往里面依次塞一个头像的图片:

Column {
  Column(
    Modifier
      .fillMaxWidth()
      .weight(1f)
      .background(Color(0xfffcfcfc))
  ) {
    Row {
      Image(
        painterResource(id = R.drawable.avatar_rengwuxian),
        "头像",
      )
    }
  }
  NavBar()
}
Markdown

一个纵向包着两块文字的 Column()

Column {
    Column(
        Modifier
            .fillMaxWidth()
            .weight(1f)
            .background(Color(0xfffcfcfc))
    ) {
        Row {
            Image(
                painterResource(id = R.drawable.avatar_rengwuxian),
                "头像",
            )
            Column {
                Text("欢迎回来!")
                Text("小朱")
            }
        }
    }
    NavBar()
}
Markdown

和一个小铃铛:

Column {
    Column(
        Modifier
            .fillMaxWidth()
            .weight(1f)
            .background(Color(0xfffcfcfc))
    ) {
        Row {
            Image(
                painterResource(id = R.drawable.avatar_rengwuxian),
                "头像",
            )
        }
        Column {
            Text("欢迎回来!")
            Text("小朱")
        }
        Image(
            painterResource(R.drawable.notification_new),
            "通知"
        )
    }
    NavBar()
}
Markdown

东西都显示出来了,接下来给它们调调样式。

头像做成圆角的,尺寸也调一下:

Image(
    painterResource(id = R.drawable.avatar_rengwuxian),
    "头像",
    Modifier
        .size(64.dp)
        .clip(CircleShape)
)
Markdown

文字的颜色和样式也打磨打磨:

Text("欢迎回来!", fontSize = 14.sp, color = Color(0xffb4b4b4))
Text("小朱", fontSize = 18.sp, fontWeight = FontWeight.Bold)
Markdown

小铃铛给缩小点:

Image(
    painterResource(R.drawable.notification_new),
    "通知",
    Modifier.size(32.dp)
)
Markdown

再加个圆形的底子:

Surface(
    Modifier.clip(CircleShape),
    color = Color(0xfffef7f0)
) {
    Image(
        painterResource(R.drawable.notification_new),
        "通知",
        Modifier.padding(10.dp).size(32.dp)
    )
}
Markdown

样式调好之后,再统一调整一下它们的布局:

Row(
    Modifier.fillMaxWidth(),
    verticalAlignment = Alignment.CenterVertically
) {
    Image(
        painterResource(id = R.drawable.avatar_rengwuxian),
        "头像",
        Modifier.size(64.dp).clip(CircleShape)
    )
    Column(
        Modifier.padding(start = 14.dp).weight(1f)
    ) {
        Text("欢迎回来!", fontSize = 14.sp, color = Color(0xffb4b4b4))
        Spacer(modifier = Modifier.height(6.dp))
        Text("小朱", fontSize = 18.sp, fontWeight = FontWeight.Bold)
    }
    Surface(
        Modifier.clip(CircleShape),
        color = Color(0xfffef7f0)
    ) {
        Image(
            painterResource(R.drawable.notification_new),
            "通知",
            Modifier
                .padding(10.dp)
                .size(32.dp)
        )
    }
}
Markdown

然后给整个 Row() 加上个 padding()

Row(
    Modifier
        .fillMaxWidth()
        .padding(28.dp, 36.dp, 28.dp, 16.dp),
    verticalAlignment = Alignment.CenterVertically
)
Markdown

好,这个顶栏就完成了。我把它的代码也抽到单独的函数里,干干净净:

Column {
    Column(
        Modifier
            .fillMaxWidth()
            .weight(1f)
            .background(Color(0xfffcfcfc))
    ) {
        TopBar()
    }
    NavBar()
}

往下是一个搜索栏:
Column(...) {
    TopBar()
    SearchBar()
}

->

@Composable
fun SearchBar() {

}
Markdown

一样的写法,基本没什么好说的:

@Composable
fun SearchBar() {
  Row(
    Modifier
      .padding(24.dp, 12.dp)
      .fillMaxWidth()
      .height(56.dp)
      .clip(RoundedCornerShape(28.dp))
      .background(Color.White), verticalAlignment = Alignment.CenterVertically
  ) {
    var searchText by remember { mutableStateOf("") }
    BasicTextField(
      searchText, { searchText = it }, Modifier
        .padding(start = 24.dp)
        .weight(1f), textStyle = TextStyle(fontSize = 16.sp)
    ) {
      if (searchText.isEmpty()) {
        Text("搜搜看?", color = Color(0xffb4b4b4), fontSize = 16.sp)
      }
      it()
    }
    Box(
      Modifier
        .padding(6.dp)
        .fillMaxHeight()
        .aspectRatio(1f)
        .clip(CircleShape)
        .background(Color(0xfffa9e51))
    ) {
      Icon(
        painterResource(R.drawable.ic_search), "搜索",
        Modifier
          .size(24.dp)
          .align(Alignment.Center),
        tint = Color.White
      )
    }
  }
}
Markdown

只有一点:它的提示文字的写法和传统的 EditText()不一样,是我自己手写的:

这种写法看起来比较麻烦,但是极大地增加了灵活性。比如我可以给它加个图标、加个帮助文字、报错文字,直接加就行,而且格式完全不受限制,而在传统的 EditText 里只能靠自定义 View 来实现这些功能。其实 Compose 官方也其实提供了更简单的提示文字的写法,直接填写一个字符串类型的 label 参数就行:

TextField(
    value = text,
    onValueChange = { text = it },
    label = { Text("Label") }
)
Markdown

只是这种写法要用 Material Design 的样式,而我想完全自己定制样式,所以就没用它。

搜索栏再往下,就是核心区域了,也就是被围观的人的信息。——刚才我说过,这是个叫做《围观》的 App 还记得吗?虽然是虚构的。

围观区域最上面是被围观的人名列表,这个很简单,横向布局里包着一些文字就完了,其中被选中的文字底部有一个圆头的横条:

TopBar()
SearchBar()
NamesBar()

...

@Composable
fun NamesBar() {
    val names = listOf("扔物线", "朱凯", "老冯", "郝哥", "张三", "孙悟空")
    var selected by remember { mutableStateOf(0) }
    LazyRow(Modifier.padding(0.dp, 8.dp), contentPadding = PaddingValues(12.dp, 0.dp)) {
        itemsIndexed(names) { index, name ->
            Column(Modifier.padding(16.dp, 4.dp).width(IntrinsicSize.Max)) {
                Text(name, color = if (index == selected) Color(0xfffa9e51) else Color(0xffb4b4b4))
                if (index == selected) {
                    Box(
                        Modifier.fillMaxWidth().height(2.dp).clip(RoundedCornerShape(1.dp))
                            .background(Color(0xfffa9e51))
                    )
                }
            }
        }
    }
}
Markdown

关于这里唯一需要说的是,由于列表的内容是可以滑动的,所以需要用 LazyRow() 而不是 Row()。这个 LazyRow() 的功能对标的是传统写法的 RecyclerView,不过写起来简单一些,不用 Adapter,也不用 ViewHolder,直接循环遍历每一项然后直接写每个项目的布局就完了:

虽然看起来是遍历的写法,但 Compose 依然会和 RecyclerView一样动态加载,用到哪一项才布局和绘制哪一项,不会像代码看起来那样老老实实地把每一项都按顺序全部计算,所以虽然是遍历的写法,但性能上不会爆炸。

名字列表再往下,是一个叫做「TA 爱的」的区域,也就是这个被围观的人他都喜欢什么。那么首先,来个标题栏:

TopBar()
SearchBar()
NamesBar()
LovesArea()

...

@Composable
fun LovesArea() {
  Row(
    Modifier
      .padding(24.dp, 12.dp, 24.dp)
      .fillMaxWidth()
  ) {
    Text("TA 爱的", fontSize = 16.sp, fontWeight = FontWeight.Bold)
    Spacer(Modifier.weight(1f))
    Text("查看全部", fontSize = 15.sp, color = Color(0xffb4b4b4))
  }
}
Markdown

好,标题栏的下方是横向排布的那些「爱」的列表:

@Composable
fun LovesArea() {
  Column {
    Row(
      Modifier
        .padding(24.dp, 12.dp, 24.dp)
        .fillMaxWidth()
    ) {
      Text("TA 爱的", fontSize = 16.sp, fontWeight = FontWeight.Bold)
      Spacer(Modifier.weight(1f))
      Text("查看全部", fontSize = 15.sp, color = Color(0xffb4b4b4))
    }
    // 就这里
  }
}
Markdown

由于是个列表,所以我用 LazyRow() 而不是 Row()

LazyRow {
}
Markdown

里面内容的填充还是同样的套路,做完之后就是这个样子:

然后我用 LazyRow()horizontalArrangement 参数来把元素互相隔开:

LazyRow(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
    ...
}
Markdown

再用 contentPadding 来给两头也加上点空白:

LazyRow(horizontalArrangement = Arrangement.spacedBy(24.dp),
       contentPadding = PaddingValues(24.dp, 0.dp)) {
    ...
}
Markdown

这个 contentPadding,它和 padding() 的区别是,在滑动的时候内容不会在两头被切掉,而是会触达到滑动区域的边缘:

那么这个「TA 爱的」部分就也做完了。看着还不错,是吧?

剩下的也都是这个写法,比如再往下的「TA 去过」板块:

以及这里的项目被点开后的新页面:

所以布局代码我就不反复说了。重点说一下这个打开和关闭的动画:

动画是分多种的,这种实质上属于界面跳转的过渡动画。要做这种动画,最传统的方式是用 Android 提供的 Transition API。不过 Transition API 的的效果会差一些,如果不涉及 Activity 或者 Fragment 的跳转,过渡动画也可以选择用 Jetpack 的 MotionLayout 来实现。MotionLayout 在 Compose 里也是可以用的,不过我还没试过 Compose 里的 MotionLayout 的功能全不全,我在这里是直接用 Compose 的动画 API 来实现的这个过渡效果。原理很简单:给这个详情页设置四种状态:未打开、已打开、打开中以及关闭中,通过状态来渐变式操作详情页中的各个属性的值——比如每个组件的尺寸和偏移——从而达到详情页的显示和关闭动作的动画形式的呈现。

效果还不错哈?实际上如果你不会 Compose,或者你们公司的项目里没在用 Compose,而是用的传统的 View 系统,这种效果也是可以实现的,你有两个选择:自定义 View,或者 MotionLayout。其中自定义 View 会麻烦一点,MotionLayout 就简单很多,性能上也不一定会比 Compose 更差——甚至对于现在来说可能会好一点,这个我不确定,因为虽然 Compose 的理论性能是比 View 系统更好的,包括 MotionLayout,但它不是比较新嘛,所以我不确定它每个部分的细节优化跟没跟上。

另外据我所知,Compose 在动画方面也正在做更多的工作,其中就包括让这种过渡动画的开发可以更加简便。虽然像我这样手写整个动画过程也不是很麻烦,但是如果有了官方的更省事的 API,那肯定还是更好的。谁还不是个小懒蛋呢?

这部分动画的详细代码大家可以去看我的源码,加我的助教,让她发你就行,而且我的 Compose 课程正式版中也会紧跟官方的脚步,扩充动画相关的内容。

今天就是这些了,喜欢的话请大家点个赞,你们的支持对我很重要。关于 Compose,以及刚才我提到的 MotionLayout,还有更多的各种 Android 开发的内容,我以后也会做更多分享。
关注我,不错过我的任何新内容。我是扔物线,我不和你比高低,我只助你成长。

本文首发于:https://rengwuxian.com/jetpack-compose-weiguan-app/

微信公众号:扔物线

转载时请保留此声明


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK