前言
在第一节我介绍Compose的时候,提到过Compose底层的实现依然是调用Android原生API,例如显示文字,底层调用了drawText
,为什么这么做是因为Compose不可避免要和原生组件打交道,如果跳过原生直接和skia渲染器打交道,那么已经有Flutter作为前辈在那里了,所以Compose它依然是一个原生UI框架体系。
1 Compose与原生View的调用
首先,我们需要知道,在什么时候会出现Compose需要和原生的View交互。一般来说,我们在新的模块或者新的需求来的时候,能够用Compose那么就可以直接用Compose,但是:
- 在Compose界面中,需要使用现有的组件,但是组件为原生View实现的,这里就不建议直接将原生View组件重写,当然后续肯定要实现从原生View到Compose的迁移,但是现阶段稳定性为主,需要在Compose中嵌入原生的View。
- 像SurfaceView和TextureView,在Compose当中没有对应的平替,它是属于Surface体系中的,渲染绘制都是在单独的BufferQueue运转机制中的,所以这种情况下就只能使用原生View。
1.1 Compose融入传统View
如果要在Compose当中加入Android的原生View,那么可以使用AndroidView
这个类,从字面意思上看,就是告诉Compose这个是Android的原生View组件。
@Composable
@UiComposable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
)
其中几个参数介绍一下:
- factory:用于构建原生的View组件,例如
TextView
、ImageView
、SurfaceView
等; - modifier:设置样式;
- update:如果想要原生View也像Compose一样具备自动刷新的能力,那么需要在这里加刷新的逻辑。 这个很重要。
setContent {
val context = LocalContext.current
var name by remember {
mutableStateOf("初始值")
}
Column {
Text(text = name)
AndroidView(factory = {
Button(context).apply {
text = "点击刷新"
setOnClickListener {
name = "Hello World~~"
}
}
})
AndroidView(factory = {
TextView(context).apply {
text = name
}
}){
//刷新逻辑
it.text = name
}
}
}
在Column中,Button
和TextView
都是原生的组件,他们融入到了Compose UI体系当中。
1.2 传统View融入Compose
在Compose当中,有一个ComposeView
,这个View的实现是通过继承ViewGroup
完成的,相当于在Compose中的传统View。
class ComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
// ......
}
AbstractComposeView
的源码,我就不带大家看了,其实就是继承自ViewGroup,整体的依赖关系:
graph RL
ViewGroup --> AbstractComposeView --> ComposeView
在 ComposeView
中有一个setContent
函数,这个函数可以放置任意Compose UI。
/**
* Set the Jetpack Compose UI content for this view.
* Initial composition will occur when the view becomes attached to a window or when
* [createComposition] is called, whichever comes first.
*/
fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}
在原生View界面中,设置一个布局容器,直接采用addView
的方式将ComposeView
添加进去即可。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val frameLayout = FrameLayout(this)
addContentView(
frameLayout,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
)
frameLayout.addView(
ComposeView(this).apply {
setContent {
TestMultiScroll()
}
}
)
}
当然这是动态加载的方案,其实从ComposeView
的构造函数中,可以看到它也支持在xml布局文件中直接使用。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/composeview"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
例如在原生界面中的某一块会使用到Compose UI,那么就可以拿到ComposeView
,设置组合函数。
private lateinit var binding:LayoutComposeViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutComposeViewBinding.inflate(layoutInflater)
setContentView(binding.root)
// 布局中的某一块使用Compose UI
binding.composeview.apply {
setContent {
TestMultiScroll()
}
}
}
其实从传统View迁移到Compose是一个非常大体量的变化,如果当前项目非常大,不建议直接从头到尾全部用Compose重写一遍,新的需求可以用Compose,老的模块可以进入迁移Compose的日程上。