Refactored KiwixShowCaseView to align with app theme and improve usability.

* Added animated pulse effect to highlight the selected view.
* Introduced support for custom width/height for the showcase circle, useful for large views that might otherwise extend off-screen. Defaults to the view's dimensions if not specified.
* Enhanced ShowCaseMessage to automatically position itself based on available space. It prefers the top, falls back to the bottom if needed, and uses left/right positioning when vertical space is insufficient — ensuring it always stays within screen bounds.
This commit is contained in:
MohitMaliFtechiz 2025-04-17 15:41:27 +05:30 committed by Kelson
parent 74216e8850
commit 0a1e5f36a1
3 changed files with 230 additions and 233 deletions

View File

@ -23,7 +23,6 @@ import android.net.wifi.p2p.WifiP2pDevice
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize 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.DodgerBlue
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIFTEEN_DP 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_FOR_TRANSFER_TEXT_SIZE
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FILE_ITEM_ICON_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.FILE_ITEM_TEXT_SIZE
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP 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_DEVICES_TEXT_SIZE
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NEARBY_DEVICE_LIST_HEIGHT import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NEARBY_DEVICE_LIST_HEIGHT
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NO_DEVICE_FOUND_TEXT_PADDING import org.kiwix.kiwixmobile.core.utils.ComposeDimens.NO_DEVICE_FOUND_TEXT_PADDING
@ -128,34 +129,29 @@ fun LocalFileTransferScreen(
) )
} }
) { padding -> ) { padding ->
Box( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding) .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 modifier = Modifier
.fillMaxSize() .padding(horizontal = FIVE_DP)
.background(Color.Transparent) )
) { TransferFilesSection(transferFileList, context, targets)
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)
} }
} }
ShowShowCaseToUserIfNotShown(targets, sharedPreferenceUtil)
} }
} }
@ -212,7 +208,8 @@ fun NearbyDevicesSection(
targets[PEER_DEVICE_LIST_SHOW_CASE_TAG] = ShowcaseProperty( targets[PEER_DEVICE_LIST_SHOW_CASE_TAG] = ShowcaseProperty(
index = 2, index = 2,
coordinates = coordinates, 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, textAlign = TextAlign.Center,
@ -250,7 +247,8 @@ private fun TransferFilesSection(
targets[FILE_FOR_TRANSFER_SHOW_CASE_TAG] = ShowcaseProperty( targets[FILE_FOR_TRANSFER_SHOW_CASE_TAG] = ShowcaseProperty(
index = 3, index = 3,
coordinates = coordinates, 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, textAlign = TextAlign.Center,
@ -277,7 +275,14 @@ private fun YourDeviceHeader(
fontStyle = FontStyle.Italic, fontStyle = FontStyle.Italic,
fontSize = YOUR_DEVICE_TEXT_SIZE, fontSize = YOUR_DEVICE_TEXT_SIZE,
modifier = Modifier 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) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f)
) )
val contentDescription = stringResource(R.string.device_name) val contentDescription = stringResource(R.string.device_name)
@ -287,14 +292,7 @@ private fun YourDeviceHeader(
fontSize = PEER_DEVICE_ITEM_TEXT_SIZE, fontSize = PEER_DEVICE_ITEM_TEXT_SIZE,
modifier = Modifier modifier = Modifier
.minimumInteractiveComponentSize() .minimumInteractiveComponentSize()
.semantics { this.contentDescription = contentDescription } .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)
)
},
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f)
) )
} }

View File

