Last active
December 11, 2021 13:30
-
-
Save ShikaSD/96e468c3ddcf35877b8e9d9611a84069 to your computer and use it in GitHub Desktop.
Measuring Compose content on background thread.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
Author
ShikaSD
commented
Aug 26, 2021
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment