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
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,22 @@ internal class CompositeBackgroundDrawable(
)
}

fun withNewOriginalBackground(newOriginalBackground: Drawable?): CompositeBackgroundDrawable {
return CompositeBackgroundDrawable(
context,
newOriginalBackground,
outerShadows,
background,
backgroundImage,
border,
feedbackUnderlay,
innerShadows,
outline,
borderInsets,
borderRadius,
)
}

fun withNewFeedbackUnderlay(newUnderlay: Drawable?): CompositeBackgroundDrawable {
return CompositeBackgroundDrawable(
context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.RippleDrawable
import androidx.appcompat.widget.SwitchCompat
import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable

/**
* Switch that has its value controlled by JS. Whenever the value of the switch changes, we do not
Expand All @@ -26,6 +27,7 @@ internal class ReactSwitch(context: Context) : SwitchCompat(context) {
private var allowChange = true
private var trackColorForFalse: Int? = null
private var trackColorForTrue: Int? = null
private var rippleColor: Int? = null

override fun setChecked(checked: Boolean) {
if (allowChange && isChecked != checked) {
Expand All @@ -41,8 +43,16 @@ internal class ReactSwitch(context: Context) : SwitchCompat(context) {
}

override fun setBackgroundColor(color: Int) {
background =
RippleDrawable(createRippleDrawableColorStateList(color), ColorDrawable(color), null)
val ripple =
RippleDrawable(
createRippleDrawableColorStateList(rippleColor ?: color),
ColorDrawable(color),
null,
)
// Keep the outline, box shadows and other styles layered over the background by
// BackgroundStyleApplicator, which may have been applied before the background color.
val composite = background as? CompositeBackgroundDrawable
background = composite?.withNewOriginalBackground(ripple) ?: ripple
}

fun setColor(drawable: Drawable, color: Int?) {
Expand All @@ -60,10 +70,14 @@ internal class ReactSwitch(context: Context) : SwitchCompat(context) {
fun setThumbColor(color: Int?) {
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)
// Set the ripple color if the background is, or is wrapped around, a RippleDrawable
val currentBackground = super.getBackground()
val ripple =
(currentBackground as? CompositeBackgroundDrawable)?.originalBackground as? RippleDrawable
?: currentBackground as? RippleDrawable
rippleColor = color
if (color != null && ripple != null) {
ripple.setColor(createRippleDrawableColorStateList(color))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* 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.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.RippleDrawable
import android.util.DisplayMetrics
import android.view.ContextThemeWrapper
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.DisplayMetricsHolder
import com.facebook.react.uimanager.drawable.CompositeBackgroundDrawable
import com.facebook.react.uimanager.style.BoxShadow
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment

/** Tests for how [ReactSwitchManager] applies a background color alongside other view styles. */
@RunWith(RobolectricTestRunner::class)
class ReactSwitchManagerTest {

private lateinit var manager: ReactSwitchManager
private lateinit var view: ReactSwitch

@Before
fun setUp() {
ReactNativeFeatureFlagsForTests.setUp()
DisplayMetricsHolder.setScreenDisplayMetrics(DisplayMetrics().apply { density = 2f })
manager = ReactSwitchManager()
view =
ReactSwitch(
ContextThemeWrapper(
RuntimeEnvironment.getApplication(),
androidx.appcompat.R.style.Theme_AppCompat,
)
)
}

@After
fun tearDown() {
DisplayMetricsHolder.setScreenDisplayMetrics(null)
ReactNativeFeatureFlags.dangerouslyReset()
}

@Test
fun backgroundColorKeepsOutlinePropsSetBeforeIt() {
manager.setOutlineColor(view, Color.BLUE)
manager.setOutlineOffset(view, 8f)
manager.setBackgroundColor(view, Color.WHITE)
manager.setOutlineWidth(view, 3f)

assertThat(BackgroundStyleApplicator.getOutlineColor(view)).isEqualTo(Color.BLUE)
assertThat(BackgroundStyleApplicator.getOutlineOffset(view)).isEqualTo(16f)
assertThat(rippleColor()).isEqualTo(Color.WHITE)
}

@Test
fun backgroundColorUpdateKeepsOutline() {
manager.setBackgroundColor(view, Color.WHITE)
manager.setOutlineColor(view, Color.BLUE)
manager.setOutlineWidth(view, 3f)
manager.setBackgroundColor(view, Color.YELLOW)

assertThat(BackgroundStyleApplicator.getOutlineColor(view)).isEqualTo(Color.BLUE)
assertThat(rippleColor()).isEqualTo(Color.YELLOW)
}

@Test
fun backgroundColorKeepsBoxShadow() {
BackgroundStyleApplicator.setBoxShadow(
view,
listOf(BoxShadow(offsetX = 0f, offsetY = 0f, color = Color.RED, spreadDistance = 6f)),
)
manager.setBackgroundColor(view, Color.WHITE)

val composite = view.background as CompositeBackgroundDrawable
assertThat(composite.outerShadows).hasSize(1)
assertThat(rippleColor()).isEqualTo(Color.WHITE)
}

@Test
fun thumbColorTintsRippleWrappedByOtherStyles() {
manager.setBackgroundColor(view, Color.WHITE)
manager.setOutlineWidth(view, 3f)
manager.setThumbColor(view, Color.GREEN)

val pressed = intArrayOf(android.R.attr.state_pressed)
assertThat(rippleEffectColor().getColorForState(pressed, 0)).isEqualTo(Color.GREEN)
}

@Test
fun backgroundColorKeepsThumbColorSetBeforeIt() {
manager.setThumbColor(view, Color.GREEN)
manager.setBackgroundColor(view, Color.WHITE)

val pressed = intArrayOf(android.R.attr.state_pressed)
assertThat(rippleEffectColor().getColorForState(pressed, 0)).isEqualTo(Color.GREEN)
assertThat(rippleColor()).isEqualTo(Color.WHITE)
}

@Test
fun backgroundColorDoesNotUseClearedThumbColor() {
manager.setThumbColor(view, Color.GREEN)
manager.setThumbColor(view, null)
manager.setBackgroundColor(view, Color.WHITE)

val pressed = intArrayOf(android.R.attr.state_pressed)
assertThat(rippleEffectColor().getColorForState(pressed, 0)).isEqualTo(Color.WHITE)
}

private fun ripple(): RippleDrawable {
val background = view.background
val drawable =
if (background is CompositeBackgroundDrawable) background.originalBackground else background
return drawable as RippleDrawable
}

private fun rippleColor(): Int = (ripple().getDrawable(0) as ColorDrawable).color

// RippleDrawable has no public getter for the color passed to setColor().
private fun rippleEffectColor(): ColorStateList {
val state =
RippleDrawable::class
.java
.getDeclaredField("mState")
.apply { isAccessible = true }
.get(ripple())
return state.javaClass.getDeclaredField("mColor").apply { isAccessible = true }.get(state)
as ColorStateList
}
}
Loading