Jetpack Compose 实用指南 - .9图与launchEffect与添加编辑复用原生AndroidView

Jetpack Compose 实用指南 - .9图与launchEffect与添加编辑复用原生AndroidView,第1张

Jetpack Compose 实用指南 - .9图与launchEffect与添加/编辑/复用原生AndroidView

开头感谢我的粉丝头子 (这位同学拒绝提供博客地址)提供的水群话题
不多说废话,直接进入正题

Compose中显示.9图

现在有.9图如下——

总所周知,compose中加载图片用Image()
image 的第一个参数:painter,
大家一般用自带的,或者官方推荐的coil库

当前文章使用的两者API版本分别为compose 1.2.0-alpha02和coil-compose:1.4.0

于是我们加载图片试试

前者使用painterResource(),会报错——

java.lang.IllegalArgumentException: only VectorDrawables and rasterized asset types are supported ex. PNG, JPG

简直是滑天下之大稽!我这个图是不是png我自己心里没点数?

后者使用rememberImagePainter()倒是能够加载图片出来,来,我们一步步试试看。

首先,很自然的一个想法:coil库支持直接填入类型为int的ResIdDrawable,我们直接——

Image(
		rememberImagePainter(R.drawable.xxx), //
						"testFor.9",
                     	Modifier.size(300.dp, 50.dp),
                       	contentScale = ContentScale.FillBounds //拉伸图片以填充
               )

得到图片如下——

显然是玩崩了,这个.9图片失去了它的特性,变成了普通的PNG图片。

然后我们想到,在传统ImageView直接使用Drawable其实也会被警告并推荐使用ContextCompat.getDrawable(context , @ResId resId)方法。
那我们这里试试看——

Image(
		rememberImagePainter(
   								ContextCompat.getDrawable(context R.drawable.xxx)
   						), 
						"testFor.9",
                     	Modifier.size(300.dp, 50.dp),//设置一个与图片明显不符的宽高测试
                       	contentScale = ContentScale.FillBounds //拉伸图片以填充
               )

得到图片如下——

wow~ awesome!

但显然大部分人会蹉跎这么一个小小的 *** 作,根本想不到这里去,并认为compose不支持.9图,起码目前不支持

这时候大家就会想到——我添加一个AndroidView,里面展示.9图,岂不美哉!

添加AndroidView

语法很简单,直接看代码——

AndroidView({ it:Context -> //传入了一个context供你初始化该view
//AndroidView中第一个参数默认返回一个传统View
//在这里进行view的初始化,它只会被调用一次,且保证在UI线程上被调用
                ImageView(it).apply {
                     this.setImageDrawable(ContextCompat.getDrawable(it, R.drawable.left_bg))
                     this.scaleType = ImageView.ScaleType.FIT_XY
                 }
            }, 
            Modifier.size(300.dp, 50.dp)){ it:ImageView ->
            //这是一个可选参数,它是最后一个参数所以可以用kotlin的语法糖挪出来   
            //此lambda在view每次recompose过程中被调用,也运行在UI线程,方便你根据数据进行一些view状态的更新        
            }

好,我会了,然后呢
然后群友提出一个有意思的问题:这个传统view啊,我想给其他模块使用
听起来很简单,好说好说——

//这个参数用mutableState和放在这里只是为了方便演示
//实际上应该它的位置应该在ViewModel或者其他地方
//也需要按需使用LiveData或者其他数据结构
var dotNineImage: ImageView? by remember { mutableStateOf(null) }
    
AndroidView(
        {
            ImageView(it).apply {
            this.setImageDrawable(ContextCompat.getDrawable(it, R.drawable.left_bg))
                this.scaleType = ImageView.ScaleType.FIT_XY
                dotNineImage = this //初始化时把当前View保存
            }
        },
        Modifier.size(300.dp, 50.dp)
)

按理来说这个dotNineImage对象就能被挪作他用了?
那肯定不行啊!

你要是直接拿此view添加到其他地方,会得到报错如下——
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
查看堆栈,出现在addView()的时候。

简单说就是:孩子只能有一个父亲!!
如果要给孩子换个家庭,那他得先和已有的parent断绝关系!!

这个报错已经很明显了,并且提示了你应该怎么做,但切记在hide此view后再将其移除
否则会在recompose过程由于尝试设置其属性出现类似以下错误的报错(具体报错可能因为各种原因略有不同,但肯定都是同一原因引发的)——
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.view.View.setMinimumWidth(int)' on a null object reference

