Compose 和 Android 传统View 互相调用

Posted 氦客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Compose 和 Android 传统View 互相调用相关的知识,希望对你有一定的参考价值。

1. 前言

Compose 具有超强的兼容性,兼容现有的所有代码,Compose 能够与现有 View 体系并存,可实现渐进式替换。这就很有意义了,我们可以在现有项目中一小块一小块逐步地替换Compose,或者在旧项目中实现新的需求的时候,使用Compose
今天,我们就来演示一下,Composeandroid View怎么互相调用,以及在双层嵌套(原生View嵌套ComposeCompose中又嵌套原生View)的情况下,在最外层原生View中,怎么获取到Compose内部的原生View

2. Android 传统 View 调用 Compose

2.1 新建传统View体系的Android项目

新建项目的时候选择 Empty Activity

2.2 项目添加Compose配置

2.2.1 在android代码块添加

appbuild.config android代码块中添加

buildFeatures 
    compose true

composeOptions 
    kotlinCompilerExtensionVersion '1.1.1'

2.2.2 在dependencies中添加依赖

appbuild.config dependencies代码块中添加

dependencies 
	//...省略...

    def compose_ui_version = '1.1.1'
    implementation "androidx.compose.ui:ui:$compose_ui_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"

    implementation 'androidx.activity:activity-compose:1.3.1' //kotlin对应版本1.6.20
    implementation 'androidx.compose.material:material:1.1.1'

2.3 定义Compose函数

MainActivity.kt中定义Compose函数

@Composable
fun ComposeContent() 
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) 
        Text(text = "Hello world!")
    

2.4 修改xml文件

activity_main.xml中添加androidx.compose.ui.platform.ComposeView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

2.5 关联Compose函数

MainActivity.kt中,先通过findViewById找到ComposeView,然后通过composeView.setContent将Android 传统View和Compose建立关联。

override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val composeView : ComposeView = findViewById(R.id.compose_view)
    composeView.setContent 
        ComposeContent()
    

2.6 运行项目

可以发现界面显示如下,成功在传统View项目中调用了Compose

3. Compose中调用Android View

3.1 调用传统View的日历

3.1.1 使用AndroidView

@Composable内使用: androidx.compose.ui.viewinterop.AndroidView,然后在factory里面返回原生View即可

@Composable
fun AndroidViewPage() 
    AndroidView(factory = 
        CalendarView(it)
    , modifier = Modifier.fillMaxWidth(), update = 
        it.setOnDateChangeListener  view, year, month, day ->
            Toast.makeText(view.context, "$year$month$day日", Toast.LENGTH_SHORT).show()
        
    )

3.1.2 显示效果如下

3.2 调用传统View的WebView

3.2.1 添加网络权限

首先需要在AndroidManifest.xml中添加网络权限

<uses-permission android:name="android.permission.INTERNET" />

3.2.2 首先要注册WebView的生命周期

@Composable
private fun rememberWebViewLifecycleObserver(webView: WebView): LifecycleEventObserver 
    return remember(webView) 
        LifecycleEventObserver  _, event ->
            run 
                when (event) 
                    Lifecycle.Event.ON_RESUME -> webView.onResume()
                    Lifecycle.Event.ON_PAUSE -> webView.onPause()
                    Lifecycle.Event.ON_DESTROY -> webView.destroy()
                    else -> Log.e("WebView", event.name)
                
            
        
    

3.2.3 创建有状态的WebView

创建有状态的WebView,并注册生命周期

@Composable
fun rememberWebViewWIthLifecycle(): WebView 
    val context = LocalContext.current
    val webView = remember 
        WebView(context)
    
    val lifecycleObserver = rememberWebViewLifecycleObserver(webView)
    val lifecycle = LocalLifecycleOwner.current.lifecycle
    DisposableEffect(lifecycle) 
        lifecycle.addObserver(lifecycleObserver)
        onDispose 
            lifecycle.removeObserver(lifecycleObserver)
        
    
    return webView

3.2.4 调用Android View

@Composable
fun WebViewPage() 
    //创建有状态的WebView,并注册生命周期
    val webView = rememberWebViewWIthLifecycle()
    AndroidView(factory = 
        webView
    , modifier = Modifier
        .fillMaxSize() //宽高占满父布局
        .background(Color.Red),
    update = webView ->
        //设置支持javascript
        val webSettings = webView.settings
        webSettings.javaScriptEnabled = true
        webView.loadUrl("https://www.baidu.com")
    )

3.2.5 显示效果如下所示

4. 双层嵌套,获取AndroidView中的原生View id

有时候,我们会遇到这种情况,就是在原生项目了,页面中有部分使用了Compose,然后在Compose中又有部分组件使用了原生View,这种情况下,要如何取到AndroidView中的原生View id 呢 ?

4.1 在定义Xml中定义ComposeView

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

4.2 关联Compose函数

MainActivity.kt中,先通过findViewById找到ComposeView,然后通过composeView.setContent将Android 传统View和Compose建立关联。

override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val composeView : ComposeView = findViewById(R.id.compose_view)
    composeView.setContent 
        ComposeContent()
    


@Composable
fun ComposeContent() 
	//....

4.3 创建ids.xml,定义原生view id

resources/values目录下创建ids.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item type="id" name="my_calendar_view" />
</resources>

4.4 实现ComposeContent

