mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
adjust animation
This commit is contained in:
@@ -1,85 +1,16 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.random.Random
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
|
||||
/**
|
||||
* Matrix-style encryption animation for messages during PoW computation
|
||||
* Animates message content with random characters that gradually resolve to real text
|
||||
*/
|
||||
@Composable
|
||||
fun AnimatedMatrixText(
|
||||
targetText: String,
|
||||
isAnimating: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontWeight: FontWeight? = null
|
||||
) {
|
||||
// Continuous animation: loop random characters until isAnimating becomes false
|
||||
var animatedText by remember(targetText) { mutableStateOf(targetText) }
|
||||
|
||||
LaunchedEffect(isAnimating, targetText) {
|
||||
if (isAnimating && targetText.isNotEmpty()) {
|
||||
val encryptedChars = "!@#$%^&*()_+-=[]{}|;:,.<>?~`".toCharArray()
|
||||
while (true) {
|
||||
val frame = CharArray(targetText.length) {
|
||||
encryptedChars[Random.nextInt(encryptedChars.size)]
|
||||
}
|
||||
animatedText = String(frame)
|
||||
delay(80L)
|
||||
}
|
||||
} else {
|
||||
animatedText = targetText
|
||||
}
|
||||
}
|
||||
|
||||
val styledText = buildAnnotatedString {
|
||||
animatedText.forEach { char ->
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = color,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = fontWeight
|
||||
)
|
||||
) {
|
||||
append(char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = styledText,
|
||||
modifier = modifier,
|
||||
fontSize = fontSize,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation state for individual characters
|
||||
@@ -90,63 +21,6 @@ private enum class CharacterAnimationState {
|
||||
FINAL // Showing final decrypted character
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate a single character from encrypted to final state
|
||||
*/
|
||||
private suspend fun animateCharacter(
|
||||
index: Int,
|
||||
targetChar: Char,
|
||||
currentText: () -> String,
|
||||
updateText: (String) -> Unit,
|
||||
updateState: (List<CharacterAnimationState>) -> Unit
|
||||
) {
|
||||
val encryptedChars = "!@#$%^&*()_+-=[]{}|;:,.<>?~`".toCharArray()
|
||||
val animationDuration = Random.nextLong(1000, 3000) // 1-3 seconds of encryption
|
||||
val startTime = System.currentTimeMillis()
|
||||
|
||||
// Phase 1: Animate with random encrypted characters
|
||||
while (System.currentTimeMillis() - startTime < animationDuration) {
|
||||
val randomChar = encryptedChars[Random.nextInt(encryptedChars.size)]
|
||||
|
||||
// Update the character at this index
|
||||
val text = currentText()
|
||||
if (index < text.length) {
|
||||
val newText = text.toCharArray()
|
||||
newText[index] = randomChar
|
||||
updateText(String(newText))
|
||||
}
|
||||
|
||||
delay(100) // Change character every 100ms
|
||||
}
|
||||
|
||||
// Phase 2: Brief transition state (yellow)
|
||||
val states = currentText().indices.map { i ->
|
||||
when {
|
||||
i == index -> CharacterAnimationState.DECRYPTING
|
||||
i < index -> CharacterAnimationState.FINAL // Already revealed
|
||||
else -> CharacterAnimationState.ENCRYPTED // Still encrypted
|
||||
}
|
||||
}
|
||||
updateState(states)
|
||||
delay(200) // Brief yellow flash
|
||||
|
||||
// Phase 3: Reveal final character
|
||||
val text = currentText()
|
||||
if (index < text.length) {
|
||||
val newText = text.toCharArray()
|
||||
newText[index] = targetChar
|
||||
updateText(String(newText))
|
||||
|
||||
val finalStates = currentText().indices.map { i ->
|
||||
when {
|
||||
i <= index -> CharacterAnimationState.FINAL
|
||||
else -> CharacterAnimationState.ENCRYPTED
|
||||
}
|
||||
}
|
||||
updateState(finalStates)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message should be animated based on its mining state
|
||||
*/
|
||||
@@ -256,26 +130,69 @@ private fun AnimatedMessageDisplay(
|
||||
var animatedContent by remember(message.content) { mutableStateOf(message.content) }
|
||||
val isAnimating = shouldAnimateMessage(message.id)
|
||||
|
||||
// Character-by-character animation state like the JavaScript version
|
||||
var characterStates by remember(message.content) {
|
||||
mutableStateOf(message.content.map { char ->
|
||||
if (char == ' ') CharacterAnimationState.FINAL else CharacterAnimationState.ENCRYPTED
|
||||
})
|
||||
}
|
||||
|
||||
// Update animated content when animation state changes
|
||||
LaunchedEffect(isAnimating, message.content) {
|
||||
if (isAnimating && message.content.isNotEmpty()) {
|
||||
val encryptedChars = "!@#$%^&*()_+-=[]{}|;:,.<>?~`".toCharArray()
|
||||
val baseLength = message.content.length
|
||||
val encryptedChars = "!@$%^&*()_+-=[]{}|;:,<>?".toCharArray()
|
||||
|
||||
// Loop animation continuously until PoW stops (LaunchedEffect will cancel on recomposition)
|
||||
while (true) {
|
||||
// Generate a new random frame of the same length as the real content
|
||||
val frame = CharArray(baseLength) {
|
||||
encryptedChars[Random.nextInt(encryptedChars.size)]
|
||||
// Start character animations with staggered delays (like JS version)
|
||||
message.content.forEachIndexed { index, targetChar ->
|
||||
if (targetChar != ' ') { // Skip spaces
|
||||
launch {
|
||||
delay(index * 50L) // Stagger start like JS version
|
||||
|
||||
// Animate this character indefinitely in a loop
|
||||
while (true) {
|
||||
// Animate with random characters
|
||||
while (characterStates.getOrNull(index) == CharacterAnimationState.ENCRYPTED) {
|
||||
// Generate random encrypted character for this position
|
||||
val newContent = animatedContent.toCharArray()
|
||||
if (index < newContent.size) {
|
||||
newContent[index] = encryptedChars[Random.nextInt(encryptedChars.size)]
|
||||
animatedContent = String(newContent)
|
||||
}
|
||||
|
||||
delay(100L) // Change character every 100ms like JS
|
||||
|
||||
// Random chance to reveal (10% like JS version)
|
||||
if (Random.nextFloat() < 0.1f) {
|
||||
// Reveal the final character
|
||||
val finalContent = animatedContent.toCharArray()
|
||||
if (index < finalContent.size) {
|
||||
finalContent[index] = targetChar
|
||||
animatedContent = String(finalContent)
|
||||
}
|
||||
|
||||
// Mark as revealed
|
||||
val finalStates = characterStates.toMutableList()
|
||||
finalStates[index] = CharacterAnimationState.FINAL
|
||||
characterStates = finalStates
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Keep revealed for 2 seconds, then fade back to encrypted (like JS)
|
||||
delay(2000L)
|
||||
|
||||
// Reset back to encrypted for next cycle
|
||||
val resetStates = characterStates.toMutableList()
|
||||
resetStates[index] = CharacterAnimationState.ENCRYPTED
|
||||
characterStates = resetStates
|
||||
}
|
||||
}
|
||||
}
|
||||
animatedContent = String(frame)
|
||||
|
||||
// Small delay for smooth animation; adjust if needed
|
||||
delay(80L)
|
||||
}
|
||||
} else {
|
||||
// Not animating, show final content
|
||||
animatedContent = message.content
|
||||
characterStates = message.content.map { CharacterAnimationState.FINAL }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user