关键代码如下——

var curImageView: ImageView? by remember { mutableStateOf(null) }
var show by remember { mutableStateOf(true) }

if (show)
    AndroidView({
                    if (curImageView == null)
                        ImageView(it).apply {
                            this.setImageDrawable(ContextCompat.getDrawable(it, R.drawable.left_bg))
                            this.scaleType = ImageView.ScaleType.FIT_XY
                            curImageView = this
                        }
                    else curImageView!!
                }, Modifier
        .size(300.dp, 50.dp))
LaunchedEffect(show) {
    if (!show) {
        (curImageView?.parent as? ViewGroup)?.removeView(curImageView)
    }
}

//来个button简单切换show的值试试
Button({show=!show}){
    Text("显隐AndroidView")
}

如上代码,关键就是这个launchEffect——
它在此处被设定为观察show,
被观察的值一旦改变,
它一定会在compose过程中紧跟着执行它的block中描述的内容。

显而易见,
当show的值为false,当compose过程到launchEffect所在位置时,AndroidView已经隐藏了,
在show的值下一次改变之前,该AndroidView因为已经从compose树中移除,不会再参与后续的compose过程,
所以它此时可以被安全移除

更妙的是:当show值改变,下一次尝试显示该AndroidView时,
只要那时候——它没有变成别人的孩子或已经被断绝亲子关系,
它——还会被自动执行addView() *** 作添加回来。

基于这段逻辑的原理稍作修改,它已经具备了出现在其他地方,又从其他地方再回来的基础。

——提出这个问题的童鞋终于可以“理论上实现将视频转到小窗播放且无需 *** 心进度、加载、加载时的空白等等一系列问题了。

错误典型

然后本来这个文章按理来说该结尾了,但童鞋说不对不对,他说他最终还是另开了一个view解决问题——

显然,之所以多用了一个view,是因为他没get到launchEffect的真谛!

敲黑板

两个共用同一AndroidView的Composable绝对不能用同一个key去控制两者的显隐,否则必然会出现图中童鞋出现的问题!
两个共用同一AndroidView的Composable绝对不能用同一个key去控制两者的显隐,否则必然会出现图中童鞋出现的问题!
两个共用同一AndroidView的Composable绝对不能用同一个key去控制两者的显隐,否则必然会出现图中童鞋出现的问题!

用本文的例子说人话就是:
当我想在另外的地方使用这个ImageView时,我必须用launchEffect,确保这一处的ImageView消失后,再让该ImageView显示在另一处。

上代码自己领悟吧——

var curImageView: ImageView? by remember { mutableStateOf(null) }
var show by remember { mutableStateOf(true) }
var showSecond by remember { mutableStateOf(false) }
Box {
    LazyColumn {
        item {
            if (show)
                AndroidView(
                        {
                            if (curImageView == null)
                                ImageView(it).apply {
                                    this.setImageDrawable(ContextCompat.getDrawable(it, R.drawable.left_bg))
                                    this.scaleType = ImageView.ScaleType.FIT_XY
                                    curImageView = this
                                }
                            else curImageView!!
                        },
                        Modifier.size(300.dp, 50.dp)
                )
            LaunchedEffect(show) {
                if (!show) {
                    showSecond = true
                }
            }
        }
    }
    Box {
        if (showSecond)
            AndroidView({ curImageView!! },
                    Modifier.size(360.dp, 70.dp)
            )
        LaunchedEffect(showSecond) {
            if (!showSecond) {
                show = true
            }
        }
    }
}
Button({
           if (show)
               show = !show
           else
               showSecond = !showSecond
       }) {
    Text("显隐")
}

这里用lazyColumn在写法错误的情况下必出问题

把LazyColumn替换成Column,或者将两个AndroidView放在同一Column中,即便用一个key同时控制两个view,问题也可能不会出现,
但如果你没认识到问题的本质,常在河边走,必定会湿鞋!

问题的本质

本质1:
你必须保证一个view已经在一处被移除后,才被添加到另一处——这是Android要求的。
本质2:
通过写在View后面,且观察view的控制key的LaunchEffect去保证此view已经在compose层次结构中消失,
此时这个view才可以被安全地从整个compose代码中移除,然后添加到其他地方
(或者从compose代码的这一处消失,然后出现在compose代码的另一处)

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5717651.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)

保存