From b886bc4db970d8c70de1596dc3f88bdc398de482 Mon Sep 17 00:00:00 2001 From: Kacper Rozniata <56474758+krozniata@users.noreply.github.com> Date: Mon, 2 Dec 2024 02:41:31 -0800 Subject: [PATCH] feat(android): migrate `ReactSwitchManager` to Kotlin (#48003) Summary: This PR migrates `ReactSwitchManager` to Kotlin Also it moves it's shadow node to a separate file (`ReactSwitchShadowNode.kt`) ## Changelog: [ANDROID] [CHANGED] - Migrate `ReactSwitchManager` to Kotlin Pull Request resolved: https://github.com/facebook/react-native/pull/48003 Test Plan: Make sure that `Switch` example in `RNTester` works correctly Reviewed By: cortinico Differential Revision: D66594606 Pulled By: javache fbshipit-source-id: 774641c4cf57d6d5f770df1fed4fcafef2af7ceb --- .../ReactAndroid/api/ReactAndroid.api | 39 --- .../views/switchview/ReactSwitchManager.java | 232 ------------------ .../views/switchview/ReactSwitchManager.kt | 152 ++++++++++++ .../views/switchview/ReactSwitchShadowNode.kt | 52 ++++ 4 files changed, 204 insertions(+), 271 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchShadowNode.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 693aba38ba8..638958e1b64 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -7137,45 +7137,6 @@ public final class com/facebook/react/views/swiperefresh/ReactSwipeRefreshLayout public fun setRefreshing (Z)V } -public class com/facebook/react/views/switchview/ReactSwitchManager : com/facebook/react/uimanager/SimpleViewManager, com/facebook/react/viewmanagers/AndroidSwitchManagerInterface { - public static final field REACT_CLASS Ljava/lang/String; - public fun ()V - protected synthetic fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Landroid/view/View;)V - protected fun addEventEmitters (Lcom/facebook/react/uimanager/ThemedReactContext;Lcom/facebook/react/views/switchview/ReactSwitch;)V - public fun createShadowNodeInstance ()Lcom/facebook/react/uimanager/LayoutShadowNode; - public synthetic fun createShadowNodeInstance ()Lcom/facebook/react/uimanager/ReactShadowNode; - protected synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View; - protected fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Lcom/facebook/react/views/switchview/ReactSwitch; - protected fun getDelegate ()Lcom/facebook/react/uimanager/ViewManagerDelegate; - public fun getName ()Ljava/lang/String; - public fun getShadowNodeClass ()Ljava/lang/Class; - public fun measure (Landroid/content/Context;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/ReadableMap;FLcom/facebook/yoga/YogaMeasureMode;FLcom/facebook/yoga/YogaMeasureMode;[F)J - public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V - public fun receiveCommand (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V - public synthetic fun setBackgroundColor (Landroid/view/View;I)V - public fun setBackgroundColor (Lcom/facebook/react/views/switchview/ReactSwitch;I)V - public synthetic fun setDisabled (Landroid/view/View;Z)V - public fun setDisabled (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V - public synthetic fun setEnabled (Landroid/view/View;Z)V - public fun setEnabled (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V - public synthetic fun setNativeValue (Landroid/view/View;Z)V - public fun setNativeValue (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V - public synthetic fun setOn (Landroid/view/View;Z)V - public fun setOn (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V - public synthetic fun setThumbColor (Landroid/view/View;Ljava/lang/Integer;)V - public fun setThumbColor (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V - public synthetic fun setThumbTintColor (Landroid/view/View;Ljava/lang/Integer;)V - public fun setThumbTintColor (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V - public synthetic fun setTrackColorForFalse (Landroid/view/View;Ljava/lang/Integer;)V - public fun setTrackColorForFalse (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V - public synthetic fun setTrackColorForTrue (Landroid/view/View;Ljava/lang/Integer;)V - public fun setTrackColorForTrue (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V - public synthetic fun setTrackTintColor (Landroid/view/View;Ljava/lang/Integer;)V - public fun setTrackTintColor (Lcom/facebook/react/views/switchview/ReactSwitch;Ljava/lang/Integer;)V - public synthetic fun setValue (Landroid/view/View;Z)V - public fun setValue (Lcom/facebook/react/views/switchview/ReactSwitch;Z)V -} - public final class com/facebook/react/views/text/DefaultStyleValuesUtil { public static final field INSTANCE Lcom/facebook/react/views/text/DefaultStyleValuesUtil; public static final fun getDefaultTextColor (Landroid/content/Context;)Landroid/content/res/ColorStateList; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java deleted file mode 100644 index f1c3b353bff..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * 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. - */ - -// switchview because switch is a keyword -package com.facebook.react.views.switchview; - -import android.content.Context; -import android.view.View; -import android.widget.CompoundButton; -import androidx.annotation.ColorInt; -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; -import com.facebook.react.uimanager.ViewManagerDelegate; -import com.facebook.react.uimanager.ViewProps; -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; - -/** View manager for {@link ReactSwitch} components. */ -public class ReactSwitchManager extends SimpleViewManager - implements AndroidSwitchManagerInterface { - - public static final String REACT_CLASS = "AndroidSwitch"; - - static class ReactSwitchShadowNode extends LayoutShadowNode implements YogaMeasureFunction { - - private int mWidth; - private int mHeight; - private boolean mMeasured; - - 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; - - public ReactSwitchManager() { - mDelegate = new AndroidSwitchManagerDelegate<>(this); - } - - @Override - public String getName() { - return REACT_CLASS; - } - - @Override - public LayoutShadowNode createShadowNodeInstance() { - return new ReactSwitchShadowNode(); - } - - @Override - public Class getShadowNodeClass() { - return ReactSwitchShadowNode.class; - } - - @Override - protected ReactSwitch createViewInstance(ThemedReactContext context) { - ReactSwitch view = new ReactSwitch(context); - view.setShowText(false); - return view; - } - - @Override - public void setBackgroundColor(ReactSwitch view, @ColorInt int backgroundColor) { - view.setBackgroundColor(backgroundColor); - } - - @Override - @ReactProp(name = "disabled", defaultBoolean = false) - public void setDisabled(ReactSwitch view, boolean disabled) { - view.setEnabled(!disabled); - } - - @Override - @ReactProp(name = ViewProps.ENABLED, defaultBoolean = true) - public void setEnabled(ReactSwitch view, boolean enabled) { - view.setEnabled(enabled); - } - - @Override - @ReactProp(name = ViewProps.ON) - public void setOn(ReactSwitch view, boolean on) { - setValueInternal(view, on); - } - - @Override - @ReactProp(name = "value") - public void setValue(ReactSwitch view, boolean value) { - setValueInternal(view, value); - } - - @Override - @ReactProp(name = "thumbTintColor", customType = "Color") - public void setThumbTintColor(ReactSwitch 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); - } - - @Override - @ReactProp(name = "trackColorForFalse", customType = "Color") - public void setTrackColorForFalse(ReactSwitch view, @Nullable Integer color) { - view.setTrackColorForFalse(color); - } - - @Override - @ReactProp(name = "trackColorForTrue", customType = "Color") - public void setTrackColorForTrue(ReactSwitch view, @Nullable Integer color) { - view.setTrackColorForTrue(color); - } - - @Override - @ReactProp(name = "trackTintColor", customType = "Color") - public void setTrackTintColor(ReactSwitch view, @Nullable Integer color) { - view.setTrackColor(color); - } - - @Override - public void setNativeValue(ReactSwitch view, boolean value) { - setValueInternal(view, value); - } - - @Override - public void receiveCommand( - @NonNull ReactSwitch view, String commandId, @Nullable ReadableArray args) { - switch (commandId) { - case "setNativeValue": - setValueInternal(view, args != null && args.getBoolean(0)); - break; - } - } - - @Override - protected void addEventEmitters(final ThemedReactContext reactContext, final ReactSwitch view) { - view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER); - } - - @Override - protected ViewManagerDelegate getDelegate() { - return mDelegate; - } - - @Override - public long measure( - Context context, - ReadableMap localData, - ReadableMap props, - ReadableMap state, - float width, - YogaMeasureMode widthMode, - float height, - YogaMeasureMode heightMode, - @Nullable float[] attachmentsPositions) { - ReactSwitch view = new ReactSwitch(context); - 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())); - } - - 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); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.kt new file mode 100644 index 00000000000..6764252b7ee --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchManager.kt @@ -0,0 +1,152 @@ +/* + * 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 android.view.View +import android.widget.CompoundButton +import androidx.annotation.ColorInt +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.uimanager.BaseViewManager +import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.uimanager.ViewProps +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.AndroidSwitchManagerDelegate +import com.facebook.react.viewmanagers.AndroidSwitchManagerInterface +import com.facebook.yoga.YogaMeasureMode +import com.facebook.yoga.YogaMeasureOutput + +internal class ReactSwitchManager : + BaseViewManager(), + AndroidSwitchManagerInterface { + + private val delegate: ViewManagerDelegate = AndroidSwitchManagerDelegate(this) + + override fun getName(): String = REACT_CLASS + + override fun createShadowNodeInstance(): ReactSwitchShadowNode = ReactSwitchShadowNode() + + override fun getShadowNodeClass(): Class = + ReactSwitchShadowNode::class.java + + override fun createViewInstance(context: ThemedReactContext): ReactSwitch = + ReactSwitch(context).apply { showText = false } + + override fun setBackgroundColor(view: ReactSwitch, @ColorInt backgroundColor: Int) { + view.setBackgroundColor(backgroundColor) + } + + @ReactProp(name = "disabled", defaultBoolean = false) + override fun setDisabled(view: ReactSwitch, disabled: Boolean) { + view.isEnabled = !disabled + } + + @ReactProp(name = ViewProps.ENABLED, defaultBoolean = true) + override fun setEnabled(view: ReactSwitch, enabled: Boolean) { + view.isEnabled = enabled + } + + @ReactProp(name = ViewProps.ON) + override fun setOn(view: ReactSwitch, on: Boolean) { + setValueInternal(view, on) + } + + @ReactProp(name = "value") + override fun setValue(view: ReactSwitch, value: Boolean) { + setValueInternal(view, value) + } + + @ReactProp(name = "thumbTintColor", customType = "Color") + override fun setThumbTintColor(view: ReactSwitch, color: Int?) { + setThumbColor(view, color) + } + + @ReactProp(name = "thumbColor", customType = "Color") + override fun setThumbColor(view: ReactSwitch, color: Int?) { + view.setThumbColor(color) + } + + @ReactProp(name = "trackColorForFalse", customType = "Color") + override fun setTrackColorForFalse(view: ReactSwitch, color: Int?) { + view.setTrackColorForFalse(color) + } + + @ReactProp(name = "trackColorForTrue", customType = "Color") + override fun setTrackColorForTrue(view: ReactSwitch, color: Int?) { + view.setTrackColorForTrue(color) + } + + @ReactProp(name = "trackTintColor", customType = "Color") + override fun setTrackTintColor(view: ReactSwitch, color: Int?) { + view.setTrackColor(color) + } + + override fun setNativeValue(view: ReactSwitch, value: Boolean) { + setValueInternal(view, value) + } + + override fun receiveCommand(view: ReactSwitch, commandId: String, args: ReadableArray?) { + when (commandId) { + "setNativeValue" -> setValueInternal(view, args?.getBoolean(0) ?: false) + } + } + + override fun addEventEmitters(reactContext: ThemedReactContext, view: ReactSwitch) { + view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER) + } + + override fun updateExtraData(root: ReactSwitch, extraData: Any) { + // Do nothing + } + + override fun getDelegate(): ViewManagerDelegate = delegate + + override fun measure( + context: Context, + localData: ReadableMap?, + props: ReadableMap?, + state: ReadableMap?, + width: Float, + widthMode: YogaMeasureMode, + height: Float, + heightMode: YogaMeasureMode, + attachmentsPositions: FloatArray? + ): Long { + val view = ReactSwitch(context).apply { showText = false } + val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + view.measure(measureSpec, measureSpec) + return YogaMeasureOutput.make( + PixelUtil.toDIPFromPixel(view.measuredWidth.toFloat()), + PixelUtil.toDIPFromPixel(view.measuredHeight.toFloat())) + } + + private fun setValueInternal(view: ReactSwitch, value: Boolean) { + // Temporarily remove the listener to avoid triggering JS events + view.setOnCheckedChangeListener(null) + view.setOn(value) + view.setOnCheckedChangeListener(ON_CHECKED_CHANGE_LISTENER) + } + + internal companion object { + public const val REACT_CLASS: String = "AndroidSwitch" + + private val ON_CHECKED_CHANGE_LISTENER = + CompoundButton.OnCheckedChangeListener { buttonView, isChecked -> + val reactContext = buttonView.context as ReactContext + val reactTag = buttonView.id + UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag) + ?.dispatchEvent( + ReactSwitchEvent(UIManagerHelper.getSurfaceId(reactContext), reactTag, isChecked)) + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchShadowNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchShadowNode.kt new file mode 100644 index 00000000000..86008e8ecc0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/switchview/ReactSwitchShadowNode.kt @@ -0,0 +1,52 @@ +/* + * 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.view.View +import com.facebook.react.uimanager.LayoutShadowNode +import com.facebook.yoga.YogaMeasureFunction +import com.facebook.yoga.YogaMeasureMode +import com.facebook.yoga.YogaMeasureOutput +import com.facebook.yoga.YogaNode + +internal class ReactSwitchShadowNode : LayoutShadowNode(), YogaMeasureFunction { + private var width = 0 + private var height = 0 + private var measured = false + + init { + initMeasureFunction() + } + + private fun initMeasureFunction() { + setMeasureFunction(this) + } + + override fun measure( + node: YogaNode, + width: Float, + widthMode: YogaMeasureMode, + height: Float, + heightMode: YogaMeasureMode + ): Long { + if (!measured) { + // 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. + val reactSwitch = ReactSwitch(themedContext) + reactSwitch.showText = false + val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + reactSwitch.measure(spec, spec) + this.width = reactSwitch.measuredWidth + this.height = reactSwitch.measuredHeight + measured = true + } + + return YogaMeasureOutput.make(this.width, this.height) + } +}