Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/react-native/Libraries/Components/Switch/Switch.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ export interface SwitchProps extends SwitchPropsIOS {
*/
thumbColor?: ColorValue | undefined;

/**
* Android only. Thumb color when the switch is off. Overrides `thumbColor`
* for the unchecked state.
* @platform android
*/
thumbColorForFalse?: ColorValue | undefined;

/**
* Android only. Thumb color when the switch is on. Overrides `thumbColor`
* for the checked state.
* @platform android
*/
thumbColorForTrue?: ColorValue | undefined;

/**
* Custom colors for the switch track
*
Expand Down Expand Up @@ -102,6 +116,21 @@ export interface SwitchProps extends SwitchPropsIOS {
*/
ios_backgroundColor?: ColorValue | undefined;

/**
* Android only. Drawable resource name(s) to display as an icon inside the
* thumb. Accepts a `{false, true}` object (like `trackColor`) so each state
* can have a different icon. Omit a key to show no icon for that state.
* Requires Material Design 3 (MaterialSwitch).
* @platform android
*/
thumbIcon?: {false?: string | null; true?: string | null} | null | undefined;

/**
* Android only. Color tint applied to the thumb icon.
* @platform android
*/
thumbIconTint?: ColorValue | undefined;

style?: StyleProp<ViewStyle> | undefined;
}

