Jetpack Compose 重磅更新!新组件上手指南!

4,986 阅读11分钟

Jetpack Compose 是Google发布的一个Android原生现代UI工具包,它完全采用Kotlin编写,可以使用Kotlin语言的全部特性,可以帮助你轻松、快速的构建高质量的Android应用程序。如果你还不了解Jetpack Compose是什么?建议你读一下我前面的2篇文章:

Android Jetpack Compose 最全上手指南

Jetpack Compose,不止是一个UI框架!

去年的Google IO 大会上,Google宣布了Jetpack Compose的面世,但是在去年11月份,它才发布第一个预览版-Developer Preview1,此后,基本保持每两周发布一个小版本,到现在,半年的时间过去了,中间发布了十多个小版本,今天,终于迎来了重大更新,Developer Preview2 发布了。

Jetpack Compose Developer Preview1发布后,开发者最关心的几个问题是,没有Compose版本的RecyclerView、Constriantlayout、动画等一系列问题。这些问题在Preview2都解决了。

当然,从Preview1 到现在发布的Preview2,变化非常大,甚至很多API都已经变了,有的属性或者类的增加或者删除。具体的变换化太多,就不在这里一一讲解,感兴趣的可以看看官方的每个小版本的更新日志。今天就带大家一起看看PreView2增加的一些重磅功能。

  • 1、Modifier
  • 2、RecyclerView
  • 3、Constriantlayout
  • 4、动画
  • 5、原生View引入Compose

好戏开场了!

1、Modifier

首先,说一下Modifier(修改器),在Preview1版本,就已经有了modifier,不过使用的地方不多,并且对于它的定位比较模糊,令人困惑,因为modifier能干的事儿,通过组合函数也能做到。但是我们发现了一件事,例如,要在Compose函数中增加padding的时候,会产生大量的嵌套,因为要给嵌套一个容器才能设置padding,因此,现在将很多功能都移动到了Modifier,它现在使用非常广泛,可以修饰一个元素、一个布局或者一些其他行为。如何使用Modifier?先来看一个例子:

首先,我们写一个Compose函数(即Compose组件),展示一张图片

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop )
}

图片显示的是原来的尺寸,然后给图片指定一个大小,比如:256dp,此时就需要使用Modifier了。

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp))
}

修改后如下,宽高都为256dp。

modifier中有很多可以配的参数,比如,增加一个padding,将图片裁剪成一个圆形

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp)
         .padding(16.dp)
         .drawShadow(8.dp,shape = shape)
        )
}

效果就成了这样:

还可以再圆形头像加一个border,代码如下:

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp)
         .padding(16.dp)
         .drawShadow(8.dp,shape = shape)
         .drawBorder(6.dp,MaterialTheme.colors.primary,shape = shape)
        )
}

效果如下:

还可以同时添加多个border,比如我再增加2个:

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp)
         .padding(16.dp)
         .drawShadow(8.dp,shape = shape)
         .drawBorder(6.dp,MaterialTheme.colors.primary,shape = shape)
         .drawBorder(12.dp,MaterialTheme.colors.secondary,shape = shape)
         .drawBorder(18.dp,MaterialTheme.colors.background,shape = shape)
        )
}

效果就成这样了:

设置点击事件也是再modifier中,比如我们要在点击这个图片后,改变形状,以前的View可麻烦了,但是Jetpack compose 却非常简单,modifier中增加如下代码:

@Composable
fun Greeting() {
    val (shape,setShape) = state<Shape> { CircleShape }
    Image(asset = imageResource(R.drawable.androidstudio),
        contentScale = ContentScale.Crop,
     modifier = Modifier.size(256.dp)
         .padding(16.dp)
         .drawShadow(8.dp,shape = shape)
         .drawBorder(6.dp,MaterialTheme.colors.primary,shape = shape)
         .drawBorder(12.dp,MaterialTheme.colors.secondary,shape = shape)
         .drawBorder(18.dp,MaterialTheme.colors.background,shape = shape)
         .clickable { // 点击事件
             setShape(
                 if(shape == CircleShape)
                     CutCornerShape(topLeft = 32.dp,bottomRight = 32.dp)
                else
                     CircleShape
             )
         }
        )
}

