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

前言

在第一节我介绍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组件。

kotlin 复制代码
@Composable
@UiComposable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
) 

其中几个参数介绍一下:

  • factory:用于构建原生的View组件,例如TextViewImageViewSurfaceView等;
  • modifier:设置样式;
  • update:如果想要原生View也像Compose一样具备自动刷新的能力,那么需要在这里加刷新的逻辑。 这个很重要。
kotlin 复制代码
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体系当中。

1.2 传统View融入Compose

在Compose当中,有一个ComposeView,这个View的实现是通过继承ViewGroup完成的,相当于在Compose中的传统View。

kotlin 复制代码
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。

kotlin 复制代码
/**
 * 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添加进去即可。

kotlin 复制代码
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 复制代码
<?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,设置组合函数。

kotlin 复制代码
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的日程上。

相关推荐
androidwork1 小时前
深入解析内存抖动:定位与修复实战(Kotlin版)
android·kotlin
梦天20151 小时前
android核心技术摘要
android
海的诗篇_2 小时前
前端开发面试题总结-原生小程序部分
前端·javascript·面试·小程序·vue·html
szhangbiao3 小时前
“开发板”类APP如果做屏幕适配
android
胡清波3 小时前
# vue 的 Diff 算法
前端·面试
Jackson_Mseven3 小时前
面试官:useEffect 为什么总背刺?我:闭包、ref 和依赖数组的三角恋
前端·react.js·面试
高林雨露4 小时前
RecyclerView中跳转到最后一条item并确保它在可视区域内显示
android
绝无仅有4 小时前
对接三方SDK开发过程中的问题排查与解决
后端·面试·架构
前端小巷子6 小时前
跨域问题解决方案:开发代理
前端·javascript·面试
天涯学馆6 小时前
JavaScript 跨域、事件循环、性能优化面试题解析教程
前端·javascript·面试