jetpack Compose绘制流程原理
Posted 小图包
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jetpack Compose绘制流程原理相关的知识,希望对你有一定的参考价值。
在 Flutter 中,Dart 对如何高效回收频繁创建与销毁的对象进行了专门优化,而 Compose 在 android 平台的实现方式本质上就是普通的 Kotlin/JVM 代码。如何设计 Compose 让其能够有可靠的性能表现,是一个挺有意思的问题。
组合在树上
在 2019 年,Leland Richardson 就在 Google Android Dev Summit 的 Understanding Compose 演讲中简要描述了 Compose 的实现原理与底层数据结构,并根据演讲内容在 Medium 上发布了两篇博客(Part 1 与 Part 2),所以这里仅进行简单的重述。
移动空间
Compose Runtime 采用了一种特殊的数据结构,称为 Slot Table。
Slot Table 与常用于文本编辑器的另一数据结构 Gap Buffer 相似,这是一个在连续空间中存储数据的类型,底层采用数组实现。区别于数组常用方式的是,它的剩余空间,称为 Gap,可根据需要移动到 Slot Table 中的任一区域,这让它在数据插入与删除时更高效。
简单来讲,一个 Slot Table 可能长这个样子,其中 _
表示一个未使用的数组元素,这些空间便构成了 Gap:
A B C D E _ _ _ _ _
复制代码
假设要在 C
后插入新的数据,则将 Gap 移动到 C 之后:
A B C _ _ _ _ _ D E
复制代码
之后便可以在 C
后直接插入新的数据:
A B C F G _ _ _ D E
复制代码
Slot Table 其本质又是一个线性的数据结构,因此可以采用树存储到数组的方式,将视图树存储在 Slot Table 中,加上 Slot Table 可移动插入点的特性,让视图树在变动之后无需重新创建整个数据结构,所以 Slot Table 其实是用数组实现了树的存储。
需要注意的是,Slot Table 相比普通数组实现了任意位置插入数据的功能,这是不能到能的跨越,但实际由于元素拷贝的原因,Gap 移动仍是一个需要尽量避免的低效操作。Google 选择这一数据结构的原因在于,他们预计界面更新大部分为数据变更,即只需要更新视图树节点数据,而视图树结构并不会经常变动。
之所以 Google 不采用树或链表等数据结构,猜测可能是数组这种内存连续的数据结构在访问效率上才能达到 Compose Runtime 的要求。
比如下面这样一个登录界面的视图树,这里通过缩进来展示层级。
VerticalLinearLayout
HorizontalLinearLayout
AccountHintTextView
AccountEditText
HorizontalLinearLayout
PasswordHintTextView
PasswordEditText
LoginButton
复制代码
在 Slot Table 中,树的子节点被称为 Node,非子节点被称为 Group。
底层数组本身并没有办法记录与树有关的信息,因此内部实际维护了其它的数据结构来保存一些节点信息,比如 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)
...
复制代码
Recompose
通过 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)
复制代码
Invalidate
同样,通过 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 的渲染。
作者:AoraMD
链接:https://juejin.cn/post/6966241418046078983
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
以上是关于jetpack Compose绘制流程原理的主要内容,如果未能解决你的问题,请参考以下文章