上面的代码中,我们还增加了判断,如果当前shape是CircleShape,我们就改变形状,否则就设置为CircleShape,效果就是点击图片,形状在这两种效果之间来回切换。

效果如下:

2. Jetpack Compose 中的RecyclerView

RecyclerView是我们Android开发中用来展示大数据列表的常用组件,它能帮助我们回收复用视图,有很好的性能体验。在Jetpack Developer PreView1 刚出来的时候,我就在官网或者代码库中找这个组件。很遗憾翻遍了所有资料都每找到,是确实没有,最终只找到了一个叫做VerticalScroller的组件你。它可以用来展示列表,但是它不是RecyclerView,它类似我们的ScrollView,也就是说,展示少量数据的列表是可以的,因为它没有复用机制,展示大列表时,内存堪忧,会OOM。

但是在这次的Preview2中,RecyclerView终于被盼来了,组件名字叫做:AdapterList,它就对应我们原生Android开发的RecyclerView。以前我们要写一个列表是非常复杂的,用写xml,Adapter,ViewHolder等,最终还要在Activity/Fragment初始化和绑定数据,非常麻烦。Jetpack Compose中的列表使用则是非常简单,简单到令人发指。来看一下我们如何展示一个列表:

@Composable
fun generateList(context: Context) {
    val list = mutableListOf<String>()
    //准备数据
    for (i in 1..100) {
        list.add(i.toString())
    }
    AdapterList(data = list) {
        Card(
            shape = MaterialTheme.shapes.medium,
            modifier = Modifier
                .preferredSize(context.resources.displayMetrics.widthPixels.dp, 60.dp)
                .padding(10.dp)
        ) {

            Box(gravity = ContentGravity.Center) {
                ProvideEmphasis(EmphasisAmbient.current.medium) {
                    Text(
                        text = it,
                        style = MaterialTheme.typography.body2
                    )
                }
            }

        }
        Spacer(modifier = Modifier.preferredHeight(10.dp))
    }
}

看到了没,就是这样几行代码,我们的列表就完成了,解释一下代码:最开始的准备数据没啥说的,向list中添加了100个数据,然后将数据源传给AdapterList,列表的每一个Item是一个卡片,用的是Card组件,卡片里展示了一个Text文本,最后的Spacer用来设置item之间的间距,相当于ItemDecoration,看一下效果:

3. Constriantlayout

Constriantlayout是一个功能非常强大的布局,也是现在Android开发中最受欢迎的布局之一,当Jetpack Compose Preview1版本才出来的时候,很多开发者都有一个疑问,Compose 中该如何使用Constriantlayout呢?它将如何运作,这确实是个有意思的问题。因为在Jetpack Compose中,所有的组件都是组合函数,获取不到View饮用,如何约束彼此之间的关系确实是一个难题。好在现在这个难题解决了,下面通过几个小例子一起来看看Compose中的Constriantlayout使用。

如下图所示,有两个View,A和B,ViewB在ViewA的右边,顶部和ViewA的底部对齐,如何使用Constriantlayout 描述它们的位置关系?

代码:

@Composable
fun GreetConstraintLayout(context: Context) {
    ConstraintLayout(constraintSet = ConstraintSet {
        val viewA = tag("ViewTagA").apply {
            left constrainTo parent.left
            centerVertically()
        }
       val viewB =  tag("ViewTagB").apply {
            left constrainTo viewA.right
            centerVertically()
            top constrainTo viewA.bottom
        }
    }, modifier = Modifier.preferredSize(context.screenWidth().dp,400.dp).drawBackground(Color.LightGray)) {

        Box(
            modifier = Modifier.tag("ViewTagA").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Blue,
            gravity = ContentGravity.Center
        ) {
            Text(text = "A")
        }
        Box(
            modifier = Modifier.tag("ViewTagB").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Green,
            gravity = ContentGravity.Center
        ) {
            Text(text = "B")
        }
    }
}

