diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index a6d6b74bd746..91c977b6a079 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -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.* @@ -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 { @@ -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 { @@ -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 { diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java index df503ba02703..ebea4fbc29d4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java @@ -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; @@ -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() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/kotlin/BackgroundMeasure.kt b/ReactAndroid/src/main/java/com/facebook/react/kotlin/BackgroundMeasure.kt new file mode 100644 index 000000000000..73b7a2cf23a7 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/kotlin/BackgroundMeasure.kt @@ -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 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)) + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/kotlin/ComposeShims.java b/ReactAndroid/src/main/java/com/facebook/react/kotlin/ComposeShims.java new file mode 100644 index 000000000000..8714d127fef6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/kotlin/ComposeShims.java @@ -0,0 +1,267 @@ +/* + * 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 static androidx.compose.ui.platform.AndroidComposeView_androidKt.getLocaleLayoutDirection; +import static androidx.compose.ui.unit.AndroidDensity_androidKt.Density; + +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 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 abstract static class BackgroundMeasureOwner implements Owner { + private final LayoutNode mRoot; + private final List mNodes; + private final Context mContext; + + public BackgroundMeasureOwner(Context context) { + mRoot = new LayoutNode(); + mRoot.setMeasurePolicy(RootMeasurePolicy.INSTANCE); + + mNodes = new ArrayList<>(); + mContext = context; + } + + public List 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 drawBlock, + @NonNull Function0 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); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/kotlin/TestComposable.kt b/ReactAndroid/src/main/java/com/facebook/react/kotlin/TestComposable.kt new file mode 100644 index 000000000000..9b897ba9f9ef --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/kotlin/TestComposable.kt @@ -0,0 +1,26 @@ +/* + * 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.util.Log +import android.view.View +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView + +@Composable +fun TestComposable() { + Text("Hello world from Compose!") +} + +fun getComposeView(context: Context): View { + Log.e("TAG", "DAVIDDAVIDDAVIDDAVIDDAVIDDAVIDDAVIDDAVIDDAVID") + + return ComposeView(context).apply { setContent { TestComposable() } } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/ReactComposeView.kt b/ReactAndroid/src/main/java/com/facebook/react/views/ReactComposeView.kt new file mode 100644 index 000000000000..378f232636d2 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/ReactComposeView.kt @@ -0,0 +1,47 @@ +/* + * 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.views + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.platform.AbstractComposeView + +open class ReactComposeView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + AbstractComposeView(context, attrs, defStyleAttr) { + + private val content = mutableStateOf<(@Composable () -> Unit)?>(null) + + @Suppress("RedundantVisibilityModifier") + protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false + // private set + + @Composable + override fun Content() { + content.value?.invoke() + } + + override fun getAccessibilityClassName(): CharSequence { + return javaClass.name + } + + /** + * Set the Jetpack Compose UI content for this view. Initial composition will occur when the view + * becomes attached to a window or when [createComposition] is called, whichever comes first. + */ + fun setContent(content: @Composable () -> Unit) { + shouldCreateCompositionOnAttachedToWindow = true + this.content.value = content + if (isAttachedToWindow) { + createComposition() + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactComposeSwitch.kt b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactComposeSwitch.kt new file mode 100644 index 000000000000..1d342580cba6 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactComposeSwitch.kt @@ -0,0 +1,63 @@ +/* + * 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.views.switchview + +import android.content.Context +import androidx.compose.material.Switch +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.Constraints +import com.facebook.react.kotlin.BackgroundMeasure +import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.views.ReactComposeView +import com.facebook.yoga.YogaMeasureOutput + +@Composable +fun ComposeSwitch( + checked: Boolean, + enabled: Boolean, + onCheckedChange: (isChecked: Boolean) -> Unit +) { + val checkedState = remember { mutableStateOf(checked) } + Switch( + checked = checkedState.value, + onCheckedChange = { + checkedState.value = it + onCheckedChange(it) + }, + enabled = enabled) +} + +fun measureInBackground( + context: Context, +): Long { + val backgroundMeasure = BackgroundMeasure(context) + val size = backgroundMeasure.measureComposable(Constraints()) { ComposeSwitch(false, false, {}) } + println("Compose switch measured to $size") + + return YogaMeasureOutput.make( + PixelUtil.toDIPFromPixel(size.width.toFloat()), + PixelUtil.toDIPFromPixel(size.height.toFloat())) +} + +class ReactComposeSwitchView : ReactComposeView { + constructor(ctx: Context) : super(ctx) + + var switchEnabled: Boolean = true + var switchChecked: Boolean = true + var onCheckedChangeListener: (ReactComposeSwitchView, Boolean) -> Unit = { _, _ -> } + + fun updateView(): ReactComposeSwitchView { + return this.apply { + setContent { + ComposeSwitch(switchChecked, switchEnabled) { onCheckedChangeListener(this, it) } + } + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java index 69c7dbd5dc66..5aa814c15c85 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitch.java @@ -22,6 +22,7 @@ * allow any other changes to that switch until JS sets a value explicitly. This stops the Switch * from changing its value multiple times, when those changes have not been processed by JS first. */ +// TODO: use Switch /*package*/ class ReactSwitch extends SwitchCompat { private boolean mAllowChange; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java index 9fc2d32315ea..18d70d12024d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java @@ -10,14 +10,12 @@ import android.content.Context; import android.view.View; -import android.widget.CompoundButton; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.LayoutShadowNode; -import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIManagerHelper; @@ -26,70 +24,32 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.viewmanagers.AndroidSwitchManagerDelegate; import com.facebook.react.viewmanagers.AndroidSwitchManagerInterface; -import com.facebook.yoga.YogaMeasureFunction; import com.facebook.yoga.YogaMeasureMode; -import com.facebook.yoga.YogaMeasureOutput; -import com.facebook.yoga.YogaNode; +import kotlin.jvm.functions.Function2; /** View manager for {@link ReactSwitch} components. */ -public class ReactSwitchManager extends SimpleViewManager - implements AndroidSwitchManagerInterface { +public class ReactSwitchManager extends SimpleViewManager + implements AndroidSwitchManagerInterface { public static final String REACT_CLASS = "AndroidSwitch"; - static class ReactSwitchShadowNode extends LayoutShadowNode implements YogaMeasureFunction { + private static final Function2 + ON_CHECKED_CHANGE_LISTENER = + new Function2() { + @Override + public kotlin.Unit invoke(ReactComposeSwitchView view, Boolean isChecked) { + ReactContext reactContext = (ReactContext) view.getContext(); - private int mWidth; - private int mHeight; - private boolean mMeasured; + int reactTag = view.getId(); + UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag) + .dispatchEvent( + new ReactSwitchEvent( + UIManagerHelper.getSurfaceId(reactContext), reactTag, isChecked)); + return null; + } + }; - private ReactSwitchShadowNode() { - initMeasureFunction(); - } - - private void initMeasureFunction() { - setMeasureFunction(this); - } - - @Override - public long measure( - YogaNode node, - float width, - YogaMeasureMode widthMode, - float height, - YogaMeasureMode heightMode) { - if (!mMeasured) { - // Create a switch with the default config and measure it; since we don't (currently) - // support setting custom switch text, this is fine, as all switches will measure the same - // on a specific device/theme/locale combination. - ReactSwitch reactSwitch = new ReactSwitch(getThemedContext()); - reactSwitch.setShowText(false); - final int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - reactSwitch.measure(spec, spec); - mWidth = reactSwitch.getMeasuredWidth(); - mHeight = reactSwitch.getMeasuredHeight(); - mMeasured = true; - } - - return YogaMeasureOutput.make(mWidth, mHeight); - } - } - - private static final CompoundButton.OnCheckedChangeListener ON_CHECKED_CHANGE_LISTENER = - new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - ReactContext reactContext = (ReactContext) buttonView.getContext(); - - int reactTag = buttonView.getId(); - UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag) - .dispatchEvent( - new ReactSwitchEvent( - UIManagerHelper.getSurfaceId(reactContext), reactTag, isChecked)); - } - }; - - private final ViewManagerDelegate mDelegate; + private final ViewManagerDelegate mDelegate; public ReactSwitchManager() { mDelegate = new AndroidSwitchManagerDelegate<>(this); @@ -102,97 +62,97 @@ public String getName() { @Override public LayoutShadowNode createShadowNodeInstance() { - return new ReactSwitchShadowNode(); + return new LayoutShadowNode(); } @Override public Class getShadowNodeClass() { - return ReactSwitchShadowNode.class; + return LayoutShadowNode.class; } @Override - protected ReactSwitch createViewInstance(ThemedReactContext context) { - ReactSwitch view = new ReactSwitch(context); - view.setShowText(false); - return view; + protected ReactComposeSwitchView createViewInstance(ThemedReactContext context) { + return new ReactComposeSwitchView(context); } @Override @ReactProp(name = "disabled", defaultBoolean = false) - public void setDisabled(ReactSwitch view, boolean disabled) { - view.setEnabled(!disabled); + public void setDisabled(ReactComposeSwitchView composeView, boolean disabled) { + composeView.setSwitchEnabled(!disabled); } @Override @ReactProp(name = ViewProps.ENABLED, defaultBoolean = true) - public void setEnabled(ReactSwitch view, boolean enabled) { - view.setEnabled(enabled); + public void setEnabled(ReactComposeSwitchView composeView, boolean enabled) { + composeView.setSwitchEnabled(enabled); } @Override @ReactProp(name = ViewProps.ON) - public void setOn(ReactSwitch view, boolean on) { - setValueInternal(view, on); + public void setOn(ReactComposeSwitchView view, boolean on) { + view.setSwitchChecked(on); } @Override @ReactProp(name = "value") - public void setValue(ReactSwitch view, boolean value) { - setValueInternal(view, value); + public void setValue(ReactComposeSwitchView view, boolean value) { + view.setSwitchChecked(value); } @Override @ReactProp(name = "thumbTintColor", customType = "Color") - public void setThumbTintColor(ReactSwitch view, @Nullable Integer color) { - this.setThumbColor(view, color); + public void setThumbTintColor(ReactComposeSwitchView view, @Nullable Integer color) { + // this.setThumbColor(view, color); } @Override @ReactProp(name = "thumbColor", customType = "Color") - public void setThumbColor(ReactSwitch view, @Nullable Integer color) { - view.setThumbColor(color); + public void setThumbColor(ReactComposeSwitchView view, @Nullable Integer color) { + // view.setThumbColor(color); } @Override @ReactProp(name = "trackColorForFalse", customType = "Color") - public void setTrackColorForFalse(ReactSwitch view, @Nullable Integer color) { - view.setTrackColorForFalse(color); + public void setTrackColorForFalse(ReactComposeSwitchView view, @Nullable Integer color) { + // view.setTrackColorForFalse(color); } @Override @ReactProp(name = "trackColorForTrue", customType = "Color") - public void setTrackColorForTrue(ReactSwitch view, @Nullable Integer color) { - view.setTrackColorForTrue(color); + public void setTrackColorForTrue(ReactComposeSwitchView view, @Nullable Integer color) { + // view.setTrackColorForTrue(color); } @Override @ReactProp(name = "trackTintColor", customType = "Color") - public void setTrackTintColor(ReactSwitch view, @Nullable Integer color) { - view.setTrackColor(color); + public void setTrackTintColor(ReactComposeSwitchView view, @Nullable Integer color) { + // view.setTrackColor(color); } @Override - public void setNativeValue(ReactSwitch view, boolean value) { - setValueInternal(view, value); + public void setNativeValue(ReactComposeSwitchView view, boolean value) { + view.setSwitchChecked(value); } @Override public void receiveCommand( - @NonNull ReactSwitch view, String commandId, @Nullable ReadableArray args) { + @NonNull ReactComposeSwitchView view, String commandId, @Nullable ReadableArray args) { switch (commandId) { case "setNativeValue": - setValueInternal(view, args != null && args.getBoolean(0)); + boolean value = args != null && args.getBoolean(0); + view.setSwitchChecked(value); break; } } @Override - protected void addEventEmitters(final ThemedReactContext reactContext, final ReactSwitch view) { + protected void addEventEmitters( + final ThemedReactContext reactContext, final ReactComposeSwitchView view) { view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER); } @Override - protected ViewManagerDelegate getDelegate() { + protected ViewManagerDelegate getDelegate() { return mDelegate; } @@ -211,16 +171,16 @@ public long measure( view.setShowText(false); int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); view.measure(measureSpec, measureSpec); - return YogaMeasureOutput.make( - PixelUtil.toDIPFromPixel(view.getMeasuredWidth()), - PixelUtil.toDIPFromPixel(view.getMeasuredHeight())); + + System.out.println( + "View measured to: " + view.getMeasuredWidth() + "x" + view.getMeasuredHeight()); + + return ReactComposeSwitchKt.measureInBackground(context); } - private static void setValueInternal(ReactSwitch view, boolean value) { - // we set the checked change listener to null and then restore it so that we don't fire an - // onChange event to JS when JS itself is updating the value of the switch - view.setOnCheckedChangeListener(null); - view.setOn(value); - view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER); + @Override + protected void onAfterUpdateTransaction(@NonNull ReactComposeSwitchView view) { + super.onAfterUpdateTransaction(view); + view.updateView(); } }