Compose-Multiplatform在Android和iOS上的实践

6d7a27dc0a61df5624d01a8ccdbc75b9.jpeg

cf7a58fb7a3abef5036d2faaf60c32ac.gif

本文字数:4680

预计阅读时间:30分钟

557978ff116427b2fa90a6ced1222e61.png

01

简介

之前我们探讨过KMM,即Kotlin Multiplatform Mobile,是Kotlin发布的移动端跨平台框架。当时的结论是KMM提倡将共有的逻辑部分抽出,由KMM封装成Android(Kotlin/JVM)的aar和iOS(Kotlin/Native)的framework,再提供给View层进行调用,从而节约一部分的工作量。共享的是逻辑而不是UI。(1)

其实在这个时候我们就知道Kotlin在移动端的跨平台绝对不是想止于逻辑层的共享,随着Compose的日渐成熟,JetBrains推出了Compose-Multiplatform,从UI层面上实现移动端,Web端,桌面端的跨平台。考虑到屏幕大小与交互方式的不同,Android和iOS之间的共享会极大的促进开发效率。比如现在已经非常成熟的Flutter。令人兴奋的是,Compose-Multiplatform目前已经发布了支持iOS系统的alpha版本,虽然还在开发实验阶段,但我们已经开始尝试用起来了。

02

Jetpack-Compose

Compose-Multiplatform

作为Android开发,Jetpack-Compose我们再熟悉不过了,是Google针对Android推出的新一代声明式UI工具包,完全基于Kotlin打造,天然具备了跨平台的使用基础。JetBrains以Jetpack-Compose为基础,相继发布了compose-desktop,compose-web和compose-iOS ,使Compose可以运行在更多不同平台,也就是我们今天要讲的Compose-Multiplatform。在通用的API上Compose-Multiplatform与Jetpack-Compose时刻保持一致,不同的只是包名发生了变化。因此作为Android开发,我们在使用Compose-Multiplatform时,可以将Jetpack-Compose代码低成本地迁移到Compose-Multiplatform:

1215f2fb6462a26774197cd85a689898.png

03

使用

既然是UI框架,那么我们就来实现一个简单的在移动端非常常规的业务需求:

从服务器请求数据,并以列表形式展现在UI上。

在此我们要说明的是,Compose-Multiplatform是要与KMM配合使用的,其中KMM负责把shared模块编译成Android的aar和iOS的framework,Compose-Multiplatform负责UI层面的交互与绘制的实现。

首先我们先回顾一下KMM工程的组织架构:

0c2f26024b2d8bdb5efeac946d652680.png

其中androidApp和iosApp分别为Android和iOS这两个平台的主工程模块,shared为共享逻辑模块,供androidApp和iosApp调用。shared模块中:

  • commonMain为公共模块,该模块的代码与平台无关,是通过expected关键字对一些api的声明(声明的实现在platform module中);

  • androidMain和iosMain分别Android和ios这两个平台,通过actual关键字在平台模块进行具体的实现。

关于kmm工程的配置与使用方式,运行方式,编译过程原理还是请回顾一下之前的文章,在此不做赘述。(2)

接下来我们看Compose-Multiplatform是怎么基于kmm工程进行的实现。 

1、添加配置

在settings.gradle文件中声明compose插件:

plugins{
//...
        val composeVersion = extra["compose.version"] as String
        id("org.jetbrains.compose").version(composeVersion)
    }

其中compose.version在gradle.properties进行了声明。需要注意的是目前Compose-Multiplatform的版本有要求,目前可以参考官方的具体配置。(3)

#Versions
kotlin.version=1.8.20
agp.version=7.4.2
compose.version=1.4.0

之后在shared模块的build.gradle文件中引用声明好的插件如下:

plugins {
//...
    id("org.jetbrains.compose")
}

同时我们需要在build.gradle文件中配置compose静态资源文件的目录,方式如下:

  • Android:

android {
//...
    sourceSets["main"].resources.srcDirs("src/commonMain/resources")
}
  • iOS:

cocoapods {
//...
        extraSpecAttributes["resources"] =
            "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
    }

这意味着在寻找如图片等资源文件时,将从src/commonMain/resources/这个目录下寻找,如下图所示:

97f11e912dc417a293ed7365838be992.jpeg

由于目前compose-iOS还处于实验阶段,我们需要在gradle.properties文件中添加如下代码开启UIKit:

org.jetbrains.compose.experimental.uikit.enabled=true

最后我们需要在为commonMain添加compose依赖:

val commonMain by getting {
            dependencies {
//...
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material)
//                //implementation(compose.materialIconsExtended) // TODO not working on iOS for now
                @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
                implementation(compose.components.resources)
                implementation(compose.ui)

            }
        }

好了到此为止我们的配置就完成了,接下来开始写业务代码了。既然是从服务器获取数据,我们肯定得封装一个网络模块,下面我们将使用ktor封装一个简单的网络模块。 

2、网络模块

先我们先在shared模块的build.gradle文件中添加依赖如下:

val commonMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-core:$ktor_version")//core
                implementation("io.ktor:ktor-client-cio:$ktor_version")//CIO
                implementation("io.ktor:ktor-client-logging:$ktor_version")//Logging
                implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
                implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")//Json格式化
//...
            }
        }

接下来我们封装一个最简单的HttpUtil,包含post和get请求;

package com.example.sharesample

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.*
import kotlinx.serialization.json.Json

class HttpUtil{
    companion object{
        val client: HttpClient = HttpClient(CIO) {
            expectSuccess = true
            engine {
                maxConnectionsCount = 1000
                requestTimeout = 30000
                endpoint {
                    maxConnectionsPerRoute = 100
                    pipelineMaxSize = 20
                    keepAliveTime = 30000
                    connectTimeout = 30000
                }
            }
            install(Logging) {
                logger = Logger.DEFAULT
                level = LogLevel.HEADERS
            }

            install(ContentNegotiation) {
                json(Json {
                    ignoreUnknownKeys = true
                    isLenient = true
                    encodeDefaults = false
                })
            }
        }

        suspend inline fun <reified T> get(
            url: String,//请求地址
        ): T?  {
            return try {
                val response: HttpResponse = client.get(url) {//GET请求
                    contentType(ContentType.Application.Json)//content-type
                }
                val data: T = response.body()
                data
            } catch (e: ResponseException) {
                print(e.response)
                null
            } catch (e: Exception) {
                print(e.message)
                null
            }
        }

        suspend inline fun <reified T> post(
            url: String,
        ): T?  {//coroutines 中的IO线程
            return try {
                val response: HttpResponse = client.post(url) {//POST请求
                    contentType(ContentType.Application.Json)//content-type
                }
                val data: T = response.body()
                data
            } catch (e: ResponseException) {
                print(e.response)
                null
            } catch (e: Exception) {
                print(e.message)
                null
            }
        }
    }
}

代码非常直观,定义了HttpClient对象,进行了基础的设置来实现网络请求。我们来定义一下接口请求返回的数据结构。

3、返回的数据结构

package com.example.sharesample.bean

@kotlinx.serialization.Serializable
class SearchResult {
    var count: Int? = null
    var resInfos: List<ResInfoBean>? = null
}
package com.example.sharesample.bean

@kotlinx.serialization.Serializable
class ResInfoBean {
    var name: String? = null
    var desc: String? = null
}

接下来我们看看是怎么发送的请求。

4、发送请求

然后我们定义个SearchApi:

package com.example.sharesample

import androidx.compose.material.Text
import androidx.compose.runtime.*
import com.example.sharesample.bean.SearchResult
import io.ktor.client.plugins.logging.*
import kotlinx.coroutines.*

class SearchApi {
    suspend fun search(): SearchResult {
        Logger.SIMPLE.log("search2")
        var result: SearchResult? =
            HttpUtil.get(url = "http://h5-yapi.sns.sohuno.com/mock/229/api/v1/resInfo/search")
        if (result == null) {
            result = SearchResult()
        }
        return result
    }
}

实现了search()方法。接着我们来看view层的实现与数据的绑定是如何实现的。

5、View层的实现

我们创建一个SearchCompose:

package com.example.sharesample

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.sharesample.bean.SearchResult
import io.ktor.client.plugins.logging.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.resource

class SearchCompose {
    private val searchApi = SearchApi()
    private var isInit = false

    @OptIn(ExperimentalResourceApi::class)
    @Composable
    fun searchCompose() {
        var searchResult by remember { mutableStateOf<SearchResult>(SearchResult()) }
        if (!isInit) {
            scope().launch {
                val result = async {
                    searchApi.search()
                }
                searchResult = result.await()
            }
            isInit = true
        }

        Column {
            Text(
                "Total: ${searchResult.count ?: 0}",
                style = TextStyle(fontSize = 20.sp),
                modifier = Modifier.padding(start = 20.dp, top = 20.dp)
            )
            val scrollState = rememberLazyListState()

            if (searchResult.resInfos != null) {
                LazyColumn(
                    state = scrollState,
                    modifier = Modifier.padding(
                        top = 14.dp,
                        bottom = 50.dp,
                        end = 14.dp,
                        start = 14.dp
                    )

                ) {
                    items(searchResult.resInfos!!) { item ->
                        Box(
                            modifier = Modifier.padding(top = 20.dp).fillMaxWidth()
                                .background(color = Color.LightGray, shape = RoundedCornerShape(10.dp))
                                .padding(all = 20.dp)
                        ) {
                            Column {
                                Row(verticalAlignment = Alignment.CenterVertically) {
                                    val picture = "1.jpg"
                                    var imageBitmap: ImageBitmap? by remember(picture) {
                                        mutableStateOf(
                                            null
                                        )
                                    }
                                    LaunchedEffect(picture) {
                                        try {
                                            imageBitmap =
                                                resource(picture).readBytes().toImageBitmap()
                                        } catch (e: Exception) {
                                        }

                                    }
                                    if (imageBitmap != null) {
                                        Image(
                                            bitmap = imageBitmap!!, "", modifier = Modifier
                                                .size(60.dp)
                                                .clip(RoundedCornerShape(10.dp))
                                        )
                                    }

                                    Text(
                                        item.name ?: "name",
                                        style = TextStyle(color = Color.Yellow),
                                        modifier = Modifier.padding(start = 10.dp)
                                    )
                                }
                                Text(item.desc ?: "desc", style = TextStyle(color = Color.White))
                            }

                        }
                    }
                }
            }
        }


    }
}

@Composable
fun scope(): CoroutineScope {
    var viewScope = rememberCoroutineScope()
    return remember {
        CoroutineScope(SupervisorJob(viewScope.coroutineContext.job) + ioDispatcher)
    }
}

在searchCompose()里我们看到了在发送请求时开启了一个协程,scope()方法指定了作用域,除此之外,我们还定义了ioDispatcher在不同平台下的实现,具体的声明如下:

expect val ioDispatcher: CoroutineDispatcher

在Android上的实现:

actual val ioDispatcher = Dispatchers.IO

在ios上的实现:

actual val ioDispatcher = Dispatchers.IO

需要注意的是,Android平台,Dispatchers.IO在jvmMain/Dispatchers,ios平台,Dispatchers.IO在nativeMain/Dispatchers下。两者是不一样的。在获取了服务端数据后,我们使用LazyColumn对列表进行实现。其中有图片和文本的展示。为了方便进行说明,图片数据我们使用本地resources目录下的图片,文本展示的是服务端返回的数据。下面我来说明一下图片的加载。

6、图片加载

具体的实现如下:

val picture = "1.jpg"
var imageBitmap: ImageBitmap? by remember(picture) {
    mutableStateOf(
        null
    )
}
LaunchedEffect(picture) {
    try {
        imageBitmap =
            resource(picture).readBytes().toImageBitmap()
    } catch (e: Exception) {
    }

}
if (imageBitmap != null) {
    Image(
        bitmap = imageBitmap!!, "", modifier = Modifier
            .size(60.dp)
            .clip(RoundedCornerShape(10.dp))
    )
}

先创建了一个ImageBitmap的remember对象,由于resource(picture).readBytes()是个挂起函数,我们需要用LaunchedEffect来执行。这段代码的作用是从resources目录下读取资源到内存中,然后我们在不同平台实现了toImageBitmap()将它转换成Bitmap。

  • toImageBitmap()的声明:

expect fun ByteArray.toImageBitmap(): ImageBitmap
  • Android端的实现:

fun ByteArray.toAndroidBitmap(): Bitmap {
    return BitmapFactory.decodeByteArray(this, 0, size)
}
  • iOS端的实现:

actual fun ByteArray.toImageBitmap(): ImageBitmap =
    Image.makeFromEncoded(this).toComposeImageBitmap()

好了通过以上的方式我们就可以实现对本地图片的加载,到此为止,Compose的相应实现就完成了。那么它是怎么被Android和ios的view引用的呢?Android端我们已经非常熟悉了,和Jetpack-Compose的调用方式一样,在MainActivity中直接调用即可:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    SearchCompose().searchCompose()
                }
            }
        }
    }
}

ios端会稍微麻烦一点。我们先来看一下iosApp模块下iOSApp.swift的实现:

import UIKit
import shared

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let mainViewController = Main_iosKt.MainViewController()
        window?.rootViewController = mainViewController
        window?.makeKeyAndVisible()
        return true
    }
}

关键代码是这两行:

let mainViewController = Main_iosKt.MainViewController()
        window?.rootViewController = mainViewController

创建了一个MainViewController对象,然后赋给window的rootViewController。这个MainViewController是在哪儿怎么定义的呢?我们回到shared模块,定义一个main.ios的文件,它会在framework编译成Main_iosKt文件。main.ios的实现如下:

package com.example.sharesample

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIViewController

@Suppress("FunctionName", "unused")
fun MainViewController(): UIViewController =
    ComposeUIViewController {
        MaterialTheme {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colors.background
            ) {
                SearchCompose().searchCompose()

            }
        }
    }

我们看到在这儿会创建一个UIViewController对象MainViewController。这个便是ios端和Compose链接的桥梁。接下来我们来看看在Android和ios上的效果。

  • Android端:

9bf3253a1b092defa9b0bdac068b8688.jpeg

  • iOS端:

eff1d4888796aabe9c0a67aec41bf7e4.png

好了到此为止,我们看到了一个简单的列表业务逻辑是怎样实现的了。由于Compose-Multiplatform还未成熟,在业务实现上势必有很多内容需要自己造轮子。 

04

Android端的compose绘制原理

由于网上已经有很多Compose的相关绘制原理,下一章我们只是进行简单的源码解析,来说明它是如何生成UI树并进行自绘的。

1、Android端的compose绘制原理

Android端是在从onCreate()里实现setContent()开始的:

setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    SearchCompose().searchCompose()
                }
            }
        }

setContent()的实现如下:

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

我们看到它主要是生成了ComposeView然后通过setContent(content)将compose的内容注册到ComposeView里,其中ComposeView继承ViewGroup,然后调用ComponentActivity的setContentView()方法将ComposeView添加到DecorView中相应的子View中。通过追踪ComposeView的setContent方法:

private fun doSetContent(
    owner: AndroidComposeView,
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    if (inspectionWanted(owner)) {
        owner.setTag(
            R.id.inspection_slot_table_set,
            Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
        )
        enableDebugInspectorInfo()
    }
   // 创建Composition对象,传入UiApplier
    val original = Composition(UiApplier(owner.root), parent)
    val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
        as? WrappedComposition
        ?: WrappedComposition(owner, original).also {
            owner.view.setTag(R.id.wrapped_composition_tag, it)
        }
   // 传入content函数
    wrapped.setContent(content)
    return wrapped
}

我们发现主要做了两件事情:

  • 1.创建Composition对象,传入UiApplier

  • 2.传入content函数

其中UiApplier的定义如下:

internal class UiApplier(
    root: LayoutNode
) : AbstractApplier<LayoutNode>(root)

持有一个LayoutNode对象,它的说明如下:

An element in the layout hierarchy, built with compose UI

可以看到LayoutNode就是在Compose渲染的时候,每一个组件就是一个LayoutNode,最终组成一个LayoutNode树,来描述UI界面。LayoutNode是怎么创建的呢?

1)LayoutNode

我们假设创建一个Image,来看看Image的实现:

fun Image(
    painter: Painter,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null
) {
//...
    Layout(
        {},
        modifier.then(semantics).clipToBounds().paint(
            painter,
            alignment = alignment,
            contentScale = contentScale,
            alpha = alpha,
            colorFilter = colorFilter
        )
    ) { _, constraints ->
        layout(constraints.minWidth, constraints.minHeight) {}
    }
}

继续追踪Layout()的实现:

@Composable inline fun Layout(
    content: @Composable @UiComposable () -> Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    val viewConfiguration = LocalViewConfiguration.current
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
        },
        skippableUpdate = materializerOf(modifier),
        content = content
    )
}

@Composable @ExplicitGroupsComposable
inline fun <T, reified E : Applier<*>> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit,
    noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
    content: @Composable () -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startReusableNode()
    if (currentComposer.inserting) {
        currentComposer.createNode(factory)
    } else {
        currentComposer.useNode()
    }
    Updater<T>(currentComposer).update()
    SkippableUpdater<T>(currentComposer).skippableUpdate()
    currentComposer.startReplaceableGroup(0x7ab4aae9)
    content()
    currentComposer.endReplaceableGroup()
    currentComposer.endNode()
}

在这里创建了ComposeUiNode对象,而LayoutNode就是ComposeUiNode的实现类。我们再来看看Composition。

2)Composition

从命名来看,Composition的作用就是将LayoutNode组合起来。其中WrappedComposition继承Composition:

private class WrappedComposition(
    val owner: AndroidComposeView,
    val original: Composition
) : Composition, LifecycleEventObserver

我们来追踪一下它的setContent()的实现:

override fun setContent(content: @Composable () -> Unit) {
        owner.setOnViewTreeOwnersAvailable {
            if (!disposed) {
                val lifecycle = it.lifecycleOwner.lifecycle
                lastContent = content
                if (addedToLifecycle == null) {
                    addedToLifecycle = lifecycle
                    // this will call ON_CREATE synchronously if we already created
                    lifecycle.addObserver(this)
                } else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
                    original.setContent {

                        @Suppress("UNCHECKED_CAST")
                        val inspectionTable =
                            owner.getTag(R.id.inspection_slot_table_set) as?
                                MutableSet<CompositionData>
                                ?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
                                    as? MutableSet<CompositionData>
                        if (inspectionTable != null) {
                            inspectionTable.add(currentComposer.compositionData)
                            currentComposer.collectParameterInformation()
                        }

                        LaunchedEffect(owner) { owner.boundsUpdatesEventLoop() }

                        CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
                            ProvideAndroidCompositionLocals(owner, content)
                        }
                    }
                }
            }
        }
    }

在页面的生命周期是CREATED的状态下,执行original.setContent():

override fun setContent(content: @Composable () -> Unit) {
        check(!disposed) { "The composition is disposed" }
        this.composable = content
        parent.composeInitial(this, composable)
    }

调用parent的composeInitial()方法,这段代码我们就不再继续追踪下去了,它最终的作用就是对布局进行组合,创建父子依赖关系。 

3)Measure和Layout

在AndroidComposeView中的dispatchDraw()实现了measureAndLayout()方法:

override fun measureAndLayout(sendPointerUpdate: Boolean) {
        trace("AndroidOwner:measureAndLayout") {
            val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
            val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
            if (rootNodeResized) {
                requestLayout()
            }
            measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
        }
    }

    fun measureAndLayout(onLayout: (() -> Unit)? = null): Boolean {
        var rootNodeResized = false
        performMeasureAndLayout {
            if (relayoutNodes.isNotEmpty()) {
                relayoutNodes.popEach { layoutNode ->
                    val sizeChanged = remeasureAndRelayoutIfNeeded(layoutNode)
                    if (layoutNode === root && sizeChanged) {
                        rootNodeResized = true
                    }
                }
                onLayout?.invoke()
            }
        }
        callOnLayoutCompletedListeners()
        return rootNodeResized
    }

调用remeasureAndRelayoutIfNeeded,遍历relayoutNodes,为每一个LayoutNode去进行measure和layout。具体的实现不分析了。

4)绘制

我们还是以Image举例:

fun Image(
    bitmap: ImageBitmap,
    contentDescription: String?,
    modifier: Modifier = Modifier,
    alignment: Alignment = Alignment.Center,
    contentScale: ContentScale = ContentScale.Fit,
    alpha: Float = DefaultAlpha,
    colorFilter: ColorFilter? = null,
    filterQuality: FilterQuality = DefaultFilterQuality
) {
    val bitmapPainter = remember(bitmap) { BitmapPainter(bitmap, filterQuality = filterQuality) }
    Image(
        painter = bitmapPainter,
        contentDescription = contentDescription,
        modifier = modifier,
        alignment = alignment,
        contentScale = contentScale,
        alpha = alpha,
        colorFilter = colorFilter
    )
}

主要的绘制工作是由BitmapPainter完成的,它继承自Painter。

override fun DrawScope.onDraw() {
        drawImage(
            image,
            srcOffset,
            srcSize,
            dstSize = IntSize(
                this@onDraw.size.width.roundToInt(),
                this@onDraw.size.height.roundToInt()
            ),
            alpha = alpha,
            colorFilter = colorFilter,
            filterQuality = filterQuality
        )
    }

在onDraw()方法里实现了drawImage():

override fun drawImage(
        image: ImageBitmap,
        srcOffset: IntOffset,
        srcSize: IntSize,
        dstOffset: IntOffset,
        dstSize: IntSize,
        /*FloatRange(from = 0.0, to = 1.0)*/
        alpha: Float,
        style: DrawStyle,
        colorFilter: ColorFilter?,
        blendMode: BlendMode,
        filterQuality: FilterQuality
    ) = drawParams.canvas.drawImageRect(
        image,
        srcOffset,
        srcSize,
        dstOffset,
        dstSize,
        configurePaint(null, style, alpha, colorFilter, blendMode, filterQuality)
    )

而最终也是在Canvas上进行了绘制。通过以上的分析,我们了解到Compose并不是和原生控件一一映射的关系,而是像Flutter一样,有自己的UI组织方式,并最终调用自绘引擎直接在Canvas上进行绘制的。在Android和iOS端使用的自绘引擎是skiko。这个skiko是什么呢?它其实是Skia for Kotlin的缩写(Flutter在移动端也是用的Skia引擎进行的绘制)。事实上不止是在移动端,我们可以通过以下的截图看到,Compose的桌面端和Web端的绘制实际上也是用了skiko:

c01b350e45a530c449edefe60d6f580a.png

关于skiko的更多信息,还请查阅文末的官方链接。(4)

好了到此为止,Compose的在Android端的绘制原理我们就讲完了。对其他端绘制感兴趣的同学可自行查看相应的源码,细节有不同,但理念都是一致的:创建自己的Compose树,并最终调用自绘引擎在Canvas上进行绘制。

05

Compose-Multiplatform与Flutter 

为什么要单拿它俩出来说一下呢?是因为在调研Compose-Multiplatform的过程中,我们发现它跟Flutter的原理类似,那未来可能就会有竞争,有竞争就意味着开发同学若在自己的项目中使用跨平台框架需要选择。那么我们来对比一下这两个框架:在之前KMM的文章中,我们比较过KMM和Flutter,结论是:

  • KMM主要实现的是共享逻辑,UI层的实现还是建议平台各自去处理;

  • Flutter是UI层的共享。

当时看来两者虽然都是跨平台,但目标不同,看上去并没有形成竞争。而在Compose-Multiplatform加入之后,结合KMM,成为了逻辑和UI都可以实现共享的结果。而且从绘制原理上来说,Compose和Flutter都是创建自己的View树,在通过自绘引擎进行渲染,原理上差异不大。再加上Kotlin和Compose作为Android的官方推荐,对于Android同学来说,基本上是没有什么学习成本的。个人认为若Compose-Multiplatform更加成熟,发布稳定版后与Flutter的竞争会非常大。 

06

总结

Compose-Multiplatform目前虽然还不成熟,但通过对其原理的分析,我们可以预见的是,结合KMM,未来将成为跨平台的有力竞争者。特别对于Android开发同学来说,可以把KMM先用起来,结合Compose去实现一些低耦合的业务,待未来Compose-iOS发布稳定版后,可以愉快的进行双端开发,节约开发成本。

参考:

(1)https://www.jianshu.com/p/e1ae5eaa894e

(2)https://www.jianshu.com/p/e1ae5eaa894e 

(3)https://github.com/JetBrains/compose-multiplatform-ios-android-template

(4)https://github.com/JetBrains/skiko

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/117136.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring Cloud分布式缓存

目录 单点Redis Redis数据持久化 RDB持久化 bgsave细节 RDB的缺点 AOF持久化 AOF的问题 RDB与AOF对比 搭建Redis主从架构 数据同步原理 全量同步 增量同步 主从同步优化 Redis哨兵 集群检测 选举主节点 故障转移 搭建哨兵集群 RedisTemplate的哨兵模式 单点…

ConcurrentHashMap是如何实现线程安全的

目录 原理&#xff1a; 初始化数据结构时的线程安全 put 操作时的线程安全 原理&#xff1a; 多段锁cassynchronize 初始化数据结构时的线程安全 在 JDK 1.8 中&#xff0c;初始化 ConcurrentHashMap 的时候这个 Node[] 数组是还未初始化的&#xff0c;会等到第一次 put() 方…

【Java】三种方案实现 Redis 分布式锁

序言 setnx、Redisson、RedLock 都可以实现分布式锁&#xff0c;从易到难得排序为&#xff1a;setnx < Redisson < RedLock。一般情况下&#xff0c;直接使用 Redisson 就可以啦&#xff0c;有很多逻辑框架的作者都已经考虑到了。 方案一&#xff1a;setnx 1.1、简单实…

PDF 表单直接保存到您的文档中--TX Text Control

TX Text Control .NET Server for ASP.NET Document Viewer 32.0.2 允许用户保存包含已填写表单字段的文档&#xff0c;从而更轻松地协作和共享信息。 TX Text Control .NET Server for ASP.NET 是一个适用于 ASP.NET 和 ASP.NET Core 的综合服务器端文档处理库。功能包括 PDF …

程序员笔记本电脑选 windows 还是 MAC

计算机选择是每个进入 IT 行业同学的第一个重要选择&#xff0c;那么你是怎么选择的呢&#xff1f; 选择操作系统&#xff08;Windows还是macOS&#xff09;取决于程序员的需求、偏好和工作流程。每个操作系统都有其优点和缺点&#xff0c;下面将分别讨论它们&#xff0c;以帮助…

volatile-无原子性案例详解

package com.nanjing.gulimall.zhouyimo.controller;import java.util.concurrent.TimeUnit;/*** author zhou* version 1.0* date 2023/11/5 7:56 下午*/ class MyNumber{int number;public synchronized void add(){number;} } public class VolatileNoAtomicDemo {public st…

gcc -static 在centos stream8 和centos stream9中运行报错的解决办法

gcc -static 在centos stream8 和centos stream9中运行报错的解决办法&#xff1a; 报/usr/bin/ld: cannot find -lc 我们下载glibc-static&#xff1a; 选择x86_64的。 还有一个是libxcrypt-static&#xff0c;依旧在这个网站里搜。 rpm -ivh glibc-static-2.28-239.el8.x…

排序——冒泡排序

冒泡排序的基本思想 从前往后&#xff08;或从后往前&#xff09;两两比较相邻元素的值&#xff0c;若为逆序&#xff08;即 A [ i − 1 ] < A [ i ] A\left [ i-1\right ]<A\left [ i\right ] A[i−1]<A[i]&#xff09;&#xff0c;则交换它们&#xff0c;直到序列…

【Linux】多路IO复用技术③——epoll详解如何使用epoll模型实现简易的一对多服务器(附图解与代码实现)

在正式阅读本篇博客之前&#xff0c;建议大家先按顺序把下面这两篇博客看一下&#xff0c;否则直接来看这篇博客的话估计很难搞懂 多路IO复用技术①——select详解&如何使用select模型在本地主机实现简易的一对多服务器http://t.csdnimg.cn/BiBib多路IO复用技术②——poll…

Web3游戏的十字路口:沿用传统IP还是另起炉灶?

人们经常问我对 Web3 游戏有什么看法。因此&#xff0c;我想以书面形式概述一下我目前的想法。 让我先澄清一下&#xff1a;我不是专家。这不是一篇深入探讨游戏世界精细指标如 MAU 或 D14 等的全面分析。请把这看作是我根据个人交流和研究&#xff0c;这反映我在游戏领域关注…

java代码检查

目录 jacoco 引入依赖 构建配置修改 单元测试 生成报告 查看报告 报告说明 1. Instructions 2. Branches 3. Cyclomatic Complexity 4. Lines 5. Methods 6. Classes sonar7.7 基础环境 需要下载软件 解压文件并配置 运行启动 jacoco 引入依赖 <dep…

jenkins安装以及基本配置

一、docker 1.安装docker 联网安装命令如下 curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun或者也可以使用国内 daocloud 一键安装命令&#xff1a; curl -sSL https://get.daocloud.io/docker | sh2.启动docker systemctl start docker二、docker…

pycharm更改远程服务器地址

一、问题描述 在运行一些项目时&#xff0c;我们常需要在pycharm中连接远程服务器&#xff0c;但万一远程服务器的ip发生了变化&#xff0c;该如何修改呢&#xff1f;我们在file-settings-python interpreter中找到远程服务器&#xff0c;但是发现ip是灰色的&#xff0c;没有办…

Azure 机器学习 - 使用 Visual Studio Code训练图像分类 TensorFlow 模型

了解如何使用 TensorFlow 和 Azure 机器学习 Visual Studio Code 扩展训练图像分类模型来识别手写数字。 关注TechLead&#xff0c;分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验&#xff0c;同济本复旦硕&#xff0c;复旦机器人智能实验室成员…

curl(七)上传和下载

一 上传 ① -T | --upload 上传 ​1、向ftp服务器 传一个文件&#xff1a;curl -T localfile -u name&#xff1a;passwd ftp://upload_site&#xff1a;port/path/2、向http服务器上传文件curl -T localfile http://www.wzj.com/wzj.html注意: 这时候使用的协议是HTTP的PUT…

基于STM32设计的室内环境监测系统(华为云IOT)_2023

一、设计需求 基于STM32+华为云物联网平台设计一个室内环境监测系统,以STM32系列单片机为主控器件,采集室内温湿度、空气质量、光照强度等环境参数,将采集的数据结果在本地通过LCD屏幕显示,同时上传到华为云平台并将上传的数据在Android移动端能够实时显示、查看。 【1…

5.数据表基本操作

目录 1.创建数据表 创建数据表的语法格式&#xff1a; 查看当前数据库的表&#xff1a; 主键 1.单字段主键 (1)在定义列的同时指定主键&#xff0c;语法规则如下&#xff1a; (2)在定义完所有列之后指定主键。 2.多字段联合主键 外键&#xff1a; 非空约束&#xff1…

react_11

MobX 介绍 需求&#xff0c;组件0 改变了数据&#xff0c;其它组件也想获得改变后的数据&#xff0c;如图所示 这种多个组件之间要共享状态数据&#xff0c;useState 就不够用了&#xff0c;useContext 也不好用了 能够和 react 配合使用的状态管理库有 MobX Redux 其中…

黑猫带你学NandFlash第3篇:NAND寻址(行列地址和block/page/LUN之间的关系)

本文依据不同型号NandFlash spec及个人工作经验整理而成,如有错误请留言。 文章为付费内容,已加入原创侵权保护,禁止私自转载及抄袭。 文章所在专栏:《黑猫带你学:NandFlash详解》 本文大约2000字,主要讲解:nand flash如何物理寻址、多plane又是如何寻址、相关计算公式等…

Spring Data Redis + RabbitMQ - 基于 string 实现缓存、计数功能(同步数据)

目录 一、Spring Data Redis 1.1、缓存功能 1.1.1、分析 1.1.2、案例实现 1.1.3、效果演示 1.2、计数功能&#xff08;Redis RabbitMQ&#xff09; 1.2.1、分析 1.2.2、案例实现 一、Spring Data Redis 1.1、缓存功能 1.1.1、分析 使用 redis 作为缓存&#xff0c; M…