@ -18,6 +18,7 @@
package org.kiwix.kiwixmobile.core.ui.components package org.kiwix.kiwixmobile.core.ui.components
import android.annotation.SuppressLint
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.RepeatMode 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.animation.core.tween
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectTapGestures 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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -38,264 +43,245 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color 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.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInRoot import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.sp 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.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.max
import kotlin.math.min import kotlin.math.roundToInt
import kotlin.math.pow
import kotlin.math.sqrt const val SHOWCASE_VIEW_ROUND_ANIMATION_DURATION = 2000
const val ONE = 1
const val TWO = 1
const val SIXTEEN = 16
@Composable @Composable
fun KiwixShowCaseView( fun KiwixShowCaseView(
targets: SnapshotStateMap<String, ShowcaseProperty>, targets: SnapshotStateMap<String, ShowcaseProperty>,
onShowCaseCompleted: () -> Unit onShowCaseCompleted: () -> Unit
) { ) {
val uniqueTargets = targets.values.sortedBy { it.index } val orderedTargets = targets.values.sortedBy { it.index }
var currentTargetIndex by remember { mutableStateOf(0) } var currentIndex by remember { mutableStateOf(ZERO) }
val currentTarget = if (uniqueTargets.isNotEmpty() && currentTargetIndex < uniqueTargets.size) { val currentTarget = orderedTargets.getOrNull(currentIndex)
uniqueTargets[currentTargetIndex]
} else {
null
}
currentTarget?.let { currentTarget?.let {
AnimatedShowCase(targets = it) { AnimatedShowCase(target = it) {
if (++currentTargetIndex >= uniqueTargets.size) { currentIndex++
onShowCaseCompleted() if (currentIndex >= orderedTargets.size) onShowCaseCompleted()
}
} }
} }
} }
@Suppress("LongMethod", "MagicNumber")
@Composable @Composable
fun AnimatedShowCase( private fun AnimatedShowCase(
targets: ShowcaseProperty, target: ShowcaseProperty,
onShowCaseCompleted: () -> Unit onShowCaseCompleted: () -> Unit
) { ) {
val targetRect = targets.coordinates.boundsInRoot() val targetRect = target.coordinates.boundsInRoot()
val targetRadius = targetRect.maxDimension / 2f + 20 val innerAnimation = remember { Animatable(PULSE_ANIMATION_START) }
val density = LocalDensity.current
// Animation setup for rounded animation val (width, height) = with(density) {
val animationSpec = infiniteRepeatable<Float>( val size = target.customSizeForShowcaseViewCircle?.toPx()
animation = tween(2000, easing = FastOutLinearInEasing), Pair(size ?: targetRect.width, size ?: targetRect.height)
repeatMode = RepeatMode.Reverse
)
val animaTable = remember { Animatable(0f) }
LaunchedEffect(animaTable) {
animaTable.animateTo(1f, animationSpec = animationSpec)
} }
val outerAnimaTable = remember { Animatable(0.6f) } val radiusBase = max(width, height) / TWO.toFloat()
val pulseRadius by innerAnimation.asState()
LaunchedEffect(targets) { LaunchedEffect(Unit) {
outerAnimaTable.snapTo(0.6f) innerAnimation.animateTo(
outerAnimaTable.animateTo( targetValue = PULSE_ANIMATION_END,
targetValue = 1f, animationSpec = infiniteRepeatable(
animationSpec = tween(500) 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( Canvas(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.pointerInput(targets) { .pointerInput(target) {
detectTapGestures { tapOffset -> detectTapGestures {
if (targetRect.contains(tapOffset)) { if (targetRect.contains(it)) onShowCaseCompleted()
onShowCaseCompleted()
}
} }
} }
.graphicsLayer(alpha = 0.99f) .graphicsLayer(alpha = PULSE_ALPHA)
) { ) {
// Animated Rounded ShowCaseView drawOverlay(targetRect, radiusBase, pulseRadius)
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
)
} }
ShowText(currentTarget = targets, targetRect = targetRect, targetRadius = targetRadius) { ShowCaseMessage(target, targetRect, radiusBase)
textCoordinate = it NextButton(onShowCaseCompleted)
}
// Next Button at the bottom center
NextButton(onShowCaseCompleted = 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 @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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp), .padding(SIXTEEN_DP),
verticalArrangement = androidx.compose.foundation.layout.Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally horizontalAlignment = Alignment.End
) { ) {
androidx.compose.material3.Button( TextButton(onClick = onClick) {
onClick = { Text(
onShowCaseCompleted() text = context.getString(R.string.next),
}, style = LocalTextStyle.current.copy(
modifier = Modifier.fillMaxWidth() fontSize = SHOWCASE_VIEW_NEXT_BUTTON_TEXT_SIZE,
) { fontWeight = FontWeight.Bold,
Text(text = "Next", 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 * Represents a single item in the showcase view sequence.
fun ShowText( *
currentTarget: ShowcaseProperty, * @param index The order in which this target should be shown in the showcase flow.
targetRect: Rect, * @param coordinates Layout coordinates used to determine position and size of the target view on screen.
targetRadius: Float, * @param showCaseMessage Message to be displayed near the highlighted target.
updateCoordinates: (LayoutCoordinates) -> Unit * @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).
var txtOffsetY by remember { mutableStateOf(0f) } * @param customSizeForShowcaseViewCircle Optional custom size for the radius of the highlight circle.
var txtOffsetX by remember { mutableStateOf(0f) } * If null, it uses the size of the target's bounds.
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)
}
data class ShowcaseProperty( data class ShowcaseProperty(
val index: Int, val index: Int,
val coordinates: LayoutCoordinates, val coordinates: LayoutCoordinates,
val showCaseMessage: String, val showCaseMessage: String,
val showCaseMessageColor: Color = Color.White, val showCaseMessageColor: Color = Color.White,
val blurOpacity: Float = 0.8f, val blurOpacity: Float = SHOWCASE_VIEW_BACKGROUND_COLOR_ALPHA,
val customWidth: Dp? = null, val customSizeForShowcaseViewCircle: Dp? = null,
val customHeight: Dp? = null,
) )

View File

@ -123,4 +123,17 @@ object ComposeDimens {
val YOUR_DEVICE_TEXT_SIZE = 13.sp val YOUR_DEVICE_TEXT_SIZE = 13.sp
val FILE_FOR_TRANSFER_TEXT_SIZE = 16.sp val FILE_FOR_TRANSFER_TEXT_SIZE = 16.sp
val NEARBY_DEVICES_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
} }