@Composable
fun ComposeContent() 
    AndroidView(factory = 
        //这里也可以通过 layoutInflater.inflate(R.layout.xxxxxx) 的方式返回原生View
        val calendarView = CalendarView(it)
        val keyboard = R.id.my_calendar_view
        Log.i(TAG,"my_calendar_view id:$keyboard")
        calendarView.id = keyboard
        calendarView
    , modifier = Modifier.fillMaxWidth(), update = 
        it.setOnDateChangeListener  view, year, month, day ->
            Toast.makeText(view.context, "$year$month$day日", Toast.LENGTH_SHORT).show()
        
    )

4.5 在外层的原生代码处,获取Compose中的原生View

在原生代码的地方,通过composeView.findViewById查找id为my_calendar_view的原生View

window?.decorView?.post 
    val calendarViewId = R.id.my_calendar_view
    Log.i(TAG,"my_calendar_view id ===>:$calendarViewId")
    val calendarView = composeView.findViewById<CalendarView>(calendarViewId)
    Log.i(TAG,"calendarView:$calendarView")
    calendarView.setOnDateChangeListener  view, year, month, day ->
        Toast.makeText(view.context, "!!!! $year$month$day日", Toast.LENGTH_SHORT).show()
    

注意这里的window?.decorView?.post : 必须在页面加载完成后,才能查找到my_calendar_view对应的原生View,如果直接在onCreate里面去查找,会发现composeView.findViewById<CalendarView>(calendarViewId)返回的是null

4.6 运行项目

选择任意一个日期,可以发现弹出的toast是!!!! year年month月day日,即原生的setOnDateChangeListener覆盖了Compose中的setOnDateChangeListener监听,这样说明我们也在原生代码处,取到了Compose内部的原生View了。

5. 本文源码下载

本文源码下载地址 : Compose 和 Android 传统View 互相调用 示例 Demo

Android Jectpack-Compose 库的粗略学习

断断续续的看了几天的文档,测试过一部分的官网示例代码,使用的是1.0.5版本。

文章目录

Compose 相较于View的体系

优点是:一次测量,一次绘制,组合灵活多变。它不需要编写xml文件,纯代码编写界面,不用分窗口编码,导致分散开发人员的注意力。
缺点是:牺牲了代码可读性,大量嵌套。预览不太及时。

在一个项目中,或多个风格类似的项目中,定义一套基础组件视图的公共库,它们都是由一个个@Composable 函数构成的,从前的一堆xml和View源码文件,变成了一个个".kt"源文件。再有一套说明文档,那应用起来应该会酸爽一些。

状态(State) 与修饰符(Modifier)

提供了一些函数,来记忆需要的自定义的视图的状态变量。(androidx.compose.runtime.State)

提供了非常多的修饰符(Modifier)。通过修饰符,您可以更改可组合项的大小、布局、外观,还可以添加高级互动,例如使元素可点击。您可以将这些修饰符链接起来,以创建更丰富的可组合项。

关于预览

当增删了可组合项时,需要再次build,不一定要部署到设备上。当仅是修改可组合项中某个已添加的属性的值时,不需要再次build,会立即看到属性变化引起的界面更新效果。

这点比起xml布局,实时效果就有不如了。每次build,都是需要耗费一定时间的。
此外,能预览为可组合项添加的动画效果。这点xml布局,当然是没有的。

关于手势、动画、Canvas绘制

手势:单击、双击、长按、拖动,快速滑动等解决方案,文档中都能找到。
动画:提供各种基础动画的实现与自定义动画的文档。
Canvas:提供了Canvas()可组合函数。

关于xml类型的drawable资源

Compose中无法使用,以前xml类型的drawable资源。

关于主题

当然是主推Material 主题。根据实际情况自定义。

关于一些可用的支持库和功能函数

  • lib: ConstraintLayout
  • lib: ViewModel
  • lib: rxjava2
  • lib: LiveData 可观察数据
  • lib: Navigation 导航
  • lib: Hilt 依赖注入
  • lib: Paging 分页
  • lib: io.coil-kt:coil-compose 图片加载
  • CameraX
  • @Composable fun rememberLauncherForActivityResult(ActivityResultContracts)
    ···

和View的混用

Compose的可组合项,最终会组成一个ComposeView,它最上层的继承是ViewGroup。
如要在某个 activity 中使用 Compose,必须使用 ComponentActivity,一般使用FragmentActivity、AppCompatActivity当然没问题。

Activity中使用setContent()

// sdk 提供
fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) 

组合一个View

// sdk 提供
@Composable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
)
在factory实现中,加载一个xml layout。

ComposeView在Fragment中的应用

// sdk 提供
class ComposeView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr)
  • 在fragment#onCreateView() 返回一个 ComposeView
override fun onCreateView(
    inflater: LayoutInflater, 
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? 
    return ComposeView(requireContext()).apply 
        setContent 
            Text(text = "Hello world.")
        
    

  • 在fragment#onCreateView() 返回一个xml-View,xml中含有 ComposeView
return inflater.inflate(R.layout.fragment_compose_ui, container, false).apply 
    findViewById<ComposeView>(R.id.composeView).setContent 
        Text(text = "Hello world.")
    

以上是关于Compose 和 Android 传统View 互相调用的主要内容,如果未能解决你的问题,请参考以下文章

Android Jectpack-Compose 库的粗略学习

Android Jectpack-Compose 库的粗略学习

Jetpack Compose实践:完成自定义手势处理

Jetpack Compose 和 Compose Navigation 如何处理 Android 活动?

Android全新UI编程 - Jetpack Compose 超详细教程

Android jetpack compose中的按钮长按监听器