Skip to content

Instantly share code, notes, and snippets.

@ShikaSD
Last active December 11, 2021 13:30
Show Gist options
  • Save ShikaSD/96e468c3ddcf35877b8e9d9611a84069 to your computer and use it in GitHub Desktop.
Save ShikaSD/96e468c3ddcf35877b8e9d9611a84069 to your computer and use it in GitHub Desktop.
Measuring Compose content on background thread.
import android.content.Context
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontLoader
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Measures content in background and displays the size and thread it was measured on
*/
@Composable
fun BackgroundMeasure(contentToMeasure: @Composable () -> Unit) {
var message by remember { mutableStateOf("") }
val context = LocalContext.current
LaunchedEffect("") {
withContext(Dispatchers.Default) {
val size = measureComposable(context, content = contentToMeasure)
val threadName = Thread.currentThread().name
message = "Content size: $size, measured on thread $threadName"
}
}
Text(message)
}
/**
* Synchronously (I hope?) measures Composable on a current thread (at least seems like so?)
*/
private fun measureComposable(context: Context, content: @Composable () -> Unit): IntSize {
val owner = object : ComposeShims.BackgroundMeasureOwner(context) {
// These methods use inline classes, so we have to override them here
override fun calculateLocalPosition(positionInWindow: Offset): Offset = positionInWindow
override fun calculatePositionInWindow(localPosition: Offset): Offset = localPosition
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? = null
override fun requestRectangleOnScreen(rect: Rect) {}
}
val root = owner.root
val applier = ComposeShims.createApplier(root)
val clock = object : MonotonicFrameClock {
override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R =
onFrame(System.nanoTime())
}
val coroutineContext = Dispatchers.Unconfined + clock
val recomposer = Recomposer(coroutineContext)
val composition = Composition(applier, recomposer)
composition.setContent {
CompositionLocalProvider(
// See ProvideCommonCompositionLocals or ProvideAndroidCompositionLocals for a full list
// Here I only added things until Text composable stopped crashing
LocalDensity.provides(owner.density),
LocalFontLoader.provides(owner.fontLoader),
LocalContext.provides(context),
LocalLayoutDirection.provides(owner.layoutDirection),
LocalViewConfiguration.provides(owner.viewConfiguration),
content = content
)
}
val runRecomposeJob = CoroutineScope(coroutineContext).launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
ComposeShims.attachOwner(root, owner)
owner.nodes.forEach {
ComposeShims.setLayoutRequired(it)
}
(root as Measurable).measure(Constraints())
runRecomposeJob.cancel()
return IntSize(ComposeShims.getNodeWidth(root), ComposeShims.getNodeHeight(root))
}
import static androidx.compose.ui.platform.AndroidComposeView_androidKt.getLocaleLayoutDirection;
import static androidx.compose.ui.unit.AndroidDensity_androidKt.Density;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.compose.runtime.Applier;
import androidx.compose.ui.autofill.Autofill;
import androidx.compose.ui.autofill.AutofillTree;
import androidx.compose.ui.focus.FocusManager;
import androidx.compose.ui.graphics.Canvas;
import androidx.compose.ui.hapticfeedback.HapticFeedback;
import androidx.compose.ui.layout.RootMeasurePolicy;
import androidx.compose.ui.node.LayoutNode;
import androidx.compose.ui.node.OwnedLayer;
import androidx.compose.ui.node.Owner;
import androidx.compose.ui.node.OwnerSnapshotObserver;
import androidx.compose.ui.node.RootForTest;
import androidx.compose.ui.node.UiApplier;
import androidx.compose.ui.platform.AccessibilityManager;
import androidx.compose.ui.platform.AndroidFontResourceLoader;
import androidx.compose.ui.platform.AndroidViewConfiguration;
import androidx.compose.ui.platform.ClipboardManager;
import androidx.compose.ui.platform.TextToolbar;
import androidx.compose.ui.platform.ViewConfiguration;
import androidx.compose.ui.platform.WindowInfo;
import androidx.compose.ui.text.font.Font;
import androidx.compose.ui.text.input.TextInputService;
import androidx.compose.ui.unit.Density;
import androidx.compose.ui.unit.LayoutDirection;
import java.util.ArrayList;
import java.util.List;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.functions.Function1;
/**
* A lot of Compose API is internal and Kotlin compiler won't let us to access it
*
* Thankfully, Java doesn't believe in internal, so we can do restricted things as long as
* we don't touch inline functions/classes
*/
@SuppressWarnings("KotlinInternalInJava")
public class ComposeShims {
public static Applier<LayoutNode> createApplier(LayoutNode root) {
return new UiApplier(root);
}
public static void setLayoutRequired(LayoutNode root) {
root.setLayoutState$ui_release(LayoutNode.LayoutState.NeedsRemeasure);
}
public static void attachOwner(LayoutNode root, Owner owner) {
root.attach$ui_release(owner);
}
public static int getNodeWidth(LayoutNode node) {
return node.getWidth();
}
public static int getNodeHeight(LayoutNode node) {
return node.getHeight();
}
/**
* Normally, Owner is a view, but it doesn't have to be!
* Below is minimal owner implementation to measure Text composable.
*/
public static abstract class BackgroundMeasureOwner implements Owner {
private final LayoutNode mRoot;
private final List<LayoutNode> mNodes;
private final Context mContext;
public BackgroundMeasureOwner(Context context) {
mRoot = new LayoutNode();
mRoot.setMeasurePolicy(RootMeasurePolicy.INSTANCE);
mNodes = new ArrayList<>();
mContext = context;
}
public List<LayoutNode> getNodes() {
return mNodes;
}
@NonNull
@Override
public LayoutNode getRoot() {
return mRoot;
}
@NonNull
@Override
public RootForTest getRootForTest() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public HapticFeedback getHapticFeedBack() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public ClipboardManager getClipboardManager() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public AccessibilityManager getAccessibilityManager() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public TextToolbar getTextToolbar() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public AutofillTree getAutofillTree() {
// FIXME: don't need for poc
return null;
}
@Nullable
@Override
public Autofill getAutofill() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public Density getDensity() {
return Density(mContext);
}
@NonNull
@Override
public TextInputService getTextInputService() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public FocusManager getFocusManager() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public WindowInfo getWindowInfo() {
// FIXME: don't need for poc
return null;
}
@NonNull
@Override
public Font.ResourceLoader getFontLoader() {
return new AndroidFontResourceLoader(mContext);
}
@NonNull
@Override
public LayoutDirection getLayoutDirection() {
return getLocaleLayoutDirection(mContext.getResources().getConfiguration());
}
@Override
public boolean getShowLayoutBounds() {
// FIXME: don't need for poc
return false;
}
@Override
public void setShowLayoutBounds(boolean showLayoutBounds) {
// FIXME: don't need for poc
}
@Override
public void onRequestMeasure(@NonNull LayoutNode layoutNode) {
// FIXME: don't need for poc
}
@Override
public void onRequestRelayout(@NonNull LayoutNode layoutNode) {
// FIXME: don't need for poc
}
@Override
public void onAttach(@NonNull LayoutNode node) {
mNodes.add(node);
}
@Override
public void onDetach(@NonNull LayoutNode node) {
mNodes.remove(node);
}
@Override
public boolean requestFocus() {
// FIXME: don't need for poc
return false;
}
@Override
public void measureAndLayout() {
// FIXME: don't need for poc
}
@NonNull
@Override
public OwnedLayer createLayer(@NonNull Function1<? super Canvas, Unit> drawBlock,
@NonNull Function0<Unit> invalidateParentLayer) {
// FIXME: don't need for poc
return null;
}
@Override
public void onSemanticsChange() {
// FIXME: don't need for poc
}
@Override
public void onLayoutChange(@NonNull LayoutNode layoutNode) {
// FIXME: don't need for poc
}
@Override
public long getMeasureIteration() {
return 0;
}
@NonNull
@Override
public ViewConfiguration getViewConfiguration() {
return new AndroidViewConfiguration(android.view.ViewConfiguration.get(mContext));
}
@NonNull
@Override
public OwnerSnapshotObserver getSnapshotObserver() {
return new OwnerSnapshotObserver(Function0::invoke);
}
}
}
@ShikaSD
Copy link
Author

ShikaSD commented Aug 26, 2021

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment