mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-08 14:52:13 -04:00
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:
parent
74216e8850
commit
0a1e5f36a1
@ -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)
|
||||
)
|
||||
}
|
||||
|
@ -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<String, ShowcaseProperty>,
|
||||
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<Float>(
|
||||
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,
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user