diff --git a/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/LocalFileTransferScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/LocalFileTransferScreen.kt index 4561a6e85..7814e00e6 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/LocalFileTransferScreen.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/LocalFileTransferScreen.kt @@ -23,7 +23,6 @@ import android.net.wifi.p2p.WifiP2pDevice import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize @@ -69,10 +68,12 @@ import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem import org.kiwix.kiwixmobile.core.ui.theme.DodgerBlue import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIFTEEN_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_FOR_TRANSFER_SHOW_CASE_VIEW_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_FOR_TRANSFER_TEXT_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_ITEM_ICON_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_ITEM_TEXT_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NEARBY_DEVICES_SHOW_CASE_VIEW_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NEARBY_DEVICES_TEXT_SIZE import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NEARBY_DEVICE_LIST_HEIGHT import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NO_DEVICE_FOUND_TEXT_PADDING @@ -128,34 +129,29 @@ fun LocalFileTransferScreen( ) } ) { padding -> - Box( + Column( modifier = Modifier .fillMaxSize() .padding(padding) + .background(Color.Transparent) ) { - Column( + YourDeviceHeader(deviceName, context, targets) + HorizontalDivider( + color = DodgerBlue, + thickness = ONE_DP, + modifier = Modifier.padding(horizontal = FIVE_DP) + ) + NearbyDevicesSection(peerDeviceList, isPeerSearching, onDeviceItemClick, context, targets) + HorizontalDivider( + color = DodgerBlue, + thickness = ONE_DP, modifier = Modifier - .fillMaxSize() - .background(Color.Transparent) - ) { - YourDeviceHeader(deviceName, context, targets) - HorizontalDivider( - color = DodgerBlue, - thickness = ONE_DP, - modifier = Modifier.padding(horizontal = FIVE_DP) - ) - NearbyDevicesSection(peerDeviceList, isPeerSearching, onDeviceItemClick, context, targets) - HorizontalDivider( - color = DodgerBlue, - thickness = ONE_DP, - modifier = Modifier - .padding(horizontal = FIVE_DP) - ) - TransferFilesSection(transferFileList, context, targets) - } - ShowShowCaseToUserIfNotShown(targets, sharedPreferenceUtil) + .padding(horizontal = FIVE_DP) + ) + TransferFilesSection(transferFileList, context, targets) } } + ShowShowCaseToUserIfNotShown(targets, sharedPreferenceUtil) } } @@ -212,7 +208,8 @@ fun NearbyDevicesSection( targets[PEER_DEVICE_LIST_SHOW_CASE_TAG] = ShowcaseProperty( index = 2, coordinates = coordinates, - showCaseMessage = context.getString(string.transfer_zim_files_list_message) + showCaseMessage = context.getString(string.nearby_devices_list_message), + customSizeForShowcaseViewCircle = NEARBY_DEVICES_SHOW_CASE_VIEW_SIZE ) }, textAlign = TextAlign.Center, @@ -250,7 +247,8 @@ private fun TransferFilesSection( targets[FILE_FOR_TRANSFER_SHOW_CASE_TAG] = ShowcaseProperty( index = 3, coordinates = coordinates, - showCaseMessage = context.getString(string.transfer_zim_files_list_message) + showCaseMessage = context.getString(string.transfer_zim_files_list_message), + customSizeForShowcaseViewCircle = FILE_FOR_TRANSFER_SHOW_CASE_VIEW_SIZE ) }, textAlign = TextAlign.Center, @@ -277,7 +275,14 @@ private fun YourDeviceHeader( fontStyle = FontStyle.Italic, fontSize = YOUR_DEVICE_TEXT_SIZE, modifier = Modifier - .padding(top = FIVE_DP, bottom = ONE_DP), + .padding(top = FIVE_DP, bottom = ONE_DP) + .onGloballyPositioned { coordinates -> + targets[YOUR_DEVICE_SHOW_CASE_TAG] = ShowcaseProperty( + index = 1, + coordinates = coordinates, + showCaseMessage = context.getString(string.your_device_name_message) + ) + }, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f) ) val contentDescription = stringResource(R.string.device_name) @@ -287,14 +292,7 @@ private fun YourDeviceHeader( fontSize = PEER_DEVICE_ITEM_TEXT_SIZE, modifier = Modifier .minimumInteractiveComponentSize() - .semantics { this.contentDescription = contentDescription } - .onGloballyPositioned { coordinates -> - targets[YOUR_DEVICE_SHOW_CASE_TAG] = ShowcaseProperty( - index = 1, - coordinates = coordinates, - showCaseMessage = context.getString(string.your_device_name_message) - ) - }, + .semantics { this.contentDescription = contentDescription }, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f) ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixShowCaseView.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixShowCaseView.kt index 51956924a..fd9df418b 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixShowCaseView.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixShowCaseView.kt @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.core.ui.components +import android.annotation.SuppressLint import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.RepeatMode @@ -25,12 +26,16 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -38,264 +43,245 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInRoot -import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.IntOffset +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.ui.theme.CornflowerBlue +import org.kiwix.kiwixmobile.core.ui.theme.White +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PULSE_ALPHA +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PULSE_ANIMATION_END +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PULSE_ANIMATION_START +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.PULSE_RADIUS_EXTRA +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_MESSAGE_SHADOW_BLUR_RADIUS +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_VIEW_MESSAGE_TEXT_SIZE +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SHOWCASE_VIEW_NEXT_BUTTON_TEXT_SIZE +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP import kotlin.math.max -import kotlin.math.min -import kotlin.math.pow -import kotlin.math.sqrt +import kotlin.math.roundToInt + +const val SHOWCASE_VIEW_ROUND_ANIMATION_DURATION = 2000 +const val ONE = 1 +const val TWO = 1 +const val SIXTEEN = 16 @Composable fun KiwixShowCaseView( targets: SnapshotStateMap, onShowCaseCompleted: () -> Unit ) { - val uniqueTargets = targets.values.sortedBy { it.index } - var currentTargetIndex by remember { mutableStateOf(0) } - val currentTarget = if (uniqueTargets.isNotEmpty() && currentTargetIndex < uniqueTargets.size) { - uniqueTargets[currentTargetIndex] - } else { - null - } + val orderedTargets = targets.values.sortedBy { it.index } + var currentIndex by remember { mutableStateOf(ZERO) } + val currentTarget = orderedTargets.getOrNull(currentIndex) currentTarget?.let { - AnimatedShowCase(targets = it) { - if (++currentTargetIndex >= uniqueTargets.size) { - onShowCaseCompleted() - } + AnimatedShowCase(target = it) { + currentIndex++ + if (currentIndex >= orderedTargets.size) onShowCaseCompleted() } } } -@Suppress("LongMethod", "MagicNumber") @Composable -fun AnimatedShowCase( - targets: ShowcaseProperty, +private fun AnimatedShowCase( + target: ShowcaseProperty, onShowCaseCompleted: () -> Unit ) { - val targetRect = targets.coordinates.boundsInRoot() - val targetRadius = targetRect.maxDimension / 2f + 20 + val targetRect = target.coordinates.boundsInRoot() + val innerAnimation = remember { Animatable(PULSE_ANIMATION_START) } + val density = LocalDensity.current - // Animation setup for rounded animation - val animationSpec = infiniteRepeatable( - animation = tween(2000, easing = FastOutLinearInEasing), - repeatMode = RepeatMode.Reverse - ) - val animaTable = remember { Animatable(0f) } - - LaunchedEffect(animaTable) { - animaTable.animateTo(1f, animationSpec = animationSpec) + val (width, height) = with(density) { + val size = target.customSizeForShowcaseViewCircle?.toPx() + Pair(size ?: targetRect.width, size ?: targetRect.height) } - val outerAnimaTable = remember { Animatable(0.6f) } + val radiusBase = max(width, height) / TWO.toFloat() + val pulseRadius by innerAnimation.asState() - LaunchedEffect(targets) { - outerAnimaTable.snapTo(0.6f) - outerAnimaTable.animateTo( - targetValue = 1f, - animationSpec = tween(500) + LaunchedEffect(Unit) { + innerAnimation.animateTo( + targetValue = PULSE_ANIMATION_END, + animationSpec = infiniteRepeatable( + animation = tween(SHOWCASE_VIEW_ROUND_ANIMATION_DURATION, easing = FastOutLinearInEasing), + repeatMode = RepeatMode.Restart + ) ) } - // Map animation to y position of the components - val dys = animaTable.value - - // Text coordinates and outer radius - var textCoordinate: LayoutCoordinates? by remember { mutableStateOf(null) } - var outerRadius by remember { mutableStateOf(0f) } - val screenHeight = LocalConfiguration.current.screenHeightDp - val textYOffset = with(LocalDensity.current) { - targets.coordinates.positionInRoot().y.toDp() - } - var outerOffset by remember { mutableStateOf(Offset(0f, 0f)) } - - textCoordinate?.let { - val textRect = it.boundsInRoot() - val textHeight = it.size.height - val isInGutter = textYOffset > screenHeight.dp - outerOffset = getOuterCircleCenter(targetRect, textRect, targetRadius, textHeight, isInGutter) - outerRadius = getOuterRadius(textRect, targetRect) + targetRadius - } - Canvas( modifier = Modifier .fillMaxSize() - .pointerInput(targets) { - detectTapGestures { tapOffset -> - if (targetRect.contains(tapOffset)) { - onShowCaseCompleted() - } + .pointerInput(target) { + detectTapGestures { + if (targetRect.contains(it)) onShowCaseCompleted() } } - .graphicsLayer(alpha = 0.99f) + .graphicsLayer(alpha = PULSE_ALPHA) ) { - // Animated Rounded ShowCaseView - drawRect( - color = CornflowerBlue.copy(alpha = 0.8f), - size = size - ) - // draw circle with animation - drawCircle( - color = Color.White, - radius = targetRect.maxDimension * dys * 2f, - center = targetRect.center, - alpha = 1 - dys - ) - drawCircle( - color = Color.White, - radius = targetRadius, - center = targetRect.center, - blendMode = BlendMode.Clear - ) + drawOverlay(targetRect, radiusBase, pulseRadius) } - ShowText(currentTarget = targets, targetRect = targetRect, targetRadius = targetRadius) { - textCoordinate = it - } - - // Next Button at the bottom center - NextButton(onShowCaseCompleted = onShowCaseCompleted) + ShowCaseMessage(target, targetRect, radiusBase) + NextButton(onShowCaseCompleted) } +/** + * Draws the overlay and animated spotlight. + */ +private fun DrawScope.drawOverlay( + targetRect: Rect, + baseRadius: Float, + animatedFraction: Float +) { + drawRect(color = CornflowerBlue.copy(alpha = SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA), size = size) + drawCircle( + color = Color.White, + radius = baseRadius * (ONE + animatedFraction), + center = targetRect.center, + alpha = ONE - animatedFraction + ) + drawCircle( + color = Color.White, + radius = baseRadius + PULSE_RADIUS_EXTRA, + center = targetRect.center, + blendMode = BlendMode.Clear + ) +} + +@SuppressLint("UnusedBoxWithConstraintsScope") @Composable -fun NextButton(onShowCaseCompleted: () -> Unit) { +private fun ShowCaseMessage( + target: ShowcaseProperty, + targetRect: Rect, + targetRadius: Float +) { + val density = LocalDensity.current + var offset by remember { mutableStateOf(Offset.Zero) } + var calculated by remember { mutableStateOf(false) } + + BoxWithConstraints(Modifier.fillMaxSize()) { + val screenWidth = with(density) { maxWidth.toPx() } + val screenHeight = with(density) { maxHeight.toPx() } + + if (calculated) { + Box( + modifier = Modifier.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) } + ) { + Text( + text = target.showCaseMessage, + color = target.showCaseMessageColor, + style = TextStyle( + fontSize = SHOWCASE_VIEW_MESSAGE_TEXT_SIZE, + shadow = Shadow( + Color.Black.copy(alpha = SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA), + defaultBlurOffsetForMessageAndNextButton(), + blurRadius = SHOWCASE_MESSAGE_SHADOW_BLUR_RADIUS + ) + ) + ) + } + } + + Text( + text = target.showCaseMessage, + modifier = Modifier + .alpha(PULSE_ANIMATION_START) + .onGloballyPositioned { + val size = it.size + val width = size.width.toFloat() + val height = size.height.toFloat() + val center = targetRect.center + + val posY = when { + screenHeight - (center.y + targetRadius) > height + SIXTEEN -> center.y + targetRadius + SIXTEEN + center.y - targetRadius > height + SIXTEEN -> center.y - targetRadius - height - SIXTEEN + else -> screenHeight / TWO - height / TWO + } + + val posX = when { + screenWidth - targetRect.right > width + SIXTEEN -> targetRect.right + SIXTEEN + targetRect.left > width + SIXTEEN -> targetRect.left - width - SIXTEEN + else -> screenWidth / TWO - width / TWO + } + + offset = Offset(posX, posY) + calculated = true + } + ) + } +} + +private fun defaultBlurOffsetForMessageAndNextButton() = + Offset(SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA, SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA) + +/** + * Composable for the "Next" button in the showcase. + */ +@Composable +private fun NextButton(onClick: () -> Unit) { + val context = LocalContext.current Column( modifier = Modifier .fillMaxSize() - .padding(16.dp), - verticalArrangement = androidx.compose.foundation.layout.Arrangement.Bottom, - horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally + .padding(SIXTEEN_DP), + verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.End ) { - androidx.compose.material3.Button( - onClick = { - onShowCaseCompleted() - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Next", fontWeight = FontWeight.Bold) + TextButton(onClick = onClick) { + Text( + text = context.getString(R.string.next), + style = LocalTextStyle.current.copy( + fontSize = SHOWCASE_VIEW_NEXT_BUTTON_TEXT_SIZE, + fontWeight = FontWeight.Bold, + color = White, + shadow = Shadow( + Color.Black.copy(alpha = SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA), + defaultBlurOffsetForMessageAndNextButton(), + blurRadius = SHOWCASE_MESSAGE_SHADOW_BLUR_RADIUS + ) + ) + ) } } } -@Suppress("MagicNumber") -@Composable -fun ShowText( - currentTarget: ShowcaseProperty, - targetRect: Rect, - targetRadius: Float, - updateCoordinates: (LayoutCoordinates) -> Unit -) { - var txtOffsetY by remember { mutableStateOf(0f) } - var txtOffsetX by remember { mutableStateOf(0f) } - var txtRightOffSet by remember { mutableStateOf(0f) } - val configuration = LocalConfiguration.current - val screenWidth = configuration.screenWidthDp.toFloat() - - Column( - modifier = Modifier - .offset( - x = with(LocalDensity.current) { txtOffsetX.toDp() }, - y = with(LocalDensity.current) { txtOffsetY.toDp() } - ) - .onGloballyPositioned { - updateCoordinates(it) - val textHeight = it.size.height - val possibleTop = - targetRect.center.y - targetRadius - textHeight - val possibleLeft = targetRect.topLeft.x - txtOffsetY = if (possibleTop > 0) { - possibleTop - } else { - targetRect.center.y + targetRadius - 140 - } - txtRightOffSet = it.boundsInRoot().topRight.x - txtOffsetX = it.boundsInRoot().topRight.x - it.size.width - txtOffsetX = if (possibleLeft >= screenWidth / 2) { - screenWidth / 2 + targetRadius - } else { - possibleLeft - } - txtRightOffSet += targetRadius - } - .padding(2.dp) - ) { - Text( - text = currentTarget.showCaseMessage, - fontSize = 14.sp, - color = currentTarget.showCaseMessageColor - ) - } -} - -fun getOuterCircleCenter( - targetRect: Rect, - textRect: Rect, - targetRadius: Float, - textHeight: Int, - isInGutter: Boolean -): Offset { - val outerCenterX: Float - var outerCenterY: Float - - val onTop = targetRect.center.y - targetRadius - textHeight > 0 - val left = min(textRect.left, targetRect.left - targetRadius) - val right = max(textRect.right, targetRect.right + targetRadius) - - val centerY = if (onTop) { - targetRect.center.y - targetRadius - textHeight - } else { - targetRect.center.y + targetRadius + textHeight - } - - outerCenterY = centerY - outerCenterX = (left + right) / 2 - - // If the text is in the gutter, adjust the vertical position - if (isInGutter) { - outerCenterY = targetRect.center.y - } - - return Offset(outerCenterX, outerCenterY) -} - -fun getOuterRadius(textRect: Rect, targetRect: Rect): Float { - // Get outer rect that covers both target and text rect - val topLeftX = min(textRect.topLeft.x, targetRect.topLeft.x) - val topLeftY = min(textRect.topLeft.y, targetRect.topLeft.y) - val bottomRightX = max(textRect.bottomRight.x, targetRect.bottomRight.x) - val bottomRightY = max(textRect.bottomRight.y, targetRect.bottomRight.y) - val newBounds = Rect(topLeftX, topLeftY, bottomRightX, bottomRightY) - - // Calculate the diagonal distance of the new bounding box - val distance = - sqrt(newBounds.height.toDouble().pow(2.0) + newBounds.width.toDouble().pow(2.0)).toFloat() - - // Return the radius (half of the diagonal distance) - return (distance / 2f) -} - +/** + * Represents a single item in the showcase view sequence. + * + * @param index The order in which this target should be shown in the showcase flow. + * @param coordinates Layout coordinates used to determine position and size of the target view on screen. + * @param showCaseMessage Message to be displayed near the highlighted target. + * @param showCaseMessageColor Optional color for the message text (default is white). + * @param blurOpacity Controls the opacity of the background overlay behind the highlight (default is 0.8). + * @param customSizeForShowcaseViewCircle Optional custom size for the radius of the highlight circle. + * If null, it uses the size of the target's bounds. + */ data class ShowcaseProperty( val index: Int, val coordinates: LayoutCoordinates, val showCaseMessage: String, val showCaseMessageColor: Color = Color.White, - val blurOpacity: Float = 0.8f, - val customWidth: Dp? = null, - val customHeight: Dp? = null, + val blurOpacity: Float = SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA, + val customSizeForShowcaseViewCircle: Dp? = null, ) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt index 53023e7a3..54d88c705 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt @@ -123,4 +123,17 @@ object ComposeDimens { val YOUR_DEVICE_TEXT_SIZE = 13.sp val FILE_FOR_TRANSFER_TEXT_SIZE = 16.sp val NEARBY_DEVICES_TEXT_SIZE = 16.sp + + // KiwixShowCase view dimens + val SHOWCASE_VIEW_MESSAGE_TEXT_SIZE = 14.sp + val SHOWCASE_VIEW_NEXT_BUTTON_TEXT_SIZE = 16.sp + val FILE_FOR_TRANSFER_SHOW_CASE_VIEW_SIZE = 100.dp + val NEARBY_DEVICES_SHOW_CASE_VIEW_SIZE = 100.dp + const val SHOWCASE_MESSAGE_SHADOW_BLUR_RADIUS = 3f + const val SHOWCASE_MESSAGE_SHADOW_COLOR_ALPHA = 0.5f + const val SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA = 0.8f + const val PULSE_ANIMATION_START = 0f + const val PULSE_ANIMATION_END = 1f + const val PULSE_ALPHA = 0.99f + const val PULSE_RADIUS_EXTRA = 20f }