Skip to content

Commit

Permalink
RUM-6200: Add Tab semantics mapper
Browse files Browse the repository at this point in the history
  • Loading branch information
ambushwork committed Nov 6, 2024
1 parent cd1b01e commit c9a142e
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal class SemanticsWireframeMapper(
private val semanticsUtils: SemanticsUtils = SemanticsUtils(),
private val semanticsNodeMapper: Map<Role, SemanticsNodeMapper> = mapOf(
// TODO RUM-6189 Add Mappers for each Semantics Role
Role.Tab to TabSemanticsNodeMapper(colorStringFormatter, semanticsUtils),
Role.Button to ButtonSemanticsNodeMapper(colorStringFormatter, semanticsUtils),
Role.Image to ImageSemanticsNodeMapper(colorStringFormatter)
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

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

import androidx.compose.ui.semantics.SemanticsNode
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.SemanticsUtils
import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback
import com.datadog.android.sessionreplay.utils.ColorStringFormatter

internal class TabSemanticsNodeMapper(
colorStringFormatter: ColorStringFormatter,
private val semanticsUtils: SemanticsUtils = SemanticsUtils()
) : AbstractSemanticsNodeMapper(colorStringFormatter, semanticsUtils) {

override fun map(
semanticsNode: SemanticsNode,
parentContext: UiContext,
asyncJobStatusCallback: AsyncJobStatusCallback
): SemanticsWireframe {
val parentFrames = resolveParentColor(semanticsNode)
return SemanticsWireframe(
wireframes = parentFrames,
uiContext = parentContext
)
}

private fun resolveParentColor(semanticsNode: SemanticsNode): List<MobileSegment.Wireframe> {
val globalBounds = resolveBounds(semanticsNode)

// TODO RUM-7082: Consider use `UiContext` to pass the color information
var parentColor = semanticsNode.parent?.let { parent ->
semanticsUtils.resolveBackgroundColor(parent)?.let {
convertColor(it)
}
}

// If parent color is not specified, it may be in the grandparent modifier info.
if (parentColor == null) {
parentColor = semanticsNode.parent?.parent?.let { grandParentNode ->
semanticsUtils.resolveBackgroundColor(grandParentNode)?.let {
convertColor(it)
}
}
}

val shapeStyle = MobileSegment.ShapeStyle(backgroundColor = parentColor)
return MobileSegment.Wireframe.ShapeWireframe(
id = semanticsNode.id.toLong(),
x = globalBounds.x,
y = globalBounds.y,
width = globalBounds.width,
height = globalBounds.height,
shapeStyle = shapeStyle
).let { listOf(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ internal class SemanticsUtils {
return backgroundInfoList
}

internal fun resolveBackgroundColor(semanticsNode: SemanticsNode): Long? {
val backgroundModifierInfo =
semanticsNode.layoutInfo.getModifierInfo().firstOrNull { modifierInfo ->
ComposeReflection.BackgroundElementClass?.isInstance(modifierInfo.modifier) == true
}
return backgroundModifierInfo?.let {
ComposeReflection.ColorField?.getSafe(it.modifier) as? Long
}
}

internal fun resolveBackgroundInfoId(backgroundInfo: BackgroundInfo): Long {
return System.identityHashCode(backgroundInfo).toLong()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import androidx.navigation.compose.composable
internal fun SampleSelectionScreen(
onTypographyClicked: () -> Unit,
onLegacyClicked: () -> Unit,
onImageClicked: () -> Unit
onImageClicked: () -> Unit,
onTabsClicked: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
Expand All @@ -45,6 +46,10 @@ internal fun SampleSelectionScreen(
text = "Image Sample",
onClick = onImageClicked
)
StyledButton(
text = "Tabs Sample",
onClick = onTabsClicked
)
StyledButton(
text = "Legacy Sample",
onClick = onLegacyClicked
Expand Down Expand Up @@ -76,6 +81,9 @@ internal fun NavGraphBuilder.selectionNavigation(navController: NavHostControlle
onImageClicked = {
navController.navigate(SampleScreen.Image.navigationRoute)
},
onTabsClicked = {
navController.navigate(SampleScreen.Tabs.navigationRoute)
},
onLegacyClicked = {
navController.navigate(SampleScreen.Legacy.navigationRoute)
}
Expand All @@ -90,6 +98,10 @@ internal fun NavGraphBuilder.selectionNavigation(navController: NavHostControlle
ImageSample()
}

composable(SampleScreen.Tabs.navigationRoute) {
TabsSample()
}

activity(SampleScreen.Legacy.navigationRoute) {
activityClass = LegacyComposeActivity::class
}
Expand All @@ -102,6 +114,7 @@ internal sealed class SampleScreen(
object Root : SampleScreen(COMPOSE_ROOT)
object Typography : SampleScreen("$COMPOSE_ROOT/typography")
object Image : SampleScreen("$COMPOSE_ROOT/image")
object Tabs : SampleScreen("$COMPOSE_ROOT/tabs")
object Legacy : SampleScreen("$COMPOSE_ROOT/legacy")

companion object {
Expand All @@ -119,6 +132,8 @@ private fun PreviewSampleSelectionScreen() {
onImageClicked = {
},
onTypographyClicked = {
},
onTabsClicked = {
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sample.compose

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.material.Tab
import androidx.compose.material.TabRow
import androidx.compose.material.TabRowDefaults
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Email
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.datadog.android.rum.GlobalRumMonitor
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch

@Composable
fun TabsSample() {
Scaffold(
bottomBar = {
NavigationBar()
}
) {
AppContent(modifier = Modifier.padding(it))
}
}

@Composable
@OptIn(ExperimentalPagerApi::class)
@Suppress("LongMethod")
private fun AppContent(modifier: Modifier = Modifier) {
Column(modifier) {
val pages = remember {
listOf(Page.Navigation, Page.Interactions)
}
val pagerState = rememberPagerState()
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
val rumMonitor = GlobalRumMonitor.get()
val screen = pages[pagerState.currentPage].trackingName
if (event == Lifecycle.Event.ON_RESUME) {
rumMonitor.startView(screen, screen)
} else if (event == Lifecycle.Event.ON_PAUSE) {
rumMonitor.stopView(screen)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}

LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }
// drop 1st, because it will be tracked by the lifecycle
.drop(1)
.collect { page ->
val screen = pages[page].trackingName
GlobalRumMonitor.get().startView(screen, screen)
}
}

TabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier.pagerTabIndicatorOffset(
pagerState,
tabPositions
),
height = TabRowDefaults.IndicatorHeight * 2
)
}
) {
val coroutineScope = rememberCoroutineScope()
pages.forEachIndexed { index, page ->
Tab(
text = { Text(page.name) },
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
}
)
}
}
HorizontalPager(
count = pages.size,
state = pagerState
) { page ->
when (page) {
0 -> NavigationSampleView()
else -> InteractionSampleView()
}
}
}
}

@Composable
private fun NavigationBar() {
val selectedIndex = remember { mutableIntStateOf(1) }
BottomNavigation {
BottomNavigationItem(
selected = selectedIndex.intValue == 1,
onClick = {
selectedIndex.intValue = 1
},
icon = {
Icon(imageVector = Icons.Filled.Edit, contentDescription = "edit")
},
label = {
Text("label 1")
}
)

BottomNavigationItem(
selected = selectedIndex.intValue == 2,
onClick = {
selectedIndex.intValue = 2
},
icon = {
Icon(imageVector = Icons.Filled.Email, contentDescription = "mail")
},
label = {
Text("label 2")
}
)
}
}

0 comments on commit c9a142e

Please sign in to comment.