Dart optimizes for efficient collection of frequently created and destroyed objects in Flutter, while Swarm’s Implementation on Android is essentially ordinary Kotlin/JVM code. It is an interesting question how Compose can be designed to provide reliable performance.

Together on a tree

In his Understanding Compose talk at the Google Android Dev Summit in 2019, Leland Richardson briefly described the implementation of Compose and its underlying data structure. According to the content of the speech, I published two blog posts on Medium (Part 1 and Part 2), so I will only make a brief restatement here.

The mobile space

The Compose Runtime employs a special data structure called Slot Table.

Slot tables are similar to the Gap Buffer, another data structure commonly used in text editors. This is a type that stores data in contiguous space. The underlying implementation is an array. Unlike arrays, its remaining space, called Gap, can be moved to any area of the Slot Table on demand, making it more efficient at inserting and deleting data.

To put it simply, a Slot Table might look like this, where _ represents an unused array element and these Spaces form a Gap:

A B C D E _ _ _ _ _
Copy the code

If we want to insert new data after C, move the Gap after C:

A B C _ _ _ _ _ D E
Copy the code

Insert new data directly after C:

A B C F G _ _ _ D E
Copy the code

Slot Table is a linear data structure in nature, so you can store the view tree in the Slot Table in the way of tree to array, and the Slot Table can move the insertion point, so that the view tree does not need to recreate the whole data structure after changing. So Slot tables use arrays to store trees.

It is important to note that Slot tables can insert data anywhere compared to normal arrays, which is not an impossible leap. However, Gap movement is still an inefficient operation to avoid due to element copying. Google chose this data structure because they expected interface updates to be mostly data changes, meaning that only the view tree node data would need to be updated, and the view tree structure would not change very often.

The reason Why Google doesn’t use a tree or a linked list is that arrays, which are contiguous in memory, are the ones that Compose Runtime requires.

For example, here is a view tree of the login screen, where the hierarchy is shown in indentation.

VerticalLinearLayout
	HorizontalLinearLayout
		AccountHintTextView
		AccountEditText
	HorizontalLinearLayout
		PasswordHintTextView
		PasswordEditText
	LoginButton
Copy the code

In Slot tables, children of the tree are called nodes, and non-children are called nodes.

The underlying array itself has no way to record tree-related information, so other data structures are actually maintained internally to store some Node information, such as the number of nodes contained in the Group, the Group to which nodes directly belong, etc.

The environment

Composable is one of the core components of the Compose system, and the functions annotated by @Composable are called Composable functions, as they are hereafter.

This is not a normal annotation. The function that adds the annotation is actually typed differently from suspend and processed at compile time, except that Compose is not a language feature and cannot be implemented as a language keyword.

Take the Compose App template generated by Android Studio for example, which contains this composable function:

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")}Copy the code

Decompilating with a tool yields the actual code:

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$GreetingThe $1extends 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); }}Copy the code

Convert to the equivalent and readable Kotlin pseudocode as follows:

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) } }Copy the code

As can be seen from the @composable annotation, additional parameters are added to the function, where the Composer type parameter is run in the chain of Composable function calls, so Composable functions cannot be called in normal functions because there is no context. Because of the incoming environment, two identical composable function calls that are called in different locations are not implemented the same way.

Composable functions implemented by the Composer, the beginning and the end of startRestartGroup () and Composer. EndRestartGroup () in the Slot in the Table to create a Group, The composable function called inside the Slot Table creates a new Group between the two calls to complete the view tree construction inside the Slot Table.

Composer determines the implementation type of these calls based on whether the view tree is currently being modified.

After the view tree is built, if some views need to be refreshed due to data update, the call of the composable function corresponding to the non-refreshed part is no longer to build the view tree, but to access the view tree, just like the call of Composer. SkipToGroupEnd () in the code. Indicates jumping directly to the end of the current Group during access.

The Composer’s operations on Slot tables are read/write separated. After the write operation is complete, all the written content is updated to the Slot Table.

In addition, the composable function will also determine whether the internal composable function is executed or skipped by passing in the bit operation of the marker parameter, which can avoid accessing nodes that need no update and improve the execution efficiency.

restructuring