解释一下上面的代码:在ConstraintSet中来定义约束,使用Tag来创建一个约束,后面我们就可以通过这个tag来使用我们定义的约束,返回的是一个ConstrainedLayoutReference,ViewA的左边与父组件的左边对齐,垂直居中。ViewB的左边与ViewA的右边对齐,top与ViewA的底部对齐。也垂直居中。

比如ViewB中就是使用ViewA来作为约束条件了。

后面使用的时候,直接用Modifier.tag()应用约束到组件上。

这还不是最牛逼,还有一个强大的功能是可以在布局约束中添加逻辑,比如:我有一个ViewC 它的位置可能有两种情况:

  • 1、ViewC 的左边与ViewA的右边对齐
  • 2、View C的左边与ViewB的右边对齐

该怎么写代码?先定一个一个Boolean 变量叫hasFlag(随便其的名,它的值根据你的业务逻辑某些情况是true,某些情况是false)

 val hasFlag = true // 它的值根据你的业务逻辑某些情况是true,某些情况是false
 
 tag("ViewC").apply {
            // 根据判断条件改变,约束也改变
            left constrainTo (if(hasFlag) viewA else viewB).right
            bottom constrainTo viewB.top
        }

完整代码如下:

@Composable
fun GreetConstraintLayout(context: Context) {
    ConstraintLayout(constraintSet = ConstraintSet {
        val hasFlag = true // 它的值根据你的业务逻辑某些情况是true,某些情况是false
        val viewA = tag("ViewTagA").apply {
            left constrainTo parent.left
            centerVertically()
        }
       val viewB =  tag("ViewTagB").apply {
            left constrainTo viewA.right
            centerVertically()
            top constrainTo viewA.bottom
        }
        tag("ViewC").apply {
            // 根据判断条件改变,约束也改变
            left constrainTo (if(hasFlag) viewA else viewB).right
            bottom constrainTo viewB.top
        }
    }, modifier = Modifier.preferredSize(context.screenWidth().dp,400.dp).drawBackground(Color.LightGray)) {

        Box(
            modifier = Modifier.tag("ViewTagA").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Blue,
            gravity = ContentGravity.Center
        ) {
            Text(text = "A")
        }
        Box(
            modifier = Modifier.tag("ViewTagB").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Green,
            gravity = ContentGravity.Center
        ) {
            Text(text = "B")
        }
        Box(
            modifier = Modifier.tag("ViewC").preferredSize(100.dp, 100.dp),
            backgroundColor = Color.Red,
            gravity = ContentGravity.Center
        ) {
            Text(text = "C")
        }
    }
}

hasFlag=true 效果如下:

hasFlag=false 效果如下:

其他的一些约束布局属性同现在我们使用的ConstraintLayout相同,有兴趣的可以去试试。

4. 动画

Jetpack Compose对动画的支持也是开发者非常关心的一个问题,这一小节就看看Compose中,动画的使用,还是来看一个小例子,先看效果图:

如上,一个简单的属性动画,图片有选中/未选中两种状态,由未选中->选中时,有一个正方形->圆形的动画,并且伴随着alpha动画。

代码如下:

@Composable
fun GreetAnimate(){
    //
    val (selected,onValueChange) = state { false }
    // radius 变化
    val radius = animate(if(selected) 100.dp else 8.dp)
    // alpha 动画
    val selectAlpha = animate(if(selected) 0.4f else 1.0f)

   Surface(shape = RoundedCornerShape(
       topLeft = radius,
       topRight = radius,
       bottomRight = radius,
       bottomLeft = radius
   )) {
       Toggleable(
           value = selected,
           onValueChange = onValueChange,
           modifier = Modifier.ripple()
       ) {

           Image(
               asset = imageResource(R.drawable.androidstudio),
               modifier = Modifier.preferredSize(200.dp,200.dp),
               contentScale = ContentScale.Crop,
               alpha = selectAlpha
           )
       }
   }
}

