diff --git a/demo-app/README.md b/demo-app/README.md index 4e777e3e..32ebdd7c 100644 --- a/demo-app/README.md +++ b/demo-app/README.md @@ -49,7 +49,7 @@ The OpenTelemetry Android Demo App currently supports the following features: - Automatically detects instances of slow rendering within the app. - Slow render events are captured as spans, providing information on when and where rendering delays occurred. - The span includes attributes such as `activity.name`, `screen.name`, `count`, and network details to help diagnose performance issues. - - Note: The app currently does not have any features designed to intentionally trigger slow rendering. + - To trigger a slowly rendering animation in the demo app, add any quantity of The Comet Book (the last product on the product list) to the cart. Note that the number of `slow-render` spans and their respective `count` attributes may vary between runs or across different machines. * Manual Instrumentation - Provides access to the OpenTelemetry APIs for manual instrumentation, allowing developers to create custom spans and events as needed. diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/components/SlowCometAnimation.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/components/SlowCometAnimation.kt new file mode 100644 index 00000000..a2ffb85e --- /dev/null +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/components/SlowCometAnimation.kt @@ -0,0 +1,185 @@ +package io.opentelemetry.android.demo.shop.ui.components + +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.zIndex +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.security.SecureRandom + +@Composable +fun SlowCometAnimation(modifier: Modifier = Modifier) { + val cometParams = listOf( + CometParams(startX = 0f, startY = 0f, targetX = 1.3f, targetY = 1.4f), + CometParams(startX = 0.2f, startY = 0f, targetX = 1.4f, targetY = 0.9f), + CometParams(startX = 0.2f, startY = -0.1f, targetX = 1.5f, targetY = 0.8f), + CometParams(startX = 0f, startY = 0.6f, targetX = 1.2f, targetY = 1.7f), + CometParams(startX = -0.2f, startY = 0.1f, targetX = 1.4f, targetY = 1.3f), + CometParams(startX = 0.5f, startY = 0f, targetX = 1.7f, targetY = 1.3f), + CometParams(startX = 0f, startY = 0f, targetX = 1.3f, targetY = 1.4f), + CometParams(startX = 0.2f, startY = 0f, targetX = 1.4f, targetY = 1.3f), + CometParams(startX = 0f, startY = 0f, targetX = 1.4f, targetY = 1.3f), + CometParams(startX = 0.2f, startY = -0.1f, targetX = 1.5f, targetY = 0.7f), + CometParams(startX = 0f, startY = 0.6f, targetX = 1.2f, targetY = 1.7f), + CometParams(startX = -0.2f, startY = 0.1f, targetX = 1.3f, targetY = 1.4f), + ) + + val cometVisibility = remember { mutableStateListOf().apply { addAll(List(cometParams.size) { false }) } } + + LaunchedEffect(Unit) { + cometParams.forEachIndexed { index, _ -> + launch { + delay(index * 500L) + cometVisibility[index] = true + } + } + } + + cometParams.forEachIndexed { index, params -> + if (cometVisibility[index]) { + SlowSingleCometAnimation( + startX = params.startX, + startY = params.startY, + targetX = params.targetX, + targetY = params.targetY, + modifier = modifier + ) + } + } +} + +@Composable +fun SlowSingleCometAnimation( + startX: Float, + startY: Float, + targetX: Float, + targetY: Float, + modifier: Modifier = Modifier +) { + val scope = rememberCoroutineScope() + + val tailPositions = remember { mutableStateListOf() } + + val animatedX = remember { Animatable(startX) } + val animatedY = remember { Animatable(startY) } + + LaunchedEffect(Unit) { + scope.launch { + launch { + delay(500) + animatedX.animateTo( + targetValue = targetX, + animationSpec = tween(durationMillis = 3000, easing = LinearEasing) + ) + } + launch { + delay(500) + animatedY.animateTo( + targetValue = targetY, + animationSpec = tween(durationMillis = 3000, easing = LinearEasing) + ) + } + } + } + + LaunchedEffect(animatedX.value, animatedY.value) { + val currentPosition = Offset(animatedX.value, animatedY.value) + tailPositions.add(currentPosition) + + if (tailPositions.size > 30) { + tailPositions.removeAt(0) + } + } + + val random = SecureRandom() + repeat(10_000) { + random.nextFloat() + } + + ExpensiveCometShape( + modifier = modifier, + headX = animatedX.value, + headY = animatedY.value, + tailPositions = tailPositions + ) +} + +@Composable +fun ExpensiveCometShape( + modifier: Modifier = Modifier, + headX: Float = 0.8f, + headY: Float = 0.3f, + tailPositions: List = emptyList() +) { + Canvas(modifier = modifier) { + val headRadius = size.minDimension * 0.05f + val animatedHeadX = size.width * headX + val animatedHeadY = size.height * headY + + + for (i in tailPositions.indices) { + val position = tailPositions[i] + val posX = size.width * position.x + val posY = size.height * position.y + + val progress = i / tailPositions.size.toFloat() + val tailRadius = headRadius * (1 - progress) + val tailAlpha = 0.3f * (1 - progress) + + repeat(100) { + drawCircle( + color = Color(0xFFFFD700).copy(alpha = tailAlpha), + radius = tailRadius, + center = Offset(posX, posY) + ) + } + } + + repeat(100) { + drawCircle( + color = Color(0x80FFFF00), + radius = headRadius * 2, + center = Offset(animatedHeadX, animatedHeadY) + ) + drawCircle( + color = Color(0xAAFFA500), + radius = headRadius * 1.5f, + center = Offset(animatedHeadX, animatedHeadY) + ) + drawCircle( + color = Color(0xFFFFD700), + radius = headRadius, + center = Offset(animatedHeadX, animatedHeadY) + ) + drawCircle( + color = Color.White, + radius = headRadius * 0.6f, + center = Offset(animatedHeadX, animatedHeadY) + ) + } + } +} + + +@Preview(showBackground = true) +@Composable +fun PreviewCometAnimation() { + SlowCometAnimation( + Modifier + .fillMaxSize() + .zIndex(1f) + ) +} + +data class CometParams( + val startX: Float, + val startY: Float, + val targetX: Float, + val targetY: Float +) \ No newline at end of file diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/products/ProductDetails.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/products/ProductDetails.kt index 0207c71f..ced9be44 100644 --- a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/products/ProductDetails.kt +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/products/ProductDetails.kt @@ -21,8 +21,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel import io.opentelemetry.android.demo.shop.ui.cart.CartViewModel import io.opentelemetry.android.demo.shop.ui.components.UpPressButton import androidx.compose.ui.Alignment +import androidx.compose.ui.zIndex import io.opentelemetry.android.demo.shop.clients.ProductCatalogClient import io.opentelemetry.android.demo.shop.clients.RecommendationService +import io.opentelemetry.android.demo.shop.ui.components.SlowCometAnimation import io.opentelemetry.android.demo.shop.ui.components.ConfirmCrashPopup import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -39,6 +41,8 @@ fun ProductDetails( val sourceProductImage = imageLoader.load(product.picture) var quantity by remember { mutableIntStateOf(1) } + var slowRender by remember { mutableStateOf(false) } + val productsClient = ProductCatalogClient(context) val recommendationService = remember { RecommendationService(productsClient, cartViewModel) } val recommendedProducts = remember { recommendationService.getRecommendedProducts(product) } @@ -86,7 +90,11 @@ fun ProductDetails( Spacer(modifier = Modifier.height(32.dp)) QuantityChooser(quantity = quantity, onQuantityChange = { quantity = it }) Spacer(modifier = Modifier.height(16.dp)) - AddToCartButton(cartViewModel = cartViewModel, product = product, quantity = quantity) + AddToCartButton( + cartViewModel = cartViewModel, + product = product, + quantity = quantity, + onSlowRenderChange = { slowRender = it }) Spacer(modifier = Modifier.height(32.dp)) RecommendedSection(recommendedProducts = recommendedProducts, onProductClick = onProductClick) } @@ -97,6 +105,13 @@ fun ProductDetails( .align(Alignment.TopStart) .padding(8.dp) ) + if (slowRender) { + SlowCometAnimation( + modifier = Modifier + .fillMaxSize() + .zIndex(1f) + ) + } } } @@ -104,9 +119,9 @@ fun ProductDetails( fun AddToCartButton( cartViewModel: CartViewModel, product: Product, - quantity: Int + quantity: Int, + onSlowRenderChange: (Boolean) -> Unit ) { - var showPopup by remember { mutableStateOf(false) } Button( @@ -114,6 +129,9 @@ fun AddToCartButton( if (product.id == "OLJCESPC7Z" && quantity == 10) { showPopup = true } else { + if (product.id == "HQTGWGPNH4") { + onSlowRenderChange(true) + } cartViewModel.addProduct(product, quantity) } }, @@ -161,7 +179,3 @@ fun multiThreadCrashing(numThreads : Int = 4) { } latch.countDown() } - - - -