Jetpack Compose实战教程(五)

Jetpack Compose实战教程(五)

第五章 如何在Compose UI中使用基于命令式UI的自定义View



一、前言

刚从命令式UI转向compose ui的小伙伴往往会有一个疑问,如果我的项目代码中用到了一些第三方的sdk,它们里面有一些自定义控件,我总不能去改别人的源码,用compose ui 重写一遍吧,成本多大啊。或者我自己项目本身就写了一些自定义控件,功能很多的,全部要用compose重写一遍,成本也很高。不用慌,compose提供了一个支持调用命令式UI的自定义View的组件。

二、本章目标

能完整的把目前项目中暂时无法用compose重写的控件熟练的运用至compose中

友情提醒,如果各位看官有不懂的代码可以先看一下之前的章节,循序渐进,如果还是有不懂的,可以给我留言

三、开始编码

3.1 先让自定义控件能跑起来

这里我以引用的第三方sdk的自定义控件(腾讯自研的pag动画)举例

BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                //AndroidView这个就是支持我们调用命令式UI的组件了,在factory中声明这个控件是什么
                    AndroidView(factory = {
                        PAGView(it).apply {
                            this.composition = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }
                    )
                }

            }
        }

运行之后,程序正常运行跑了起来,但它是铺满屏幕的,我们实际使用时,可能需要指定它的位置和大小,这里提供两个方案:

BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                    AndroidView(factory = {
                        PAGView(it).apply {
                            //方案一,使用我们熟知的kotlin代码来指定大小
                            val params = ConstraintLayout.LayoutParams(200f.dp2px().toInt(),200f.dp2px().toInt())
                            this.layoutParams = params
                            this.composition = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier //方案二,使用compose 的modifier来指定大小
                        .width(200.dp)
                        .height(200.dp)
                    )
                }

            }
        }

两个方案都可以将这个PAGView的大小和宽高指定为200dp,但建议使用方案二,因为如果这个AndroidView是需要依赖其它compose控件的位置而发生改变的话,那么方案一就无效了。

3.2给自定义控件使用compose的方式赋值

上述代码我们是写死了动画的执行资源以及执行次数的,那么想要动态改变它,要怎么处理呢?
在实现这个逻辑之前,我需要先解释一个东西,上述代码只贴了compose ui的具体代码,它的完整代码是这样的:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(content = {viewRoot()})
       
}

@Composable
fun viewRoot() {
        BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                    AndroidView(factory = {
                        PAGView(it).apply {
                            //方案一,使用我们熟知的kotlin代码来指定大小
                            val params = ConstraintLayout.LayoutParams(200f.dp2px().toInt(),200f.dp2px().toInt())
                            this.layoutParams = params
                            this.composition = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier //方案二,使用compose 的modifier来指定大小
                        .width(200.dp)
                        .height(200.dp)
                    )
                }

            }
        }
    }

请留意这个 @Composable注解,它代表了viewRoot 这个函数可以使用compose 的相关代码,这是因为compose有要求,所有compose ui的代码,只能在Composable作用域下执行。这意味着,如果我们是要在kotlin代码中去动态改变值的话,那么我们就不能使用compose ui的相关参数,我们先来看在使用compose ui相关参数的办法:

BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                //这里我们定义了一个用于观察的可变参数
                    var compositionValue = remember {
                        mutableStateOf(PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901")))
                    }
                    var testBoolean = false //定义一个boolean变量
                    AndroidView(factory = {
                        PAGView(it).apply {
                            this.composition = compositionValue.value //然后将它的值赋值给PAGView使用
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier
                        .width(200.dp)
                        .height(200.dp)
                        .clickable {
                            testBoolean = !testBoolean //点击一次就改变一次值,从而改变PAGView播放的动画资源
                            if(testBoolean){
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002201"))
                            }else{
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            }
                        }
                    )
                }

            }
        }

代码写完了,然后运行一下,发现点击之后并没有任何变化,这里就要提及一下AndroidView的这个组件了,我们先点击进去看一下AndroidView里面都干了啥:

@Composable
@UiComposable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
) {
    AndroidView(
        factory = factory,
        modifier = modifier,
        update = update,
        onRelease = NoOpUpdate
    )
}

如果再点击里面的AndroidView则能看到它的具体实现,但我们目前所引用的这个,就能解决我们的问题,先不做过深讲解,我们先来看上面的几个函数,factory是我们声明了这个自定义View是啥,我们上述的代码是初始化了这个PAGView,并给它赋了值,但如果要它的参数动态改变的话,我们要使用update函数,将代码改成如下:

BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                    var compositionValue = remember {
                        mutableStateOf(PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901")))
                    }
                    var testBoolean = false
                    AndroidView(factory = {
                        PAGView(it).apply {
                            this.composition = compositionValue.value
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier
                        .width(200.dp)
                        .height(200.dp)
                        .clickable {
                            testBoolean = !testBoolean
                            if(testBoolean){
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002201"))
                            }else{
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            }
                        },
                        update = {
                            it.composition = compositionValue.value //将数据的改动设置到update函数中来
                        }
                    )
                }

            }
        }

再次运行,得到了我们想要的效果。细心的网友应该会发现一些问题,为什么上面的compositionValue要用remember括起来,并且里面还用了一个mutableStateOf来初始化参数值,直接像testBoolean一样不行吗?
所谓实践出真知,那么我们来修改一下代码,改成如下:

BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                    var compositionValue = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                    var testBoolean = false
                    AndroidView(factory = {
                        PAGView(it).apply {
                            this.composition = compositionValue
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier
                        .width(200.dp)
                        .height(200.dp)
                        .clickable {
                            testBoolean = !testBoolean
                            if(testBoolean){
                                compositionValue = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002201"))
                            }else{
                                compositionValue = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            }
                        },
                        update = {
                            it.composition = compositionValue
                        }
                    )
                }

            }
        }

运行,发现点击并没有改变PAGView的播放资源 这是因为,compose是使用观察者模式,通过记录每个参数的变化来刷新UI的,而我们自行定义的参数,并不能让compose记住它的值的变化。

3.3如何在非composable作用域下使用被记忆的变量

好,接下来,假设我们有这么一个需求,我们要监听手机电量的变化,当电量低的时候,播放一个电量比较低的PAG动画,而监听电量的变化是通过系统的广播来实现的,这就意味着我们需要把 compositionValue这个变量提取出来,变成整个Activity的局部变量,才能在收到电量广播的变化时,修改它的值,于是我们依葫芦画瓢,写出如下代码:

private var compositionValue = remember {
        mutableStateOf(PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901")))
    }

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(content = {viewRoot()})
       
}

然而,当我们刚把这个代码复制上去的时候,android studio就提示编译错误了
在这里插入图片描述
再次提醒我们,@Composable调用只能发生在@Composable函数的上下文中,所以我们需要做一些简单的改动,提示编译错误的是remember这个函数,我们将它干掉即可

private var compositionValue = mutableStateOf(PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901")))

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(content = {viewRoot()})
       
}

@Composable
fun viewRoot() {
        BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                    var testBoolean = false
                    AndroidView(factory = {
                        PAGView(it).apply {
                            this.composition = compositionValue.value
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier
                        .width(200.dp)
                        .height(200.dp)
                        .clickable {
                            testBoolean = !testBoolean
                            if(testBoolean){
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002201"))
                            }else{
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            }
                        },
                        update = {
                            it.composition = compositionValue.value
                        }
                    )
                }

            }
        }
    }

这样编译是没有报错了,但当我们运行时,Logcat抛给了我们另外一个错误:
在这里插入图片描述
很明显,我们在初始化compositionValue的时候,不能在参数那里直接调用PAGFile.Load(assets, Constant.PagConstant.getSourceByName(“002901”)),此时界面还没绘制,读取资源文件会报空指针,然后我们尝试修改一下:

private var compositionValue = mutableStateOf(PAGFile)
@Composable
    override fun viewRoot() {
        compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
        BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                    var testBoolean = false
                    AndroidView(factory = {
                        PAGView(it).apply {
                            this.composition = compositionValue.value
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier
                        .width(200.dp)
                        .height(200.dp)
                        .clickable {
                            testBoolean = !testBoolean
                            if(testBoolean){
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002201"))
                            }else{
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            }
                        },
                        update = {
                            it.composition = compositionValue.value
                        }
                    )
                }

            }
        }
    }

此时android studio又报编译错误了
在这里插入图片描述
原来这个PAGFile的构造函数是私有的,于是我们只能改成这样:

private var pagFile:PAGFile?=null //我们定义一个为空的PAGFile,然后赋值给compositionValue
    private var compositionValue = mutableStateOf(pagFile)


    @Composable
    override fun viewRoot() {
    //然后尽可能早的给compositionValue赋值一个真实的数据
        compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
        BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                    var testBoolean = false
                    AndroidView(factory = {
                        PAGView(it).apply {
                            this.composition = compositionValue.value
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier
                        .width(200.dp)
                        .height(200.dp)
                        .clickable {
                            testBoolean = !testBoolean
                            if(testBoolean){
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002201"))
                            }else{
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            }
                        },
                        update = {
                            it.composition = compositionValue.value
                        }
                    )
                }

            }
        }
    }

运行,程序按我们的要求跑起来了。那么举一反三,设置播放次数 以及何时播放,都可以用外部参数来控制,这里建议大家亲自尝试一下

3.4 及时释放资源

我们再回到刚才AndroidView的实现这里

@Composable
@UiComposable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
) {
    AndroidView(
        factory = factory,
        modifier = modifier,
        update = update,
        onRelease = NoOpUpdate
    )
}

它还有一个函数 onRelease,当我们需要在生命周期结尾的时候释放资源,就需要用到它了,那么我们来编写一下代码

private var pagFile:PAGFile?=null
    private var compositionValue = mutableStateOf(pagFile)


    @Composable
    override fun viewRoot() {
        compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
        BaseTheme {
            Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                ConstraintLayout {
                    var testBoolean = false
                    AndroidView(factory = {
                        PAGView(it).apply {
                            Log.e("test","初始化资源")
                            this.composition = compositionValue.value
                            this.setRepeatCount(0)
                            this.play()
                        }
                    }, modifier = Modifier
                        .width(200.dp)
                        .height(200.dp)
                        .clickable {
                            testBoolean = !testBoolean
                            if(testBoolean){
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002201"))
                            }else{
                                compositionValue.value = PAGFile.Load(assets, Constant.PagConstant.getSourceByName("002901"))
                            }
                        },
                        update = {
                            Log.e("test","加载资源")
                            it.composition = compositionValue.value
                        },
                        onRelease = {
                            Log.e("test","释放资源")
                            it.freeCache()
                        }
                    )
                }

            }
        }
    }

在这里插入图片描述
本章内容至此结束,各位看官一定要亲自编写代码,才能更加熟练的使用compose ui,祝各位都能更上一层楼

  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值