动画使用animate Compose函数来完成,只需要为它提供不同的target的值,它就能帮你完成之间的变化,一旦动画创建,它就和普通的Compose函数是一样的。

注意一点animate创建的动画是不能被取消的,要创建可以被取消的动画可以使用animatedValue。还有其他两个相似动画函数:animatedFloat,animatedColor

啥?你说看起来有点熟悉?那可不是嘛,ObjectAnimator,ValueAnimator, 你细品,更多关于动画的使用方式这里不展开了,有兴趣的同学下来自己动手试试。

4. 与原生View 的兼容

一门新的语言,一个新的框架,考虑兼容是很有必要的,就像Kotlin那样,我们使用Kotlin不必一下子重写整个项目,你可以添加一个新的类,一个新的模块中使用Kotlin,因为它们与Java 完全相互调用。

Jetpack Compose 借鉴了经验,我们要使用Jetpack Compose,也可以慢慢来,以前的代码不用动,在你的新模块中一点一点的添加,这就涉及到与原来的View的兼容,在Compose中,可以使用AndroidView来兼容以前的Views。

比如我的Jetpack Compose 中要使用到Webview,而它本身也没有提供,该如何是好?别担心,用原来的就行。

首先,创建一个xml文件webview.xml,里面添加Webview 布局:

<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
</WebView>

然后写一个compose 函数,使用AndroidView 来加载:

@Composable
fun androidView(){
    AndroidView(R.layout.webview){ view ->
        val webView = view as WebView
        webView.settings.javaScriptEnabled =true
        webView.settings.allowFileAccess = true
        webView.settings.allowContentAccess = true
        webView.loadUrl("https://juejin.cn")
    }
}

加载了一个原生的Webview,然后在webview中加载了掘金的网址,效果如下:

看一下AndroidView函数签名:

@Composable
// TODO(popam): support modifiers here
fun AndroidView(@LayoutRes resId: Int, postInflationCallback: (View) -> Unit = { _ -> }) {
    AndroidViewHolder(
        postInflationCallback = postInflationCallback,
        resId = resId
    )
}

接受一个布局文件资源id,和一个回调postInflationCallback,当View被inflate出来后,会调用这个回调,然后你就可以在回调中使用它了。

但是,注意: 回调通常是在主线程被调用。

5.总结

总的来说,这次Developer PreView2 更新比较多,并且很多API发生了变化,增加了一些关键的组件如AdapterList,ConstraintLayout动画组件等,使用方式也与Preview1有很多不同。可以来看一下Google关于Jetpack Compose 上的时间表:

  • 2019.5 宣布Jetpack Compose
  • 2019.11 第一个 Developer Preview
  • 2020.6 第二个 Developer Preview
  • 2020 夏天将发布Alpha版本
  • 2021 将发布release 1.0版本

但是,要说的是,现在很多API还不是最终版本,可以看到,每一个打版本的变化还是蛮大的,现在仍然不能用在商用项目上。但是就jetpack Compose 本身来说,个人还是比较期待的,从上面的时间表就可以看到,大概明年就能出第一个release版本,敬请期待吧!

对了,最新版本的Jetpack Compose 需要Android Studio 4.2以上版本才能使用,想要体验的同学先安卓Android Studio 4.2 Canary ​版本。​去官网下载!

小版本日志列表请看:developer.android.com/jetpack/and…

youtobe视频介绍请看:www.youtube.com/watch?v=U5B…

文章首发于公众号:「 技术最TOP 」,每天都有干货文章持续更新,可以微信搜索「 技术最TOP 」第一时间阅读,回复【思维导图】【面试】【简历】有我准备一些Android进阶路线、面试指导和简历模板送给你