In addition, Composer. EndRestartGroup () returns an object of type ScopeUpdateScope. Its ScopeUpdateScope. UpdateScope () function is called, was introduced into the current composable function called Lambda. The Compose Runtime defines the range of composable functions based on the current environment.

When changes occurred in the view data, Compose the Runtime will determine the need to perform according to the scope of the data of composable functions, this process is called the reorganization, the previous code that executes ScopeUpdateScope. UpdateScope () function is registered restructuring can combine functions need to be performed.

The name of the updateScope function is deceptively confusing. The Lambda passed in is a callback and is not executed immediately. Better to think of names like onScopeUpdate or setUpdateScope.

To illustrate the Compose reorganization mechanism, we need to talk about the Compose management data structure, State.

Because Compose is a Declarative framework, State adopts the observer mode to implement interface automatic update with data. First, an example is given to illustrate how State is used.

@Composable fun Content(a) {
    val state by remember { mutableStateOf(1) }
    Column {
        Button(onClick = { state++ }) {
            Text(text = "click to change state")
        }
        Text("state value: $state")}}Copy the code

Remember () is a composable function, similar to lazy, that remembers objects in a composable function call. Combinable functions call remember() to retrieve the contents of the last call when the position of the call chain remains unchanged.

This is related to the characteristics of composable functions, which can be understood as remember() records data at the current position of the tree. It also means that the same composable function is called at different calling positions, and the internal content of remember() is different, because different calling positions lead to different nodes in the tree.

Because of the design of the observer pattern, recombination is triggered when state writes data, so you can guess that the implementation that triggers recombination is in the implementation of state writes.

MutableStateOf () will return ParcelableSnapshotMutableState object, the code is located in its superclass 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) 
  
    ...
}
Copy the code

StateStateRecord. Overwritable () will call notifyWrite notice () observer.

@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject){ snapshot.writeObserver? .invoke(state) }Copy the code

The next step is to determine the callback, through the Debugger can quickly locate to writeObserver GlobalSnapshotManager. EnsureStarted registered () :

/** * 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(a) {
        if (started.compareAndSet(false.true)) {
            val channel = Channel<Unit>(Channel.CONFLATED)
            CoroutineScope(AndroidUiDispatcher.Main).launch {
                channel.consumeEach {
                    Snapshot.sendApplyNotifications()
                }
            }
            Snapshot.registerGlobalWriteObserver {
                channel.offer(Unit)}}}}Copy the code

When the channel to push objects, in the main thread to trigger the Snapshot. SendApplyNotifications () call, the call chain reaches advanceGlobalSnapshot (), which implements the data update listener callback.

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)
        }
    }

    ...
}
Copy the code

Recompose

Through the Debugger to debug and selection, can be found that observers contains two callback, one is located in 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)}... }}... }Copy the code

Triggering the callback adds elements in snapshotInvalidations, as explained later.

When AbstractComposeView. OnAttachToWindow () is invoked, Recomposer. RunRecomposeAndApplyChanges () is called, and enable the circular wait restructuring events.

.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(a) = 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() } ... }}}}... }Copy the code

When restructuring events produce recordComposerModificationLocked () will trigger, the contents of the compositionInvalidations is updated, the renewal of the object and dependent on snapshotInvalidations, This causes hasFrameWorkLocked to change to True.

AndroidUiFrameClock. WithFrameNanos () is called, it will registered vertical synchronous signal with the Choreographer callback, Recomposer. PerformRecompose () will eventually trigger from ScopeUpdateScope. UpdateScope registered Lambda () call.

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) } } } }}Copy the code

Invalidate

Also, through the Debugger to debug and selection, can locate to another callback is 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()
            }
        }
    }
  
    ... 
}
Copy the code

By SnapshotStateObserver. CallOnChanged () can be positioned to callback LayoutNodeWrapper.Com panion. OnCommitAffectingLayer.

Call chain:

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()
        }
        ...
    }
}
Copy the code

Finally in RenderNodeLayer. Invalidate () to trigger the top AndroidComposeView re-paint, realize the view update.

/** * 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(a) {
        if(! isDirty && ! isDestroyed) { ownerView.invalidate() ownerView.dirtyLayers +=this
            isDirty = true}}... }Copy the code

Draw an empty

How is Compose drawn?

The execution of the composable function completes the construction of the view tree, but does not render the view tree. The realization of the two is separated, and the system will deliver the view tree generated after the completion of the reorganization function to the rendering module for running.

Composable functions do not have to run only on the main thread and may even run concurrently in multiple threads, but this does not mean that time-consuming operations can be performed directly in composable functions, since composable functions may be called frequently, even once a frame.

Recombination is an optimistic operation. If the data is updated before the recombination is complete, the recombination may be cancelled. Therefore, reconfigurable functions should be idempotent in design and have no side effects.

structure

ComposeView and AbstractComposeView are mentioned in Google’s documentation for Compose compatibility with Views, but if you look at the code, This is not a successor to the AndroidComposeView we mentioned earlier.

Let’s take a look at the official example of how to convert a composable function to a 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(a) {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}
Copy the code

Looking for AbstractComposeView. The Content () the caller, ultimately positioning to ViewGroup. The setContent () extension function,

/**
 * 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)
}
Copy the code

As you can see, the View Group will retain only one AndroidComposeView View, while the doSetContent() function sets the composition function to AndroidComposeView.

Apply colours to a drawing

Calls to composable functions eventually build a tree of data and view information. Each view composable function eventually calls the composable function ReusableComposeNode() and creates a LayoutNode object to record into the tree as a child node.

Layoutnodes exist just like elements in a Flutter. They are part of the view tree structure and are relatively stable.

Compose’s implementation on Android ultimately relies on AndroidComposeView, which is a ViewGroup, so from the perspective of native view rendering, Take a look at AndroidComposeView’s implementation of onDraw() and dispatchDraw() to see how Compose renders.

@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)}... }... }Copy the code

CanvasHolder. DrawInto () will be an android. Graphics. Canvas into androidx.com pose. UI. Graphics. The Canvas to the top floor LayoutNode root object The layoutnode.draw () function is used to render the view tree.

Since the design of composable functions of various view types is different, the composable function Image(), which draws a Bitmap, is only used as an example here, and its implementation is as follows.

/**
 * 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) {}
    }
}
Copy the code

This builds a Modifier passed into Layout() that contains BitmapPainter, which is eventually set to the corresponding LayoutNode object.

As mentioned earlier, when layoutNode.draw () is called, layoutNodeWrapper.draw () of its outLayoutNodeWrapper will be called.

/** * 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)
        }
    }
    
    ...
}
Copy the code

After multilayer entrust LayoutNodeWrapper. The draw () will call InnerPlaceholder. PerformDraw pair () implementation view rendering distribution.

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)
        }
    }
  
    ...
}
Copy the code

When the Image view node that renders the Bitmap is finally reached, the implementation of LayoutNodeWrapper is 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()
                }
            }
        }
    }
  
    ...
}
Copy the code

The DrawScope. Draw () implementation of PainterModifier is called here.

This is a very fancy way of writing it using Kotlin extension functions, which can be used as interface functions, implemented by the interface implementation class, and must be called with(), apply(), run(), and other functions that set the this scope to build the environment.

However, readability needs to be explored when nesting multiple layers of this, as in the drawscope.draw () call above. If you don’t understand what the code above contains that is worth teasing, take a look at the following example 🤔.

class Api {
    fun String.show(a) {
        println(this)}}fun main(a) {
    "Hello world!".apply {
        Api().apply {
            show()
        }
    }
}
Copy the code

The drawscope.ondraw () implementation of BitmapPainter is then called.

/**
 * [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
 * @paramsrcOffset 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(a) {
        drawImage(
            image,
            srcOffset,
            srcSize,
            dstSize = IntSize(
                this@onDraw.size.width.roundToInt(),
                this@onDraw.size.height.roundToInt()
            ),
            alpha = alpha,
            colorFilter = colorFilter
        )
    }

    ...
}
Copy the code

In DrawScope. Ontouch () will call androidx.com pose. The UI. The graphics, Canvas for drawing, which will eventually entrusted to its internal hold android. Graphics. Draw the Canvas object, The final implementation of Compose’s rendering.