Initial commit: rosetta-android-prime
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.rosetta.messenger.ui.onboarding
|
||||
|
||||
data class OnboardingPage(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val highlightWords: List<String> = emptyList()
|
||||
)
|
||||
|
||||
val onboardingPages = listOf(
|
||||
OnboardingPage(
|
||||
title = "Rosetta",
|
||||
description = "A local-based messaging app.\nYour data stays on your device.",
|
||||
highlightWords = listOf("local-based", "your device")
|
||||
),
|
||||
OnboardingPage(
|
||||
title = "Fast",
|
||||
description = "Rosetta delivers messages faster\nthan any other application.",
|
||||
highlightWords = listOf("faster")
|
||||
),
|
||||
OnboardingPage(
|
||||
title = "Free",
|
||||
description = "Rosetta is free forever. No ads.\nNo subscription fees. Ever.",
|
||||
highlightWords = listOf("free forever", "No ads", "Ever")
|
||||
),
|
||||
OnboardingPage(
|
||||
title = "Secure",
|
||||
description = "Rosetta keeps your messages safe\nwith local storage and encryption.",
|
||||
highlightWords = listOf("safe", "local storage", "encryption")
|
||||
),
|
||||
OnboardingPage(
|
||||
title = "Private",
|
||||
description = "No servers. No tracking.\nEverything stays on your device.",
|
||||
highlightWords = listOf("No servers", "No tracking", "your device")
|
||||
)
|
||||
|
||||
)
|
||||
@@ -0,0 +1,634 @@
|
||||
package com.rosetta.messenger.ui.onboarding
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.rosetta.messenger.ui.theme.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.hypot
|
||||
import com.airbnb.lottie.compose.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.rosetta.messenger.R
|
||||
import androidx.compose.foundation.Image
|
||||
|
||||
// App colors (matching React Native)
|
||||
val PrimaryBlue = Color(0xFF248AE6) // primary light theme
|
||||
val PrimaryBlueDark = Color(0xFF1A72C2) // primary dark theme
|
||||
val LightBlue = Color(0xFF74C0FC) // lightBlue
|
||||
val OnboardingBackground = Color(0xFF1E1E1E) // dark background
|
||||
val OnboardingBackgroundLight = Color(0xFFFFFFFF)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun OnboardingScreen(
|
||||
isDarkTheme: Boolean,
|
||||
onThemeToggle: () -> Unit,
|
||||
onStartMessaging: () -> Unit
|
||||
) {
|
||||
val pagerState = rememberPagerState(pageCount = { onboardingPages.size })
|
||||
|
||||
// Preload Lottie animations
|
||||
val ideaComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Idea.json"))
|
||||
val moneyComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Money.json"))
|
||||
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
|
||||
val bookComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Book.json"))
|
||||
|
||||
// Theme transition animation
|
||||
var isTransitioning by remember { mutableStateOf(false) }
|
||||
var transitionProgress by remember { mutableStateOf(0f) }
|
||||
var clickPosition by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) }
|
||||
var shouldUpdateStatusBar by remember { mutableStateOf(false) }
|
||||
var hasInitialized by remember { mutableStateOf(false) }
|
||||
var previousTheme by remember { mutableStateOf(isDarkTheme) }
|
||||
var targetTheme by remember { mutableStateOf(isDarkTheme) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
hasInitialized = true
|
||||
}
|
||||
|
||||
LaunchedEffect(isTransitioning) {
|
||||
if (isTransitioning) {
|
||||
shouldUpdateStatusBar = false
|
||||
val duration = 800f
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (transitionProgress < 1f) {
|
||||
val elapsed = System.currentTimeMillis() - startTime
|
||||
transitionProgress = (elapsed / duration).coerceAtMost(1f)
|
||||
|
||||
// Update status bar when wave reaches top (around 15% progress)
|
||||
if (transitionProgress >= 0.15f && !shouldUpdateStatusBar) {
|
||||
shouldUpdateStatusBar = true
|
||||
}
|
||||
|
||||
delay(16) // ~60fps
|
||||
}
|
||||
isTransitioning = false
|
||||
transitionProgress = 0f
|
||||
shouldUpdateStatusBar = false
|
||||
previousTheme = targetTheme
|
||||
}
|
||||
}
|
||||
|
||||
// Update status bar and navigation bar icons when wave reaches the top
|
||||
val view = LocalView.current
|
||||
LaunchedEffect(shouldUpdateStatusBar, isDarkTheme) {
|
||||
if (shouldUpdateStatusBar && !view.isInEditMode) {
|
||||
val window = (view.context as android.app.Activity).window
|
||||
val insetsController = WindowCompat.getInsetsController(window, view)
|
||||
insetsController.isAppearanceLightStatusBars = !isDarkTheme
|
||||
insetsController.isAppearanceLightNavigationBars = !isDarkTheme
|
||||
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
}
|
||||
}
|
||||
|
||||
// Animate navigation bar color with theme transition
|
||||
LaunchedEffect(isTransitioning, transitionProgress, isDarkTheme) {
|
||||
if (!view.isInEditMode) {
|
||||
val window = (view.context as android.app.Activity).window
|
||||
if (isTransitioning) {
|
||||
// Interpolate color during transition
|
||||
val oldColor = if (previousTheme) 0xFF1E1E1E else 0xFFFFFFFF
|
||||
val newColor = if (targetTheme) 0xFF1E1E1E else 0xFFFFFFFF
|
||||
|
||||
val r1 = (oldColor shr 16 and 0xFF)
|
||||
val g1 = (oldColor shr 8 and 0xFF)
|
||||
val b1 = (oldColor and 0xFF)
|
||||
|
||||
val r2 = (newColor shr 16 and 0xFF)
|
||||
val g2 = (newColor shr 8 and 0xFF)
|
||||
val b2 = (newColor and 0xFF)
|
||||
|
||||
val r = (r1 + (r2 - r1) * transitionProgress).toInt()
|
||||
val g = (g1 + (g2 - g1) * transitionProgress).toInt()
|
||||
val b = (b1 + (b2 - b1) * transitionProgress).toInt()
|
||||
|
||||
window.navigationBarColor = (0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
|
||||
} else {
|
||||
// Set final color when not transitioning
|
||||
window.navigationBarColor = if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = if (isDarkTheme) OnboardingBackground else OnboardingBackgroundLight,
|
||||
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
|
||||
label = "backgroundColor"
|
||||
)
|
||||
val textColor by animateColorAsState(
|
||||
targetValue = if (isDarkTheme) Color.White else Color.Black,
|
||||
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
|
||||
label = "textColor"
|
||||
)
|
||||
val secondaryTextColor by animateColorAsState(
|
||||
targetValue = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
|
||||
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
|
||||
label = "secondaryTextColor"
|
||||
)
|
||||
val indicatorColor by animateColorAsState(
|
||||
targetValue = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
|
||||
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
|
||||
label = "indicatorColor"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
// Base background - shows the OLD theme color during transition
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(if (isTransitioning) {
|
||||
if (previousTheme) OnboardingBackground else OnboardingBackgroundLight
|
||||
} else backgroundColor)
|
||||
)
|
||||
|
||||
// Circular reveal overlay - draws the NEW theme color expanding
|
||||
if (isTransitioning) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val maxRadius = hypot(size.width, size.height)
|
||||
val radius = maxRadius * transitionProgress
|
||||
|
||||
// Draw the NEW theme color expanding from click point
|
||||
drawCircle(
|
||||
color = if (targetTheme) OnboardingBackground else OnboardingBackgroundLight,
|
||||
radius = radius,
|
||||
center = clickPosition
|
||||
)
|
||||
}
|
||||
}
|
||||
// Theme toggle button in top right
|
||||
ThemeToggleButton(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onToggle = { position ->
|
||||
if (!isTransitioning) {
|
||||
previousTheme = isDarkTheme
|
||||
targetTheme = !isDarkTheme
|
||||
clickPosition = position
|
||||
isTransitioning = true
|
||||
onThemeToggle()
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(16.dp)
|
||||
.statusBarsPadding()
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.weight(0.15f))
|
||||
|
||||
// Animated Logo
|
||||
AnimatedRosettaLogo(
|
||||
pagerState = pagerState,
|
||||
ideaComposition = ideaComposition,
|
||||
moneyComposition = moneyComposition,
|
||||
lockComposition = lockComposition,
|
||||
bookComposition = bookComposition,
|
||||
modifier = Modifier.size(150.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Pager for text content
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(150.dp)
|
||||
) { page ->
|
||||
OnboardingPageContent(
|
||||
page = onboardingPages[page],
|
||||
textColor = textColor,
|
||||
secondaryTextColor = secondaryTextColor,
|
||||
highlightColor = PrimaryBlue,
|
||||
pageOffset = ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Page indicators
|
||||
PagerIndicator(
|
||||
pageCount = onboardingPages.size,
|
||||
currentPage = pagerState.currentPage,
|
||||
selectedColor = PrimaryBlue,
|
||||
unselectedColor = indicatorColor
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.3f))
|
||||
|
||||
// Start messaging button
|
||||
StartMessagingButton(
|
||||
onClick = onStartMessaging,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemeToggleButton(
|
||||
isDarkTheme: Boolean,
|
||||
onToggle: (androidx.compose.ui.geometry.Offset) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (isDarkTheme) 360f else 0f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = 0.6f,
|
||||
stiffness = Spring.StiffnessLow
|
||||
),
|
||||
label = "rotation"
|
||||
)
|
||||
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = 1f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = 0.4f,
|
||||
stiffness = Spring.StiffnessMedium
|
||||
),
|
||||
label = "scale"
|
||||
)
|
||||
|
||||
val iconColor by animateColorAsState(
|
||||
targetValue = if (isDarkTheme) Color.White else Color(0xFF2A2A2A),
|
||||
animationSpec = tween(800, easing = FastOutSlowInEasing),
|
||||
label = "iconColor"
|
||||
)
|
||||
|
||||
var buttonPosition by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) }
|
||||
var isClickable by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(isDarkTheme) {
|
||||
isClickable = false
|
||||
delay(800)
|
||||
isClickable = true
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { if (isClickable) onToggle(buttonPosition) },
|
||||
enabled = isClickable,
|
||||
modifier = modifier
|
||||
.size(48.dp)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
val bounds = coordinates.boundsInWindow()
|
||||
buttonPosition = androidx.compose.ui.geometry.Offset(
|
||||
x = bounds.center.x,
|
||||
y = bounds.center.y
|
||||
)
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.scale(scale)
|
||||
.rotate(rotation),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
|
||||
contentDescription = if (isDarkTheme) "Switch to Light Mode" else "Switch to Dark Mode",
|
||||
tint = iconColor,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AnimatedRosettaLogo(
|
||||
pagerState: PagerState,
|
||||
ideaComposition: Any?,
|
||||
moneyComposition: Any?,
|
||||
lockComposition: Any?,
|
||||
bookComposition: Any?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val currentPage = pagerState.currentPage
|
||||
val pageOffset = pagerState.currentPageOffsetFraction
|
||||
|
||||
// Animate scale and alpha based on swipe
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = 1f - (pageOffset.absoluteValue * 0.08f),
|
||||
animationSpec = tween(400, easing = FastOutSlowInEasing),
|
||||
label = "scale"
|
||||
)
|
||||
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = 1f - (pageOffset.absoluteValue * 0.3f),
|
||||
animationSpec = tween(400, easing = FastOutSlowInEasing),
|
||||
label = "alpha"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.scale(scale)
|
||||
.graphicsLayer { this.alpha = alpha },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Pre-render all animations to avoid lag
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Rosetta icon (page 0)
|
||||
if (currentPage == 0) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.rosetta_icon),
|
||||
contentDescription = "Rosetta Logo",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
// Fast page - idea animation (page 1)
|
||||
ideaComposition?.let { comp ->
|
||||
val lottieComp = comp as? com.airbnb.lottie.LottieComposition
|
||||
val progress by animateLottieCompositionAsState(
|
||||
composition = lottieComp,
|
||||
iterations = 1,
|
||||
isPlaying = currentPage == 1
|
||||
)
|
||||
if (currentPage == 1) {
|
||||
LottieAnimation(
|
||||
composition = lottieComp,
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Free page - money animation (page 2)
|
||||
moneyComposition?.let { comp ->
|
||||
val lottieComp = comp as? com.airbnb.lottie.LottieComposition
|
||||
val progress by animateLottieCompositionAsState(
|
||||
composition = lottieComp,
|
||||
iterations = 1,
|
||||
isPlaying = currentPage == 2
|
||||
)
|
||||
if (currentPage == 2) {
|
||||
LottieAnimation(
|
||||
composition = lottieComp,
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Secure page - lock animation (page 3)
|
||||
lockComposition?.let { comp ->
|
||||
val lottieComp = comp as? com.airbnb.lottie.LottieComposition
|
||||
val progress by animateLottieCompositionAsState(
|
||||
composition = lottieComp,
|
||||
iterations = 1,
|
||||
isPlaying = currentPage == 3
|
||||
)
|
||||
if (currentPage == 3) {
|
||||
LottieAnimation(
|
||||
composition = lottieComp,
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Private page - book animation (page 4)
|
||||
bookComposition?.let { comp ->
|
||||
val lottieComp = comp as? com.airbnb.lottie.LottieComposition
|
||||
val progress by animateLottieCompositionAsState(
|
||||
composition = lottieComp,
|
||||
iterations = 1,
|
||||
isPlaying = currentPage == 4
|
||||
)
|
||||
if (currentPage == 4) {
|
||||
LottieAnimation(
|
||||
composition = lottieComp,
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnboardingPageContent(
|
||||
page: OnboardingPage,
|
||||
textColor: Color,
|
||||
secondaryTextColor: Color,
|
||||
highlightColor: Color,
|
||||
pageOffset: Float
|
||||
) {
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = 1f - (pageOffset * 0.7f).coerceIn(0f, 1f),
|
||||
animationSpec = tween(400, easing = FastOutSlowInEasing),
|
||||
label = "alpha"
|
||||
)
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = 1f - (pageOffset * 0.1f).coerceIn(0f, 1f),
|
||||
animationSpec = tween(400, easing = FastOutSlowInEasing),
|
||||
label = "scale"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.graphicsLayer {
|
||||
this.alpha = alpha
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Title
|
||||
Text(
|
||||
text = page.title,
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Description with highlighted words
|
||||
val annotatedDescription = buildAnnotatedString {
|
||||
var currentIndex = 0
|
||||
val description = page.description
|
||||
|
||||
// Find and highlight words
|
||||
page.highlightWords.forEach { word ->
|
||||
val startIndex = description.indexOf(word, currentIndex, ignoreCase = true)
|
||||
if (startIndex >= 0) {
|
||||
// Add text before the word
|
||||
if (startIndex > currentIndex) {
|
||||
withStyle(SpanStyle(color = secondaryTextColor)) {
|
||||
append(description.substring(currentIndex, startIndex))
|
||||
}
|
||||
}
|
||||
// Add highlighted word
|
||||
withStyle(SpanStyle(color = highlightColor, fontWeight = FontWeight.SemiBold)) {
|
||||
append(description.substring(startIndex, startIndex + word.length))
|
||||
}
|
||||
currentIndex = startIndex + word.length
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (currentIndex < description.length) {
|
||||
withStyle(SpanStyle(color = secondaryTextColor)) {
|
||||
append(description.substring(currentIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = annotatedDescription,
|
||||
fontSize = 17.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PagerIndicator(
|
||||
pageCount: Int,
|
||||
currentPage: Int,
|
||||
selectedColor: Color,
|
||||
unselectedColor: Color,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
repeat(pageCount) { index ->
|
||||
val isSelected = index == currentPage
|
||||
val width by animateDpAsState(
|
||||
targetValue = if (isSelected) 20.dp else 8.dp,
|
||||
animationSpec = spring(dampingRatio = 0.8f),
|
||||
label = "indicatorWidth"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(8.dp)
|
||||
.width(width)
|
||||
.clip(CircleShape)
|
||||
.background(if (isSelected) selectedColor else unselectedColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StartMessagingButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// Shining effect animation
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "shine")
|
||||
val shimmerTranslate by infiniteTransition.animateFloat(
|
||||
initialValue = -0.5f,
|
||||
targetValue = 1.5f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(2000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "shimmerTranslate"
|
||||
)
|
||||
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(54.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = PrimaryBlue
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
// Draw shimmer on top
|
||||
val shimmerWidth = size.width * 0.6f
|
||||
val shimmerStart = shimmerTranslate * size.width
|
||||
drawRect(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0f),
|
||||
Color.White.copy(alpha = 0.3f),
|
||||
Color.White.copy(alpha = 0f)
|
||||
),
|
||||
start = Offset(shimmerStart, 0f),
|
||||
end = Offset(shimmerStart + shimmerWidth, size.height)
|
||||
)
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Start Messaging",
|
||||
fontSize = 17.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user