zoukankan      html  css  js  c++  java
  • Jetpack Compose和View的互操作性

    Jetpack Compose Interoperability

    Compose风这么大, 对于已有项目使用新技术, 难免会担心兼容性.
    对于Compose来说, 至少和View的结合是无缝的.
    (目前来讲, 已有项目要采用Compose, 可能初期要解决的就是升级gradle plugin, gradle, Android Studio, kotlin之类的问题.)

    构建UI的灵活性还是有保证的:

    • 新界面想用Compose, 可以.
    • Compose支持不了的, 用View.
    • 已有界面不想动, 可以不动.
    • 已有界面的一部分想用Compose, 可以.
    • 有的UI效果想复用之前的, 好的, 可以直接拿来内嵌.

    本文就是一些互相调用的简单小demo, 初期用的时候可以复制粘贴一下很趁手.

    官方文档:
    https://developer.android.com/jetpack/compose/interop/interop-apis

    在Activity或者Fragment中全部使用Compose来搭建UI

    Use Compose in Activity

    class ExampleActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            setContent { // In here, we can call composables!
                MaterialTheme {
                    Greeting(name = "compose")
                }
            }
        }
    }
    
    @Composable
    fun Greeting(name: String) {
        Text(text = "Hello $name!")
    }
    

    Use Compose in Fragment

    class PureComposeFragment : Fragment() {
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            return ComposeView(requireContext()).apply {
                setContent {
                    MaterialTheme {
                        Text("Hello Compose!")
                    }
                }
            }
        }
    }
    

    在View中使用Compose

    ComposeView内嵌在Xml中:

    一个平平无奇的xml布局文件中加入ComposeView:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <TextView
            android:id="@+id/hello_world"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Hello from XML layout" />
    
        <androidx.compose.ui.platform.ComposeView
            android:id="@+id/compose_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </LinearLayout>
    

    使用的时候, 先根据id查找出来, 再setContent:

    class ComposeViewInXmlActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_compose_view_in_xml)
    
            findViewById<ComposeView>(R.id.compose_view).setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
    }
    

    动态添加ComposeView

    在代码中使用addView()来添加View对于ComposeView来说也同样适用:

    class ComposeViewInViewActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            setContentView(LinearLayout(this).apply {
                orientation = VERTICAL
                addView(ComposeView(this@ComposeViewInViewActivity).apply {
                    id = R.id.compose_view_x
                    setContent {
                        MaterialTheme {
                            Text("Hello Compose View 1")
                        }
                    }
                })
                addView(TextView(context).apply {
                    text = "I'm am old TextView"
                })
                addView(ComposeView(context).apply {
                    id = R.id.compose_view_y
                    setContent {
                        MaterialTheme {
                            Text("Hello Compose View 2")
                        }
                    }
                })
            })
        }
    }
    

    这里在LinearLayout中添加了三个child: 两个ComposeView中间还有一个TextView.

    起到桥梁作用的ComposeView是一个ViewGroup, 它本身是一个View, 所以可以混进View的hierarchy tree里占位,
    它的setContent()方法开启了Compose世界的大门, 在这里可以传入composable的方法, 绘制UI.

    在Compose中使用View

    都用Compose搭建UI了, 什么时候会需要在其中内嵌View呢?

    • 要用的View还没有Compose版本, 比如AdView, MapView, WebView.
    • 有一块之前写好的UI, (暂时或者永远)不想动, 想直接用.
    • 用Compose实现不了想要的效果, 就得用View.

    在Compose中加入Android View

    例子:

    @Composable
    fun CustomView() {
        val state = remember { mutableStateOf(0) }
    
        //widget.Button
        AndroidView(
            factory = { ctx ->
                //Here you can construct your View
                android.widget.Button(ctx).apply {
                    text = "My Button"
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
                    setOnClickListener {
                        state.value++
                    }
                }
            },
            modifier = Modifier.padding(8.dp)
        )
        //widget.TextView
        AndroidView(factory = { ctx ->
            //Here you can construct your View
            TextView(ctx).apply {
                layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
            }
        }, update = {
            it.text = "You have clicked the buttons: " + state.value.toString() + " times"
        })
    }
    

    这里的桥梁是AndroidView, 它是一个composable方法:

    @Composable
    fun <T : View> AndroidView(
        factory: (Context) -> T,
        modifier: Modifier = Modifier,
        update: (T) -> Unit = NoOpUpdate
    )
    

    factory接收一个Context参数, 用来构建一个View.
    update方法是一个callback, inflate之后会执行, 读取的状态state值变化后也会被执行.

    在Compose中使用xml布局

    上面提到的在Compose中使用AndroidView的方法, 对于少量的UI还行.
    如果需要复用一个已经存在的xml布局怎么办?
    不用怕, view binding登场了.

    使用起来也很简单:

    • 首先你需要开启View Binding.
    buildFeatures {
        compose true
        viewBinding true
    }
    
    • 其次你需要一个xml的布局, 比如叫complex_layout.
    • 然后添加一个Compose view binding的依赖: androidx.compose.ui:ui-viewbinding.

    然后build一下, 生成binding类,
    这样就好了, 哒哒:

    @Composable
    private fun ComposableFromLayout() {
        AndroidViewBinding(ComplexLayoutBinding::inflate) {
            sampleButton.setBackgroundColor(Color.GRAY)
        }
    }
    

    其中ComplexLayoutBinding是根据布局名字生成的类.

    AndroidViewBinding内部还是调用了AndroidView这个composable方法.

    番外篇: 在Compose中显示Fragment

    这个场景听上去有点奇葩, 因为Compose的设计理念, 貌似就是为了跟Fragment说再见.
    在Compose构建的UI中, 再找地方显示一个Fragment, 有点新瓶装旧酒的意思.

    但是遇到的场景多了, 你没准真能遇上呢.

    Fragment通过FragmentManager添加, 需要一个布局容器.
    把上面ViewBinding的例子改改, 布局里加入一个fragmentContainer, 点击显示Fragment:

    Column(Modifier.fillMaxSize()) {
        Text("I'm a Compose Text!")
        Button(
            onClick = {
                showFragment()
            }
        ) {
            Text(text = "Show Fragment")
        }
        ComposableFromLayout()
    }
    
    @Composable
    private fun ComposableFromLayout() {
        AndroidViewBinding(
            FragmentContrainerBinding::inflate,
            modifier = Modifier.fillMaxSize()
        ) {
    
        }
    }
    
    private fun showFragment() {
        supportFragmentManager
            .beginTransaction()
            .add(R.id.fragmentContainer, PureComposeFragment())
            .commit()
    }
    

    这里没有考虑时机的问题, 因为点击按钮展示Fragment, 将时机拖后了.
    如果直接在初始化的时候想显示Fragment, 可能会抛出异常:

    java.lang.IllegalArgumentException: No view found for id
    

    解决办法:

    @Composable
    private fun ComposableFromLayout() {
        AndroidViewBinding(
            FragmentContrainerBinding::inflate,
            modifier = Modifier.fillMaxSize()
        ) {
            // here is safe
            showFragment()
        }
    }
    

    所以show的时机至少要保证container view已经inflated了.

    Theme & Style

    迁移View的app到Compose, 你可能会需要Theme Adapter:
    https://github.com/material-components/material-components-android-compose-theme-adapter

    关于在现有的view app中使用compose:
    https://developer.android.com/jetpack/compose/interop/compose-in-existing-ui

    总结

    Compose和View的结合, 主要是靠两个桥梁.
    还挺有趣的:

    • ComposeView其实是个Android View.
    • AndroidView其实是个Composable方法.

    Compose和View可以互相兼容的特点保证了项目可以逐步迁移, 并且也给够了安全感, 像极了当年java项目迁移kotlin.
    至于什么学习曲线, 经验不足, 反正早晚都要学的, 整点新鲜的也挺好, 亦可赛艇.

    作者: 圣骑士Wind
    出处: 博客园: 圣骑士Wind
    Github: https://github.com/mengdd
    微信公众号: 圣骑士Wind
    微信公众号: 圣骑士Wind
  • 相关阅读:
    离鞘剑(四)
    Node.js 蚕食计划(四)—— Express + SQL Server 搭建电影网站
    离鞘剑(三)
    python数据类型(第三弹)
    python数据类型(第二弹)
    python数据类型(第一弹)
    开发工具的安装与使用(总结篇)
    开发工具的安装与使用(第四弹)
    PriorityQueue和PriorityBlockingQueue
    一文弄懂java中的Queue家族
  • 原文地址:https://www.cnblogs.com/mengdd/p/Jetpack-Compose-Interoperability.html
Copyright © 2011-2022 走看看