Skip to content

Commit

Permalink
Use compose to render Switch in Fabric
Browse files Browse the repository at this point in the history
Summary:
This change is a proof of concept to try Compose integration with RN component on Android by measuring and drawing Switch component through a Composable function.

To interface Composable nodes with the view system, we use a "classic" view interoperability layer to plug Compose-based Switch into existing ViewManager and propagate props and events as we do with “classic” views.

Fabric also requires initial measure during layout, which was implemented through custom background `Recomposer` and `LayoutNode` hacks to avoid creating full-blown view.

Differential Revision: D31657259

fbshipit-source-id: ad5d2434d8c783d03f93b829e46e7e09ef5c4564
  • Loading branch information
Andrei Shikov authored and facebook-github-bot committed Jan 11, 2022
1 parent 0fccbd5 commit ac9eb36
Show file tree
Hide file tree
Showing 9 changed files with 606 additions and 98 deletions.
38 changes: 38 additions & 0 deletions ReactAndroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins {
id("com.facebook.react")
id("maven-publish")
id("de.undercouch.download")
id("kotlin-android")
}

import com.facebook.react.tasks.internal.*
Expand Down Expand Up @@ -336,6 +337,7 @@ android {
exclude("com/facebook/react/processing")
exclude("com/facebook/react/module/processing")
}
// kotlin.srcDirs += 'src/main/java/com/facebook/react/kotlin'
}

lintOptions {
Expand All @@ -352,6 +354,24 @@ android {
extractJNI
javadocDeps.extendsFrom api
}

buildFeatures {
compose true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

composeOptions {
kotlinCompilerExtensionVersion '1.0.1'
kotlinCompilerVersion "1.5.10"
kotlinCompilerExtensionVersion "1.0.0-beta08"
}

kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
Expand Down Expand Up @@ -387,6 +407,24 @@ dependencies {
androidTestImplementation("androidx.test:runner:${ANDROIDX_TEST_VERSION}")
androidTestImplementation("androidx.test:rules:${ANDROIDX_TEST_VERSION}")
androidTestImplementation("org.mockito:mockito-core:${MOCKITO_CORE_VERSION}")

implementation("androidx.core:core-ktx:1.3.2")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.21")

implementation "androidx.compose.compiler:compiler:1.0.0-beta08"

// Integration with activities
implementation 'androidx.activity:activity-compose:1.3.1'
// Compose Material Design
implementation 'androidx.compose.material:material:1.0.1'
// Animations
implementation 'androidx.compose.animation:animation:1.0.1'
// Tooling support (Previews, etc.)
implementation 'androidx.compose.ui:ui-tooling:1.0.1'
// Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07'
// UI Tests
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.0.1'
}

react {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Callback;
Expand Down Expand Up @@ -89,7 +93,22 @@ protected ReactRootView createRootView() {

protected void loadApp(String appKey) {
mReactDelegate.loadApp(appKey);
getPlainActivity().setContentView(mReactDelegate.getReactRootView());
Context context = getContext();
LinearLayout linearLayout = new LinearLayout(context);
linearLayout.setLayoutParams(
new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));

linearLayout.setOrientation(LinearLayout.VERTICAL);
View composeView = com.facebook.react.kotlin.TestComposableKt.getComposeView(context);

TextView text = new TextView(context);
text.setText("This is a text");
linearLayout.addView(text);
linearLayout.addView(composeView);
linearLayout.addView(mReactDelegate.getReactRootView());

getPlainActivity().setContentView(linearLayout);
}

protected void onPause() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.kotlin

import android.content.Context
import android.widget.FrameLayout
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.getValue
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.LocalView
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

class BackgroundMeasure(private val context: Context) {
private val view = FrameLayout(context)
private 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) {}
}
private val root = owner.root
private 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)

/** Synchronously (I hope?) measures Composable on a current thread (at least seems like so?) */
fun measureComposable(constraints: Constraints, content: @Composable () -> Unit): IntSize {
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),
LocalView.provides(view),
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))
}
}
Loading

0 comments on commit ac9eb36

Please sign in to comment.