前言

在第一节我介绍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组件,例如TextViewImageViewSurfaceView等;
  • 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中,ButtonTextView都是原生的组件,他们融入到了Compose UI体系当中。

Compose编程思想 -- Compose UI与原生View的互相调用

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的日程上。