1.简介
之前我们探讨过KMM
,即Kotlin Multiplatform Mobile
,是Kotlin
发布的移动端跨平台框架。当时的结论是KMM
提倡将共有的逻辑部分抽出,由KMM
封装成Android(Kotlin/JVM)的aar
和iOS(Kotlin/Native)的framework
,再提供给View
层进行调用,从而节约一部分的工作量。共享的是逻辑而不是UI
。大家可阅读以下链接进行回顾:
https://www.jianshu.com/p/e1ae5eaa894e
其实在这个时候我们就知道Kotlin
在移动端的跨平台绝对不是想止于逻辑层的共享,随着Compose
的日渐成熟,JetBrains
推出了Compose-Multiplatform
,从UI
层面上实现移动端,Web端,桌面端的跨平台。考虑到屏幕大小与交互方式的不同,Android
和iOS
之间的共享会极大的促进开发效率。比如现在已经非常成熟的Flutter
。令人兴奋的是,Compose-Multiplatform
目前已经发布了支持iOS
系统的alpha
版本,虽然还在开发实验阶段,但我们已经开始尝试用起来了。
2.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
:
3.使用
既然是UI
框架,那么我们就来实现一个简单的在移动端非常常规的业务需求:
从服务器请求数据,并以列表形式展现在
UI
上。
在此我们要说明的是,Compose-Multiplatform
是要与KMM
配合使用的,其中KMM
负责把shared
模块编译成Android
的aar
和iOS
的framework
,Compose-Multiplatform
负责UI
层面的交互与绘制的实现。
首先我们先回顾一下KMM
工程的组织架构:
其中
androidApp
和iosApp
分别为Android
和iOS
这两个平台的主工程模块,shared
为共享逻辑模块,供androidApp
和iosApp
调用。shared
模块中:
-
commonMain
为公共模块,该模块的代码与平台无关,是通过expected
关键字对一些api
的声明(声明的实现在platform module中
)。 -
androidMain
和iosMain
分别为Android
和iOS
这两个平台,通过actual
关键字在平台模块进行具体的实现。
关于KMM
工程的配置与使用方式,运行方式,编译过程原理还是请回顾一下之前的文章,在此不做赘述:
https://www.jianshu.com/p/e1ae5eaa894e
接下来我们看Compose-Multiplatform
是怎么基于KMM
工程进行的实现。
3.1.添加配置
在settings.gradle
文件中声明compose
插件:
plugins{
//...
val composeVersion = extra["compose.version"] as String
id("org.jetbrains.compose").version(composeVersion)
}
其中compose.version
在gradle.properties
进行了声明。需要注意的是目前Compose-Multiplatform
对Kotlin
的版本有要求,目前可以参考官方的具体配置:
https://github.com/JetBrains/compose-multiplatform-ios-android-template
#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/这个目录下寻找,如下图所示:
由于目前
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
封装一个简单的网络模块。
3.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.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
}
接下来我们看看是怎么发送的请求。
3.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
层的实现与数据的绑定是如何实现的。
3.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
目录下的图片,文本展示的是服务端返回的数据。下面我来说明一下图片的加载。
3.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端:
- iOS端:
好了到此为止,我们看到了一个简单的列表业务逻辑是怎样实现的了。由于Compose-Multiplatform
还未成熟,在业务实现上势必有很多内容需要自己造轮子。
4.绘制原理
由于网上已经有很多Compose
的相关绘制原理,下一章我们只是进行简单的源码解析,来说明它是如何生成UI
树并进行自绘的。
4.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
是怎么创建的呢?
4.1.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
。
4.1.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()
方法,这段代码我们就不再继续追踪下去了,它最终的作用就是对布局进行组合,创建父子依赖关系。
4.1.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.1.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
:
关于
skiko
的更多信息,还请查阅以下官方链接:https://github.com/JetBrains/skiko
好了到此为止,
Compose
的在Android
端的绘制原理我们就讲完了。对其他端绘制感兴趣的同学可自行查看相应的源码,细节有不同,但理念都是一致的:创建自己的Compose
树,并最终调用自绘引擎在Canvas
上进行绘制。
5.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
的竞争会非常大。
6.总结
Compose-Multiplatform
目前虽然还不成熟,但通过对其原理的分析,我们可以预见的是,结合KMM
,未来将成为跨平台的有力竞争者。特别对于Android
开发同学来说,可以把KMM
先用起来,结合Compose
去实现一些低耦合的业务,待未来Compose-iOS
发布稳定版后,可以愉快的进行双端开发,节约开发成本。