Skip to content

Commit

Permalink
RUM-7116: Add radiobutton colors
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Jan 12, 2025
1 parent cad0d62 commit 67331c3
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
-keep class androidx.compose.material.CheckboxKt$CheckboxImpl$1$1 {
<fields>;
}
-keep class androidx.compose.material.RadioButtonKt$RadioButton$2$1 {
<fields>;
}
-keep class androidx.compose.ui.draw.DrawBehindElement {
<fields>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import com.datadog.android.sessionreplay.compose.internal.data.UiContext
import com.datadog.android.sessionreplay.compose.internal.utils.ColorUtils
import com.datadog.android.sessionreplay.compose.internal.utils.PathUtils
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils.Companion.DEFAULT_COLOR_BLACK
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils.Companion.DEFAULT_COLOR_WHITE
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
Expand Down Expand Up @@ -78,6 +80,7 @@ internal class CheckboxSemanticsNodeMapper(
)
}

@Suppress("LongMethod")
private fun createCheckboxWireframes(
parentContext: UiContext,
asyncJobStatusCallback: AsyncJobStatusCallback,
Expand All @@ -89,8 +92,11 @@ internal class CheckboxSemanticsNodeMapper(
val rawFillColor = semanticsUtils.resolveCheckboxFillColor(semanticsNode)
val rawCheckmarkColor = semanticsUtils.resolveCheckmarkColor(semanticsNode)
val fillColorRgba = rawFillColor?.let { convertColor(it) } ?: DEFAULT_COLOR_WHITE
val fallbackColor = parentContext.parentContentColor?.takeIf { colorUtils.isDarkColor(it) }?.let {
DEFAULT_COLOR_WHITE
} ?: DEFAULT_COLOR_BLACK
val checkmarkColorRgba = rawCheckmarkColor?.let { convertColor(it) }
?: getFallbackCheckmarkColor(DEFAULT_COLOR_WHITE)
?: fallbackColor
val parsedFillColor = colorUtils.parseColorSafe(fillColorRgba)
val isChecked = isCheckboxChecked(semanticsNode)
val checkmarkColor = resolveCheckmarkColor(isChecked, checkmarkColorRgba, parsedFillColor)
Expand Down Expand Up @@ -141,6 +147,7 @@ internal class CheckboxSemanticsNodeMapper(

// if we failed to create a wireframe from the path
return createManualCheckedWireframes(
parentContext = parentContext,
semanticsNode = semanticsNode,
globalBounds = globalBounds,
backgroundColor = fillColorRgba,
Expand All @@ -163,12 +170,15 @@ internal class CheckboxSemanticsNodeMapper(
}

private fun createManualCheckedWireframes(
parentContext: UiContext,
semanticsNode: SemanticsNode,
globalBounds: GlobalBounds,
backgroundColor: String,
borderColor: String
): List<MobileSegment.Wireframe> {
val strokeColor = getFallbackCheckmarkColor(backgroundColor)
val strokeColor = parentContext.parentContentColor?.takeIf { colorUtils.isDarkColor(it) }?.let {
DEFAULT_COLOR_WHITE
} ?: DEFAULT_COLOR_BLACK

val wireframes = mutableListOf<MobileSegment.Wireframe>()

Expand Down Expand Up @@ -233,17 +243,7 @@ internal class CheckboxSemanticsNodeMapper(
private fun isCheckboxChecked(semanticsNode: SemanticsNode): Boolean =
semanticsNode.config.getOrNull(SemanticsProperties.ToggleableState) == ToggleableState.On

private fun getFallbackCheckmarkColor(backgroundColor: String?) =
if (backgroundColor == DEFAULT_COLOR_WHITE) {
DEFAULT_COLOR_BLACK
} else {
DEFAULT_COLOR_WHITE
}

internal companion object {
internal const val DEFAULT_COLOR_BLACK = "#000000FF"
internal const val DEFAULT_COLOR_WHITE = "#FFFFFFFF"

// when we create the checkmark manually, what % of the checkbox size should it be
internal const val CHECKMARK_SIZE_FACTOR = 0.5

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,24 @@

package com.datadog.android.sessionreplay.compose.internal.mappers.semantics

import androidx.annotation.VisibleForTesting
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getOrNull
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.compose.internal.data.SemanticsWireframe
import com.datadog.android.sessionreplay.compose.internal.data.UiContext
import com.datadog.android.sessionreplay.compose.internal.utils.ColorUtils
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils.Companion.DEFAULT_COLOR_BLACK
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils.Companion.DEFAULT_COLOR_WHITE
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ColorStringFormatter

internal class RadioButtonSemanticsNodeMapper(
colorStringFormatter: ColorStringFormatter,
semanticsUtils: SemanticsUtils = SemanticsUtils(),
val semanticsUtils: SemanticsUtils = SemanticsUtils(),
private val colorUtils: ColorUtils = ColorUtils()
) : AbstractSemanticsNodeMapper(
colorStringFormatter,
Expand All @@ -30,14 +34,39 @@ internal class RadioButtonSemanticsNodeMapper(
parentContext: UiContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): SemanticsWireframe {
val color = parentContext.parentContentColor?.takeIf { colorUtils.isDarkColor(it) }?.let {
val wireframes = mutableListOf<MobileSegment.Wireframe>()

val fallbackColor = parentContext.parentContentColor?.takeIf { colorUtils.isDarkColor(it) }?.let {
DEFAULT_COLOR_WHITE
} ?: DEFAULT_COLOR_BLACK
val boxWireframe = resolveBoxWireframe(semanticsNode, 0, color)
val dotWireframe = resolveDotWireframe(semanticsNode, 1, color)

val radioButtonColor = if (isMasked(parentContext)) {
DEFAULT_COLOR_GRAY
} else {
val rawRadioButtonColor = semanticsUtils.resolveRadioButtonColor(semanticsNode)
rawRadioButtonColor?.let { convertColor(it) }
?: fallbackColor
}

resolveBoxWireframe(
semanticsNode = semanticsNode,
color = radioButtonColor,
wireframeIndex = 0
)
.let { wireframes.add(it) }

if (!isMasked(parentContext)) {
resolveDotWireframe(
semanticsNode = semanticsNode,
color = radioButtonColor,
wireframeIndex = 1
)
?.let { wireframes.add(it) }
}

return SemanticsWireframe(
uiContext = null,
wireframes = listOfNotNull(boxWireframe, dotWireframe)
wireframes = wireframes
)
}

Expand All @@ -47,6 +76,7 @@ internal class RadioButtonSemanticsNodeMapper(
color: String
): MobileSegment.Wireframe {
val globalBounds = resolveBounds(semanticsNode)

return MobileSegment.Wireframe.ShapeWireframe(
id = resolveId(semanticsNode, wireframeIndex),
x = globalBounds.x,
Expand Down Expand Up @@ -87,10 +117,14 @@ internal class RadioButtonSemanticsNodeMapper(
}
}

companion object {
private fun isMasked(uiContext: UiContext): Boolean {
return uiContext.textAndInputPrivacy != TextAndInputPrivacy.MASK_SENSITIVE_INPUTS
}

internal companion object {
private const val DOT_PADDING_DP = 4
private const val DEFAULT_COLOR_BLACK = "#000000FF"
private const val DEFAULT_COLOR_WHITE = "#FFFFFFFF"

@VisibleForTesting internal const val DEFAULT_COLOR_GRAY = "#8E8F94"
private const val BOX_BORDER_WIDTH = 1L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import com.datadog.android.sessionreplay.compose.internal.data.SemanticsWirefram
import com.datadog.android.sessionreplay.compose.internal.data.UiContext
import com.datadog.android.sessionreplay.compose.internal.utils.ColorUtils
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils.Companion.DEFAULT_COLOR_BLACK
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils.Companion.DEFAULT_COLOR_WHITE
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ColorStringFormatter
Expand Down Expand Up @@ -174,7 +176,5 @@ internal class SwitchSemanticsNodeMapper(
const val CORNER_RADIUS_DP = 20
const val THUMB_DIAMETER_DP = 20
const val BORDER_WIDTH_DP = 1L
const val DEFAULT_COLOR_BLACK = "#000000FF"
const val DEFAULT_COLOR_WHITE = "#FFFFFFFF"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,18 @@ internal object ComposeReflection {
val ColorField = BackgroundElementClass?.getDeclaredFieldSafe("color")
val ShapeField = BackgroundElementClass?.getDeclaredFieldSafe("shape")

val CheckDrawingCacheClass = getClassSafe("androidx.compose.material.CheckDrawingCache")
val CheckboxKtClass = getClassSafe("androidx.compose.material.CheckboxKt\$CheckboxImpl\$1\$1")
val DrawBehindElementClass = getClassSafe("androidx.compose.ui.draw.DrawBehindElement")
val CheckboxKtClass = getClassSafe("androidx.compose.material.CheckboxKt\$CheckboxImpl\$1\$1")
val RadioButtonKtClass = getClassSafe("androidx.compose.material.RadioButtonKt\$RadioButton\$2\$1")
val CheckDrawingCacheClass = getClassSafe("androidx.compose.material.CheckDrawingCache")

val BorderColorField = CheckboxKtClass?.getDeclaredFieldSafe("\$borderColor\$delegate")
val BoxColorField = CheckboxKtClass?.getDeclaredFieldSafe("\$boxColor\$delegate")
val CheckCacheField = CheckboxKtClass?.getDeclaredFieldSafe("\$checkCache")
val CheckColorField = CheckboxKtClass?.getDeclaredFieldSafe("\$checkColor\$delegate")
val CheckPathField = CheckDrawingCacheClass?.getDeclaredFieldSafe("checkPath")
val OnDrawField = DrawBehindElementClass?.getDeclaredFieldSafe("onDraw")
val RadioColorField = RadioButtonKtClass?.getDeclaredFieldSafe("\$radioColor")

val PaddingElementClass = getClassSafe("androidx.compose.foundation.layout.PaddingElement")
val StartField = PaddingElementClass?.getDeclaredFieldSafe("start")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ internal class ReflectionUtils {
return ComposeReflection.BoxColorField?.getSafe(onDrawInstance) as? AnimationState<*, *>
}

fun getRadioColor(onDrawInstance: Any): AnimationState<*, *>? {
return ComposeReflection.RadioColorField?.getSafe(onDrawInstance) as? AnimationState<*, *>
}

fun getCheckColor(onDrawInstance: Any): AnimationState<*, *>? {
return ComposeReflection.CheckColorField?.getSafe(onDrawInstance) as? AnimationState<*, *>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,27 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref
}

internal fun resolveCheckboxFillColor(semanticsNode: SemanticsNode): Long? =
resolveReflectedProperty(
resolveOnDrawProperty(
semanticsNode,
CheckmarkFieldType.FILL_COLOR
OnDrawFieldType.FILL_COLOR
)

internal fun resolveCheckmarkColor(semanticsNode: SemanticsNode): Long? =
resolveReflectedProperty(
resolveOnDrawProperty(
semanticsNode,
CheckmarkFieldType.CHECKMARK_COLOR
OnDrawFieldType.CHECKMARK_COLOR
)

internal fun resolveRadioButtonColor(semanticsNode: SemanticsNode): Long? =
resolveOnDrawProperty(
semanticsNode,
OnDrawFieldType.RADIO_BUTTON_COLOR
)

internal fun resolveBorderColor(semanticsNode: SemanticsNode): Long? =
resolveReflectedProperty(
resolveOnDrawProperty(
semanticsNode,
CheckmarkFieldType.BORDER_COLOR
OnDrawFieldType.BORDER_COLOR
)

private fun shrinkInnerBounds(
Expand Down Expand Up @@ -338,20 +344,23 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref
}
}

private fun resolveReflectedProperty(semanticsNode: SemanticsNode, fieldType: CheckmarkFieldType): Long? {
private fun resolveOnDrawProperty(semanticsNode: SemanticsNode, fieldType: OnDrawFieldType): Long? {
val onDrawInstance = resolveOnDrawInstance(semanticsNode)

val color = onDrawInstance?.let {
when (fieldType) {
CheckmarkFieldType.FILL_COLOR -> {
OnDrawFieldType.FILL_COLOR -> {
reflectionUtils.getBoxColor(onDrawInstance)
}
CheckmarkFieldType.CHECKMARK_COLOR -> {
OnDrawFieldType.CHECKMARK_COLOR -> {
reflectionUtils.getCheckColor(onDrawInstance)
}
CheckmarkFieldType.BORDER_COLOR -> {
OnDrawFieldType.BORDER_COLOR -> {
reflectionUtils.getBorderColor(onDrawInstance)
}
OnDrawFieldType.RADIO_BUTTON_COLOR -> {
reflectionUtils.getRadioColor(onDrawInstance)
}
}
}

Expand All @@ -361,11 +370,14 @@ internal class SemanticsUtils(private val reflectionUtils: ReflectionUtils = Ref
}

internal companion object {
internal enum class CheckmarkFieldType {
internal enum class OnDrawFieldType {
FILL_COLOR,
CHECKMARK_COLOR,
BORDER_COLOR
BORDER_COLOR,
RADIO_BUTTON_COLOR
}
internal const val DEFAULT_COLOR_BLACK = "#000000FF"
internal const val DEFAULT_COLOR_WHITE = "#FFFFFFFF"
}

internal fun getProgressBarRangeInfo(semanticsNode: SemanticsNode): ProgressBarRangeInfo? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import com.datadog.android.sessionreplay.compose.internal.data.UiContext
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.CheckboxSemanticsNodeMapper.Companion.BOX_BORDER_WIDTH_DP
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.CheckboxSemanticsNodeMapper.Companion.CHECKBOX_CORNER_RADIUS
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.CheckboxSemanticsNodeMapper.Companion.CHECKBOX_SIZE_DP
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.CheckboxSemanticsNodeMapper.Companion.DEFAULT_COLOR_BLACK
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.CheckboxSemanticsNodeMapper.Companion.DEFAULT_COLOR_WHITE
import com.datadog.android.sessionreplay.compose.internal.mappers.semantics.CheckboxSemanticsNodeMapper.Companion.STROKE_WIDTH_DP
import com.datadog.android.sessionreplay.compose.internal.utils.ColorUtils
import com.datadog.android.sessionreplay.compose.internal.utils.PathUtils
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils.Companion.DEFAULT_COLOR_BLACK
import com.datadog.android.sessionreplay.compose.internal.utils.SemanticsUtils.Companion.DEFAULT_COLOR_WHITE
import com.datadog.android.sessionreplay.compose.test.elmyr.SessionReplayComposeForgeConfigurator
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
Expand Down Expand Up @@ -269,7 +269,7 @@ internal class CheckboxSemanticsNodeMapperTest : AbstractSemanticsNodeMapperTest
// Then
val foregroundWireframe = semanticsWireframe.wireframes[1] as MobileSegment.Wireframe.ShapeWireframe
val expectedShapeStyle = MobileSegment.ShapeStyle(
backgroundColor = DEFAULT_COLOR_WHITE,
backgroundColor = DEFAULT_COLOR_BLACK,
opacity = 1f,
cornerRadius = CHECKBOX_CORNER_RADIUS
)
Expand Down
Loading

0 comments on commit 67331c3

Please sign in to comment.