在 Flutter 中,Dart 對如何高效回收頻繁建立與銷燬的對象進行了專門優化,而 Compose 在 Android 平臺的實現方式本質上就是普通的 Kotlin/JVM 代碼。如何設計 Compose 讓其可以有可靠的性能表現,是一個挺有意思的問題。java
在 2019 年,Leland Richardson 就在 Google Android Dev Summit 的 Understanding Compose 演講中簡要描述了 Compose 的實現原理與底層數據結構,並根據演講內容在 Medium 上發佈了兩篇博客(Part 1 與 Part 2),因此這裏僅進行簡單的重述。android
Compose Runtime 採用了一種特殊的數據結構,稱爲 Slot Table。編程
Slot Table 與經常使用於文本編輯器的另外一數據結構 Gap Buffer 類似,這是一個在連續空間中存儲數據的類型,底層採用數組實現。區別於數組經常使用方式的是,它的剩餘空間,稱爲 Gap,可根據須要移動到 Slot Table 中的任一區域,這讓它在數據插入與刪除時更高效。bootstrap
簡單來說,一個 Slot Table 可能長這個樣子,其中 _
表示一個未使用的數組元素,這些空間便構成了 Gap:canvas
A B C D E _ _ _ _ _
複製代碼
假設要在 C
後插入新的數據,則將 Gap 移動到 C 以後:數組
A B C _ _ _ _ _ D E
複製代碼
以後即可以在 C
後直接插入新的數據:markdown
A B C F G _ _ _ D E
複製代碼
Slot Table 其本質又是一個線性的數據結構,所以能夠採用樹存儲到數組的方式,將視圖樹存儲在 Slot Table 中,加上 Slot Table 可移動插入點的特性,讓視圖樹在變更以後無需從新建立整個數據結構,因此 Slot Table 實際上是用數組實現了樹的存儲。數據結構
須要注意的是,Slot Table 相比普通數組實現了任意位置插入數據的功能,這是不能到能的跨越,但實際因爲元素拷貝的緣由,Gap 移動還是一個須要儘可能避免的低效操做。Google 選擇這一數據結構的緣由在於,他們預計界面更新大部分爲數據變動,即只須要更新視圖樹節點數據,而視圖樹結構並不會常常變更。併發
之因此 Google 不採用樹或鏈表等數據結構,猜想多是數組這種內存連續的數據結構在訪問效率上才能達到 Compose Runtime 的要求。app
好比下面這樣一個登陸界面的視圖樹,這裏經過縮進來展現層級。
VerticalLinearLayout
HorizontalLinearLayout
AccountHintTextView
AccountEditText
HorizontalLinearLayout
PasswordHintTextView
PasswordEditText
LoginButton
複製代碼
在 Slot Table 中,樹的子節點被稱爲 Node,非子節點被稱爲 Node。
底層數組自己並無辦法記錄與樹有關的信息,所以內部實際維護了其它的數據結構來保存一些節點信息,好比 Group 包含的 Node 個數,Node 直接所屬的 Group 等。
@Composable
是 Compose 系統的核心之一,被 @Composable
所註解的函數稱爲 可組合函數,下文也如此稱呼。
這並非一個普通的註解,添加該註解的函數會被真實地改變類型,改變方式與 suspend
相似,在編譯期進行處理,只不過 Compose 並不是語言特性,沒法採用語言關鍵字的形式進行實現。
以 Android Studio 生成的 Compose App 模版爲例,其中包含這樣一個可組合函數:
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
複製代碼
經過工具進行反編譯可獲得實際代碼:
public static final void Greeting(String name, Composer $composer, int $changed) {
Intrinsics.checkNotNullParameter(name, HintConstants.AUTOFILL_HINT_NAME);
Composer $composer2 = $composer.startRestartGroup(105642380);
ComposerKt.sourceInformation($composer2, "C(Greeting)51@1521L27:MainActivity.kt#xfcxsz");
int $dirty = $changed;
if (($changed & 14) == 0) {
$dirty |= $composer2.changed(name) ? 4 : 2;
}
if ((($dirty & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
TextKt.m866Text6FffQQw(LiveLiterals$MainActivityKt.INSTANCE.m4017String$0$str$arg0$callText$funGreeting() + name + LiveLiterals$MainActivityKt.INSTANCE.m4018String$2$str$arg0$callText$funGreeting(), null, Color.m1136constructorimpl(ULong.m2785constructorimpl(0)), TextUnit.m2554constructorimpl(0), null, null, null, TextUnit.m2554constructorimpl(0), null, null, TextUnit.m2554constructorimpl(0), null, false, 0, null, null, $composer2, 0, 0, 65534);
} else {
$composer2.skipToGroupEnd();
}
ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
if (endRestartGroup != null) {
endRestartGroup.updateScope(new MainActivityKt$Greeting$1(name, $changed));
}
}
public final class MainActivityKt$Greeting$1 extends Lambda implements Function2<Composer, Integer, Unit> {
final int $$changed;
final String $name;
MainActivityKt$Greeting$1(String str, int i) {
super(2);
this.$name = str;
this.$$changed = i;
}
@Override
public Unit invoke(Composer composer, Integer num) {
invoke(composer, num.intValue());
return Unit.INSTANCE;
}
public final void invoke(Composer composer, int i) {
MainActivityKt.Greeting(this.$name, composer, this.$$changed | 1);
}
}
複製代碼
轉換爲等效且可讀性強的 Kotlin 僞代碼以下:
fun Greeting(name: String, parentComposer: Composer, changed: Int) {
val composer = parentComposer.startRestartGroup(GROUP_HASH)
val dirty = calculateState(changed)
if (stateHasChanged(dirty) || composer.skipping) {
Text("Hello $name", composer = composer, changed = ...)
} else {
composer.skipToGroupEnd()
}
composer.endRestartGroup()?.updateScope {
Greeting(name, changed)
}
}
複製代碼
可見被 @Composable
註解後,函數增添了額外的參數,其中的 Composer
類型參數做爲運行環境貫穿在整個可組合函數調用鏈中,因此可組合函數沒法在普通函數中調用,由於不包含相應的環境。由於環境傳入的關係,調用位置不一樣的兩個相同的可組合函數調用,其實現效果並不相同。
可組合函數實現的起始與結尾經過 Composer.startRestartGroup()
與 Composer.endRestartGroup()
在 Slot Table 中建立 Group,而可組合函數內部所調用的可組合函數在兩個調用之間建立新的 Group,從而在 Slot Table 內部完成視圖樹的構建。
Composer 根據當前是否正在修改視圖樹而肯定這些調用的實現類型。
在視圖樹構建完成後,若數據更新致使部分視圖須要刷新,此時非刷新部分對應可組合函數的調用就再也不是進行視圖樹的構建,而是視圖樹的訪問,正如代碼中的
Composer.skipToGroupEnd()
調用,表示在訪問過程當中直接跳到當前 Group 的末端。Composer 對 Slot Table 的操做是讀寫分離的,只有寫操做完成後纔將全部寫入內容更新到 Slot Table 中。
除此以外,可組合函數還將經過傳入標記參數的位運算判斷內部的可組合函數執行或跳過,這能夠避免訪問無需更新的節點,提高執行效率。
前面的文字與代碼提到兩點,一是可組合函數可經過傳入標記參數的位運算判斷內部的可組合函數執行或跳過,二是可組合函數內 Composer.endRestartGroup()
返回了一個 ScopeUpdateScope
類型對象,其 ScopeUpdateScope.updateScope()
函數被調用,傳入了調用當前可組合函數的 Lambda。這些內容代表,Compose Runtime 可根據當前環境肯定可組合函數的調用範圍。
當視圖數據發生變更時,Compose Runtime 會根據數據影響範圍肯定須要從新執行的可組合函數,這一步驟被稱爲重組,前面代碼中執行 ScopeUpdateScope.updateScope()
的做用即是註冊重組須要執行的可組合函數。
updateScope
這個函數名稱具備迷惑性,傳入的 Lambda 是一個回調,並不會當即執行,更利於理解的名稱是onScopeUpdate
或setUpdateScope
。
爲了說明 Compose 的重組機制,就須要聊一聊 Compose 管理數據的結構,State。
由於 Compose 是一個聲明式(Declarative)框架,State 採用觀察者模式來實現界面隨數據自動更新,首先用一個例子來講明 State 的使用方式。
@Composable fun Content() {
val state by remember { mutableStateOf(1) }
Column {
Button(onClick = { state++ }) {
Text(text = "click to change state")
}
Text("state value: $state")
}
}
複製代碼
remember()
是一個可組合函數,相似於lazy
,其做用是在可組合函數調用中記憶對象。可組合函數在調用鏈位置不變的狀況下,調用remember()
便可獲取上次調用時記憶的內容。這與可組合函數的特性相關,可理解爲
remember()
在樹的當前位置記錄數據,也意味着同一個可組合函數在不一樣調用位置被調用,內部的remember()
獲取內容並不相同,這是由於調用位置不一樣,對應樹上的節點也不一樣。
因觀察者模式的設計,當 state
寫入數據時會觸發重組,所以能夠猜想觸發重組的實如今 State 寫入的實現中。
mutableStateOf()
最終會返回 ParcelableSnapshotMutableState
的對象,相關代碼位於其超類 SnapshotMutableStateImpl
。
/** * A single value holder whose reads and writes are observed by Compose. * * Additionally, writes to it are transacted as part of the [Snapshot] system. * * @param value the wrapped value * @param policy a policy to control how changes are handled in a mutable snapshot. * * @see mutableStateOf * @see SnapshotMutationPolicy */
internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
@Suppress("UNCHECKED_CAST")
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}
private var next: StateStateRecord<T> = StateStateRecord(value)
...
}
複製代碼
StateStateRecord.overwritable()
最終會調用 notifyWrite()
實現觀察者的通知。
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}
複製代碼
下一步即是肯定回調,經過 Debugger 能夠快速定位到 writeObserver
在 GlobalSnapshotManager.ensureStarted()
中被註冊:
/** * Platform-specific mechanism for starting a monitor of global snapshot state writes * in order to schedule the periodic dispatch of snapshot apply notifications. * This process should remain platform-specific; it is tied to the threading and update model of * a particular platform and framework target. * * Composition bootstrapping mechanisms for a particular platform/framework should call * [ensureStarted] during setup to initialize periodic global snapshot notifications. * For Android, these notifications are always sent on [AndroidUiDispatcher.Main]. Other platforms * may establish different policies for these notifications. */
internal object GlobalSnapshotManager {
private val started = AtomicBoolean(false)
fun ensureStarted() {
if (started.compareAndSet(false, true)) {
val channel = Channel<Unit>(Channel.CONFLATED)
CoroutineScope(AndroidUiDispatcher.Main).launch {
channel.consumeEach {
Snapshot.sendApplyNotifications()
}
}
Snapshot.registerGlobalWriteObserver {
channel.offer(Unit)
}
}
}
}
複製代碼
當向 channel
推送對象,在 主線程 觸發 Snapshot.sendApplyNotifications()
調用後,調用鏈會到達 advanceGlobalSnapshot()
,這裏實現了數據更新監聽器的回調。
private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
...
// If the previous global snapshot had any modified states then notify the registered apply
// observers.
val modified = previousGlobalSnapshot.modified
if (modified != null) {
val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
observers.fastForEach { observer ->
observer(modified, previousGlobalSnapshot)
}
}
...
}
複製代碼
經過 Debugger 進行調試與篩選,能夠發現 observers
包含了兩個回調,其中一個位於 Recomposer.recompositionRunner()
。
/** * The scheduler for performing recomposition and applying updates to one or more [Composition]s. */
// RedundantVisibilityModifier suppressed because metalava picks up internal function overrides
// if 'internal' is not explicitly specified - b/171342041
// NotCloseable suppressed because this is Kotlin-only common code; [Auto]Closeable not available.
@Suppress("RedundantVisibilityModifier", "NotCloseable")
@OptIn(InternalComposeApi::class)
class Recomposer(
effectCoroutineContext: CoroutineContext
) : CompositionContext() {
...
@OptIn(ExperimentalComposeApi::class)
private suspend fun recompositionRunner( block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit ) {
withContext(broadcastFrameClock) {
...
// Observe snapshot changes and propagate them to known composers only from
// this caller's dispatcher, never working with the same composer in parallel.
// unregisterApplyObserver is called as part of the big finally below
val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
synchronized(stateLock) {
if (_state.value >= State.Idle) {
snapshotInvalidations += changed
deriveStateLocked()
} else null
}?.resume(Unit)
}
...
}
}
...
}
複製代碼
觸發回調將增長 snapshotInvalidations
中的元素,後續說明。
當 AbstractComposeView.onAttachToWindow()
被調用時,Recomposer.runRecomposeAndApplyChanges()
被調用,並啓用循環等待重組事件。
...
class Recomposer(
effectCoroutineContext: CoroutineContext
) : CompositionContext() {
...
/** * Await the invalidation of any associated [Composer]s, recompose them, and apply their * changes to their associated [Composition]s if recomposition is successful. * * While [runRecomposeAndApplyChanges] is running, [awaitIdle] will suspend until there are no * more invalid composers awaiting recomposition. * * This method will not return unless the [Recomposer] is [close]d and all effects in managed * compositions complete. * Unhandled failure exceptions from child coroutines will be thrown by this method. */
suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->
...
while (shouldKeepRecomposing) {
...
// Don't await a new frame if we don't have frame-scoped work
if (
synchronized(stateLock) {
if (!hasFrameWorkLocked) {
recordComposerModificationsLocked()
!hasFrameWorkLocked
} else false
}
) continue
// Align work with the next frame to coalesce changes.
// Note: it is possible to resume from the above with no recompositions pending,
// instead someone might be awaiting our frame clock dispatch below.
// We use the cached frame clock from above not just so that we don't locate it
// each time, but because we've installed the broadcastFrameClock as the scope
// clock above for user code to locate.
parentFrameClock.withFrameNanos { frameTime ->
...
trace("Recomposer:recompose") {
...
val modifiedValues = IdentityArraySet<Any>()
try {
toRecompose.fastForEach { composer ->
performRecompose(composer, modifiedValues)?.let {
toApply += it
}
}
if (toApply.isNotEmpty()) changeCount++
} finally {
toRecompose.clear()
}
...
}
}
}
}
...
}
複製代碼
當重組事件產生時,recordComposerModificationLocked()
將觸發,compositionInvalidations
中的內容被更新,而該對象的更新依賴於 snapshotInvalidations
,最終致使 hasFrameWorkLocked
變動爲 true
。
AndroidUiFrameClock.withFrameNanos()
將被調用,這將向 Choreographer 註冊垂直同步信號回調,Recomposer.performRecompose()
最終將觸發從 ScopeUpdateScope.updateScope()
註冊 Lambda 的調用。
class AndroidUiFrameClock(
val choreographer: Choreographer
) : androidx.compose.runtime.MonotonicFrameClock {
override suspend fun <R> withFrameNanos( onFrame: (Long) -> R ): R {
val uiDispatcher = coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
return suspendCancellableCoroutine { co ->
// Important: this callback won't throw, and AndroidUiDispatcher counts on it.
val callback = Choreographer.FrameCallback { frameTimeNanos ->
co.resumeWith(runCatching { onFrame(frameTimeNanos) })
}
// If we're on an AndroidUiDispatcher then we post callback to happen *after*
// the greedy trampoline dispatch is complete.
// This means that onFrame will run on the current choreographer frame if one is
// already in progress, but withFrameNanos will *not* resume until the frame
// is complete. This prevents multiple calls to withFrameNanos immediately dispatching
// on the same frame.
if (uiDispatcher != null && uiDispatcher.choreographer == choreographer) {
uiDispatcher.postFrameCallback(callback)
co.invokeOnCancellation { uiDispatcher.removeFrameCallback(callback) }
} else {
choreographer.postFrameCallback(callback)
co.invokeOnCancellation { choreographer.removeFrameCallback(callback) }
}
}
}
}
複製代碼
一樣,經過 Debugger 進行調試與篩選,能夠定位到另外一個回調是 SnapshotStateObserver.applyObserver
。
class SnapshotStateObserver(private val onChangedExecutor: (callback: () -> Unit) -> Unit) {
private val applyObserver: (Set<Any>, Snapshot) -> Unit = { applied, _ ->
var hasValues = false
...
if (hasValues) {
onChangedExecutor {
callOnChanged()
}
}
}
...
}
複製代碼
由 SnapshotStateObserver.callOnChanged()
可定位到回調 LayoutNodeWrapper.Companion.onCommitAffectingLayer
。
調用鏈:
SnapshotStateObserver.callOnChanged()
-->
SnapshotStateObserver.ApplyMap.callOnChanged()
-->
SnapshotStateObserver.ApplyMap.onChanged.invoke()
- implementation ->
LayoutNodeWrapper.Companion.onCommitAffectingLayer.invoke()
/** * Measurable and Placeable type that has a position. */
internal abstract class LayoutNodeWrapper(
internal val layoutNode: LayoutNode
) : Placeable(), Measurable, LayoutCoordinates, OwnerScope, (Canvas) -> Unit {
...
internal companion object {
...
private val onCommitAffectingLayer: (LayoutNodeWrapper) -> Unit = { wrapper ->
wrapper.layer?.invalidate()
}
...
}
}
複製代碼
最終在 RenderNodeLayer.invalidate()
中觸發頂層 AndroidComposeView
重繪,實現視圖更新。
/** * RenderNode implementation of OwnedLayer. */
@RequiresApi(Build.VERSION_CODES.M)
internal class RenderNodeLayer(
val ownerView: AndroidComposeView,
val drawBlock: (Canvas) -> Unit,
val invalidateParentLayer: () -> Unit
) : OwnedLayer {
...
override fun invalidate() {
if (!isDirty && !isDestroyed) {
ownerView.invalidate()
ownerView.dirtyLayers += this
isDirty = true
}
}
...
}
複製代碼
Compose 是如何繪製的?
可組合函數的執行完成了視圖樹的構建,但並無進行視圖樹的渲染,二者的實現是分離的,系統會將重組函數運行完成後生成的視圖樹交由渲染模塊運行。
可組合函數不必定僅在主線程運行,甚至可能在多個線程中併發運行,但這不意味着能夠在可組合函數中直接進行耗時操做,由於可組合函數可能會被頻繁調用,甚至一幀一次。
重組是樂觀的操做,當數據在重組完成前更新,本次重組可能會被取消,所以可重組函數在設計上應該冪等且沒有附帶效應,相關內容可瞭解函數式編程。
在 Google 關於 Compose 與 View 進行兼容的文檔中提到了 ComposeView
與 AbstractComposeView
,但若是查看代碼會發現,這與咱們前文說起的 AndroidComposeView
並無繼承關係。
先經過 官方示例 看一看如何將可組合函數轉換爲 View:
@Composable
fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) {
Button(
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary
),
onClick = onClick,
modifier = modifier,
) {
Text(text)
}
}
class CallToActionViewButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {
var text by mutableStateOf<String>("")
var onClick by mutableStateOf<() -> Unit>({})
@Composable
override fun Content() {
YourAppTheme {
CallToActionButton(text, onClick)
}
}
}
複製代碼
尋找 AbstractComposeView.Content()
的調用方,最終會定位到 ViewGroup.setContent()
擴展函數,
/** * Composes the given composable into the given view. * * The new composition can be logically "linked" to an existing one, by providing a * [parent]. This will ensure that invalidations and CompositionLocals will flow through * the two compositions as if they were not separate. * * Note that this [ViewGroup] should have an unique id for the saved instance state mechanism to * be able to save and restore the values used within the composition. See [View.setId]. * * @param parent The [Recomposer] or parent composition reference. * @param content Composable that will be the content of the view. */
internal fun ViewGroup.setContent( parent: CompositionContext, content: @Composable () -> Unit ): Composition {
GlobalSnapshotManager.ensureStarted()
val composeView =
if (childCount > 0) {
getChildAt(0) as? AndroidComposeView
} else {
removeAllViews(); null
} ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
return doSetContent(composeView, parent, content)
}
複製代碼
可見,View Group 將只保留一個 AndroidComposeView
視圖,同時 doSetContent()
函數將組合函數設置到 AndroidComposeView
中。
可組合函數的調用最終會構築出包含數據與視圖信息的樹,各類視圖類型可組合函數最終都將調用可組合函數 ReusableComposeNode()
,並建立一個 LayoutNode
對象做爲子節點記錄到樹中。
LayoutNode
的存在相似於 Flutter 中的Element
,它們是視圖樹結構的組成部分,而且是相對穩定的。
Compose 在 Android 上的實現最終依賴於 AndroidComposeView
,且這是一個 ViewGroup
,那麼按原生視圖渲染的角度,看一下 AndroidComposeView
對 onDraw()
與 dispatchDraw()
的實現,便可看到 Compose 渲染的原理。
@SuppressLint("ViewConstructor", "VisibleForTests")
@OptIn(ExperimentalComposeUiApi::class)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
internal class AndroidComposeView(context: Context) :
ViewGroup(context), Owner, ViewRootForTest, PositionCalculator {
...
override fun onDraw(canvas: android.graphics.Canvas) {
}
...
override fun dispatchDraw(canvas: android.graphics.Canvas) {
...
measureAndLayout()
// we don't have to observe here because the root has a layer modifier
// that will observe all children. The AndroidComposeView has only the
// root, so it doesn't have to invalidate itself based on model changes.
canvasHolder.drawInto(canvas) { root.draw(this) }
...
}
...
}
複製代碼
CanvasHolder.drawInto()
將 android.graphics.Canvas
轉化爲 androidx.compose.ui.graphics.Canvas
實現傳遞至頂層 LayoutNode
對象 root
的 LayoutNode.draw()
函數中,實現視圖樹的渲染。
因爲各類視圖類型可組合函數的設計不一樣,這裏僅以繪製 Bitmap 的可組合函數 Image()
做爲例子,其實現以下。
/** * A composable that lays out and draws a given [ImageBitmap]. This will attempt to * size the composable according to the [ImageBitmap]'s given width and height. However, an * optional [Modifier] parameter can be provided to adjust sizing or draw additional content (ex. * background). Any unspecified dimension will leverage the [ImageBitmap]'s size as a minimum * constraint. * * The following sample shows basic usage of an Image composable to position and draw an * [ImageBitmap] on screen * @sample androidx.compose.foundation.samples.ImageSample * * For use cases that require drawing a rectangular subset of the [ImageBitmap] consumers can use * overload that consumes a [Painter] parameter shown in this sample * @sample androidx.compose.foundation.samples.BitmapPainterSubsectionSample * * @param bitmap The [ImageBitmap] to draw * @param contentDescription text used by accessibility services to describe what this image * represents. This should always be provided unless this image is used for decorative purposes, * and does not represent a meaningful action that a user can take. This text should be * localized, such as by using [androidx.compose.ui.res.stringResource] or similar * @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex. * background) * @param alignment Optional alignment parameter used to place the [ImageBitmap] in the given * bounds defined by the width and height * @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used * if the bounds are a different size from the intrinsic size of the [ImageBitmap] * @param alpha Optional opacity to be applied to the [ImageBitmap] when it is rendered onscreen * @param colorFilter Optional ColorFilter to apply for the [ImageBitmap] when it is rendered * onscreen */
@Composable
fun Image( bitmap: ImageBitmap, contentDescription: String?, modifier: Modifier = Modifier, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null ) {
val bitmapPainter = remember(bitmap) { BitmapPainter(bitmap) }
Image(
painter = bitmapPainter,
contentDescription = contentDescription,
modifier = modifier,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter
)
}
/** * Creates a composable that lays out and draws a given [Painter]. This will attempt to size * the composable according to the [Painter]'s intrinsic size. However, an optional [Modifier] * parameter can be provided to adjust sizing or draw additional content (ex. background) * * **NOTE** a Painter might not have an intrinsic size, so if no LayoutModifier is provided * as part of the Modifier chain this might size the [Image] composable to a width and height * of zero and will not draw any content. This can happen for Painter implementations that * always attempt to fill the bounds like [ColorPainter] * * @sample androidx.compose.foundation.samples.BitmapPainterSample * * @param painter to draw * @param contentDescription text used by accessibility services to describe what this image * represents. This should always be provided unless this image is used for decorative purposes, * and does not represent a meaningful action that a user can take. This text should be * localized, such as by using [androidx.compose.ui.res.stringResource] or similar * @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex. * background) * @param alignment Optional alignment parameter used to place the [Painter] in the given * bounds defined by the width and height. * @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used * if the bounds are a different size from the intrinsic size of the [Painter] * @param alpha Optional opacity to be applied to the [Painter] when it is rendered onscreen * the default renders the [Painter] completely opaque * @param colorFilter Optional colorFilter to apply for the [Painter] when it is rendered onscreen */
@Composable
fun Image( painter: Painter, contentDescription: String?, modifier: Modifier = Modifier, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null ) {
val semantics = if (contentDescription != null) {
Modifier.semantics {
this.contentDescription = contentDescription
this.role = Role.Image
}
} else {
Modifier
}
// Explicitly use a simple Layout implementation here as Spacer squashes any non fixed
// constraint with zero
Layout(
{},
modifier.then(semantics).clipToBounds().paint(
painter,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter
)
) { _, constraints ->
layout(constraints.minWidth, constraints.minHeight) {}
}
}
複製代碼
這裏構建了一個包含 BitmapPainter
的 Modifier
傳入 Layout()
中,而這一 Modifier
對象最終會被設置到對應的 LayoutNode
對象中。
而由前文說起的,當 LayoutNode.draw()
被調用時,其 outLayoutNodeWrapper
的 LayoutNodeWrapper.draw()
會被調用。
/** * An element in the layout hierarchy, built with compose UI. */
internal class LayoutNode : Measurable, Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode {
...
internal fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)
...
}
/** * Measurable and Placeable type that has a position. */
internal abstract class LayoutNodeWrapper(
internal val layoutNode: LayoutNode
) : Placeable(), Measurable, LayoutCoordinates, OwnerScope, (Canvas) -> Unit {
...
/** * Draws the content of the LayoutNode */
fun draw(canvas: Canvas) {
val layer = layer
if (layer != null) {
layer.drawLayer(canvas)
} else {
val x = position.x.toFloat()
val y = position.y.toFloat()
canvas.translate(x, y)
performDraw(canvas)
canvas.translate(-x, -y)
}
}
...
}
複製代碼
通過多層委託以後,LayoutNodeWrapper.draw()
將調用 InnerPlaceholder.performDraw()
實現對子視圖的渲染分發。
internal class InnerPlaceable(
layoutNode: LayoutNode
) : LayoutNodeWrapper(layoutNode), Density by layoutNode.measureScope {
...
override fun performDraw(canvas: Canvas) {
val owner = layoutNode.requireOwner()
layoutNode.zSortedChildren.forEach { child ->
if (child.isPlaced) {
child.draw(canvas)
}
}
if (owner.showLayoutBounds) {
drawBorder(canvas, innerBoundsPaint)
}
}
...
}
複製代碼
最終到達渲染 Bitmap 的 Image 視圖節點時,LayoutNodeWrapper
的實現是 ModifiedDrawNode
。
internal class ModifiedDrawNode(
wrapped: LayoutNodeWrapper,
drawModifier: DrawModifier
) : DelegatingLayoutNodeWrapper<DrawModifier>(wrapped, drawModifier), OwnerScope {
...
// This is not thread safe
override fun performDraw(canvas: Canvas) {
...
val drawScope = layoutNode.mDrawScope
drawScope.draw(canvas, size, wrapped) {
with(drawScope) {
with(modifier) {
draw()
}
}
}
}
...
}
複製代碼
這裏調用的是 PainterModifier
的 DrawScope.draw()
實現。
這是採用 Kotlin 擴展函數實現的一種很是奇特的寫法,擴展函數能夠做爲接口函數,由接口實現類實現,調用時則必須經過
with()
、apply()
、run()
等設定this
範圍的函數構建環境。可是這種寫法在多層
this
嵌套時,可讀性上還需進行探討,正如上方對DrawScope.draw()
的調用。若是沒法理解上方的代碼包含了什麼值得吐槽的東西,能夠看看下面的例子 🤔。class Api { fun String.show() { println(this) } } fun main() { "Hello world!".apply { Api().apply { show() } } } 複製代碼
接着調用 BitmapPainter
的 DrawScope.onDraw()
實現。
/** * [Painter] implementation used to draw an [ImageBitmap] into the provided canvas * This implementation can handle applying alpha and [ColorFilter] to it's drawn result * * @param image The [ImageBitmap] to draw * @param srcOffset Optional offset relative to [image] used to draw a subsection of the * [ImageBitmap]. By default this uses the origin of [image] * @param srcSize Optional dimensions representing size of the subsection of [image] to draw * Both the offset and size must have the following requirements: * * 1) Left and top bounds must be greater than or equal to zero * 2) Source size must be greater than zero * 3) Source size must be less than or equal to the dimensions of [image] */
class BitmapPainter(
private val image: ImageBitmap,
private val srcOffset: IntOffset = IntOffset.Zero,
private val srcSize: IntSize = IntSize(image.width, image.height)
) : 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
)
}
...
}
複製代碼
在 DrawScope.onDraw()
中將調用 androidx.compose.ui.graphics.Canvas
進行繪製,而這最終將委託給其內部持有的 android.graphics.Canvas
對象繪製,最終實現 Compose 的渲染。