Expand Down
38 changes: 38 additions & 0 deletions packages/react-native/Libraries/Components/Switch/Switch.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ type SwitchPropsBase = {
*/
thumbColor?: ?ColorValue,

/**
* Android only. Thumb color when the switch is off. Overrides `thumbColor`
* for the unchecked state.
* @platform android
*/
thumbColorForFalse?: ?ColorValue,

/**
* Android only. Thumb color when the switch is on. Overrides `thumbColor`
* for the checked state.
* @platform android
*/
thumbColorForTrue?: ?ColorValue,

/**
Custom colors for the switch track.

Expand All @@ -94,6 +108,21 @@ type SwitchPropsBase = {
*/
ios_backgroundColor?: ?ColorValue,

/**
* Android only. Drawable resource name(s) to display as an icon inside the
* thumb. Accepts a `{false, true}` object (like `trackColor`) so each state
* can have a different icon. Omit a key to show no icon for that state.
* Requires Material Design 3 (MaterialSwitch).
* @platform android
*/
thumbIcon?: ?Readonly<{false?: ?string, true?: ?string}>,

/**
* Android only. Color tint applied to the thumb icon.
* @platform android
*/
thumbIconTint?: ?ColorValue,

/**
Invoked when the user tries to change the value of the switch. Receives
the change event as an argument. If you want to only receive the new
Expand Down Expand Up @@ -180,6 +209,10 @@ const Switch: component(
onValueChange,
style,
thumbColor,
thumbColorForFalse,
thumbColorForTrue,
thumbIcon,
thumbIconTint,
trackColor,
value,
...restProps
Expand Down Expand Up @@ -243,6 +276,11 @@ const Switch: component(
on: value === true,
style,
thumbTintColor: thumbColor,
thumbColorForFalse,
thumbColorForTrue,
thumbIconForFalse: thumbIcon?.false,
thumbIconForTrue: thumbIcon?.true,
thumbIconTint,
trackColorForFalse: trackColorForFalse,
trackColorForTrue: trackColorForTrue,
trackTintColor: value === true ? trackColorForTrue : trackColorForFalse,
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/ReactAndroid/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ dependencies {
api(libs.androidx.appcompat)
api(libs.androidx.appcompat.resources)
api(libs.androidx.autofill)
api(libs.material)
api(libs.androidx.swiperefreshlayout)
api(libs.androidx.tracing)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,36 @@ package com.facebook.react.views.switchview

import android.content.Context
import android.content.res.ColorStateList
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.RippleDrawable
import androidx.appcompat.widget.SwitchCompat
import android.graphics.drawable.StateListDrawable
import android.view.ContextThemeWrapper
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import com.google.android.material.materialswitch.MaterialSwitch

/**
* Switch that has its value controlled by JS. Whenever the value of the switch changes, we do not
* 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.
*/
internal class ReactSwitch(context: Context) : SwitchCompat(context) {
internal class ReactSwitch(context: Context) : MaterialSwitch(
ContextThemeWrapper(context, R.style.Theme_Material3_DayNight)
) {

private var allowChange = true
private var trackColorForFalse: Int? = null
private var thumbColorForTrue: Int? = null
private var thumbColorForFalse: Int? = null
private var trackColorForTrue: Int? = null
private var trackColorForFalse: Int? = null
private var thumbIconDrawableForTrue: Drawable? = null
private var thumbIconDrawableForFalse: Drawable? = null

override fun setChecked(checked: Boolean) {
if (allowChange && isChecked != checked) {
allowChange = false
super.setChecked(checked)
setTrackColor(checked)
} else {
// Even if mAllowChange is set to false or the checked value hasn't changed, we still must
// call the super method, since it will make sure the thumb is moved back to the correct edge.
Expand All @@ -45,64 +52,89 @@ internal class ReactSwitch(context: Context) : SwitchCompat(context) {
RippleDrawable(createRippleDrawableColorStateList(color), ColorDrawable(color), null)
}

fun setColor(drawable: Drawable, color: Int?): Unit {
if (color == null) {
drawable.clearColorFilter()
} else {
drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)
}
}
fun setThumbColor(color: Int?) = setThumbColorForTrue(color)

fun setTrackColor(color: Int?): Unit {
setColor(super.getTrackDrawable(), color)
fun setThumbColorForTrue(color: Int?) {
thumbColorForTrue = color
applyThumbTintList()
}

fun setThumbColor(color: Int?): Unit {
setColor(super.getThumbDrawable(), color)

// Set the ripple color if background is instance of RippleDrawable
if (color != null && super.getBackground() is RippleDrawable) {
val customColorState = createRippleDrawableColorStateList(color)
(super.getBackground() as RippleDrawable).setColor(customColorState)
}
fun setThumbColorForFalse(color: Int?) {
thumbColorForFalse = color
applyThumbTintList()
}

fun setOn(on: Boolean): Unit {
fun setOn(on: Boolean) {
// If the switch has a different value than the value sent by JS, we must change it.
if (isChecked != on) {
super.setChecked(on)
setTrackColor(on)
}
allowChange = true
}

fun setTrackColorForTrue(color: Int?): Unit {
if (color == trackColorForTrue) {
fun setTrackColorForTrue(color: Int?) {
trackColorForTrue = color
applyTrackTintList()
}

fun setTrackColorForFalse(color: Int?) {
trackColorForFalse = color
applyTrackTintList()
}

fun setThumbIconForFalse(drawable: Drawable?) {
thumbIconDrawableForFalse = drawable
applyThumbIconDrawable()
}

fun setThumbIconForTrue(drawable: Drawable?) {
thumbIconDrawableForTrue = drawable
applyThumbIconDrawable()
}

private fun applyThumbIconDrawable() {
if (thumbIconDrawableForTrue == null && thumbIconDrawableForFalse == null) {
setThumbIconDrawable(null)
return
}
trackColorForTrue = color
if (isChecked) {
setTrackColor(trackColorForTrue)
val stateList = StateListDrawable()
thumbIconDrawableForTrue?.let {
stateList.addState(intArrayOf(android.R.attr.state_checked), it)
}
thumbIconDrawableForFalse?.let {
stateList.addState(intArrayOf(-android.R.attr.state_checked), it)
}
setThumbIconDrawable(stateList)
}

fun setTrackColorForFalse(color: Int?): Unit {
if (color == trackColorForFalse) {
private fun applyThumbTintList() {
if (thumbColorForTrue == null && thumbColorForFalse == null) {
setThumbTintList(null)
return
}
trackColorForFalse = color
if (!isChecked) {
setTrackColor(trackColorForFalse)
}
setThumbTintList(
ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)),
intArrayOf(
thumbColorForTrue ?: MaterialColors.getColor(this, R.attr.colorOnPrimary),
thumbColorForFalse ?: MaterialColors.getColor(this, R.attr.colorOutline))))
}

private fun setTrackColor(checked: Boolean) {
if (trackColorForTrue != null || trackColorForFalse != null) {
// Update the track color to reflect the new value. We only want to do this if these
// props were actually set from JS; otherwise we'll just reset the color to the default.
val currentTrackColor = if (checked) trackColorForTrue else trackColorForFalse
setTrackColor(currentTrackColor)
private fun applyTrackTintList() {
if (trackColorForTrue == null && trackColorForFalse == null) {
setTrackTintList(null)
return
}
setTrackTintList(
ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)),
intArrayOf(
trackColorForTrue ?: MaterialColors.getColor(this, R.attr.colorPrimary),
trackColorForFalse ?: MaterialColors.getColor(this, R.attr.colorSurfaceContainerHighest))))
}

private fun createRippleDrawableColorStateList(color: Int): ColorStateList =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
package com.facebook.react.views.switchview

import android.content.Context
import android.content.ContextWrapper
import android.content.res.ColorStateList
import android.view.View
import android.widget.CompoundButton
import androidx.annotation.ColorInt
Expand All @@ -25,6 +27,7 @@ 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.react.views.imagehelper.ResourceDrawableIdHelper
import com.facebook.yoga.YogaMeasureMode
import com.facebook.yoga.YogaMeasureOutput

Expand Down Expand Up @@ -76,6 +79,16 @@ internal class ReactSwitchManager :
view.setThumbColor(value)
}

@ReactProp(name = "thumbColorForFalse", customType = "Color")
override fun setThumbColorForFalse(view: ReactSwitch, value: Int?) {
view.setThumbColorForFalse(value)
}

@ReactProp(name = "thumbColorForTrue", customType = "Color")
override fun setThumbColorForTrue(view: ReactSwitch, value: Int?) {
view.setThumbColorForTrue(value)
}

@ReactProp(name = "trackColorForFalse", customType = "Color")
override fun setTrackColorForFalse(view: ReactSwitch, value: Int?) {
view.setTrackColorForFalse(value)
Expand All @@ -87,8 +100,21 @@ internal class ReactSwitchManager :
}

@ReactProp(name = "trackTintColor", customType = "Color")
override fun setTrackTintColor(view: ReactSwitch, value: Int?) {
view.setTrackColor(value)
override fun setTrackTintColor(view: ReactSwitch, value: Int?) = setTrackColorForTrue(view, value)

@ReactProp(name = "thumbIconForFalse")
override fun setThumbIconForFalse(view: ReactSwitch, value: String?) {
view.setThumbIconForFalse(ResourceDrawableIdHelper.getResourceDrawable(view.context, value))
}

@ReactProp(name = "thumbIconForTrue")
override fun setThumbIconForTrue(view: ReactSwitch, value: String?) {
view.setThumbIconForTrue(ResourceDrawableIdHelper.getResourceDrawable(view.context, value))
}

@ReactProp(name = "thumbIconTint", customType = "Color")
override fun setThumbIconTint(view: ReactSwitch, value: Int?) {
view.setThumbIconTintList(if (value != null) ColorStateList.valueOf(value) else null)
}

override fun setNativeValue(view: ReactSwitch, value: Boolean) {
Expand Down Expand Up @@ -137,7 +163,13 @@ internal class ReactSwitchManager :

private val ON_CHECKED_CHANGE_LISTENER =
CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
val reactContext = buttonView.context as ReactContext
// The view's context is wrapped in a ContextThemeWrapper (for Material3 theming),
// so walk up the chain to find the underlying ReactContext.
var ctx: android.content.Context = buttonView.context
while (ctx !is ReactContext && ctx is ContextWrapper) {
ctx = ctx.baseContext
}
val reactContext = ctx as ReactContext
val reactTag = buttonView.id
UIManagerHelper.getEventDispatcher(reactContext)
?.dispatchEvent(
Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ndkVersion = "27.1.12297006"
agp = "8.12.0"
androidx-annotation = "1.6.0"
androidx-appcompat = "1.7.0"
material = "1.13.0"
androidx-autofill = "1.3.0"
androidx-benchmark-macro-junit4 = "1.3.3"
androidx-profileinstaller = "1.4.1"
Expand Down Expand Up @@ -54,6 +55,7 @@ nlohmannjson="3.11.2"
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-appcompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "androidx-appcompat" }
material = { module = "com.google.android.material:material", version.ref = "material" }
Comment thread
adrcotfas marked this conversation as resolved.
androidx-autofill = { module = "androidx.autofill:autofill", version.ref = "androidx-autofill" }
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidx-benchmark-macro-junit4" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,17 @@ type AndroidSwitchNativeProps = Readonly<{
disabled?: WithDefault<boolean, false>,
enabled?: WithDefault<boolean, true>,
thumbColor?: ?ColorValue,
thumbColorForFalse?: ?ColorValue,
thumbColorForTrue?: ?ColorValue,
trackColorForFalse?: ?ColorValue,
trackColorForTrue?: ?ColorValue,
value?: WithDefault<boolean, false>,
on?: WithDefault<boolean, false>,
thumbTintColor?: ?ColorValue,
trackTintColor?: ?ColorValue,
thumbIconForFalse?: ?string,
thumbIconForTrue?: ?string,
thumbIconTint?: ?ColorValue,

// Events
onChange?: BubblingEventHandler<AndroidSwitchChangeEvent>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>

</vector>
Loading