Last active
June 14, 2023 16:56
-
-
Save mapm14/86630ba8644218bf00a76e1a9041bb01 to your computer and use it in GitHub Desktop.
Passport Encryption Composable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.manuperera.passportencryption | |
import android.content.pm.ActivityInfo | |
import android.os.Bundle | |
import androidx.activity.ComponentActivity | |
import androidx.activity.compose.setContent | |
import androidx.compose.animation.core.LinearEasing | |
import androidx.compose.animation.core.animateFloatAsState | |
import androidx.compose.animation.core.animateOffsetAsState | |
import androidx.compose.animation.core.tween | |
import android.app.Activity | |
import android.content.Context | |
import android.content.ContextWrapper | |
import androidx.compose.runtime.DisposableEffect | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.foundation.Canvas | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.BoxScope | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxHeight | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.layout.size | |
import androidx.compose.foundation.layout.width | |
import androidx.compose.foundation.shape.GenericShape | |
import androidx.compose.foundation.shape.RoundedCornerShape | |
import androidx.compose.material.Icon | |
import androidx.compose.material.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.derivedStateOf | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.composed | |
import androidx.compose.ui.draw.clipToBounds | |
import androidx.compose.ui.draw.drawWithCache | |
import androidx.compose.ui.draw.scale | |
import androidx.compose.ui.geometry.CornerRadius | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.geometry.Rect | |
import androidx.compose.ui.geometry.Size | |
import androidx.compose.ui.graphics.Brush | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.PathEffect | |
import androidx.compose.ui.graphics.drawscope.Stroke | |
import androidx.compose.ui.graphics.graphicsLayer | |
import androidx.compose.ui.platform.LocalConfiguration | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.res.painterResource | |
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.FontWeight | |
import androidx.compose.ui.text.style.TextAlign | |
import androidx.compose.ui.text.withStyle | |
import androidx.compose.ui.tooling.preview.Preview | |
import androidx.compose.ui.unit.Dp | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import com.manuperera.passportencryption.ui.theme.PassportEncryptionTheme | |
import kotlinx.coroutines.delay | |
import kotlinx.coroutines.launch | |
import kotlin.math.roundToInt | |
import kotlin.random.Random | |
class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContent { | |
PassportEncryptionTheme { | |
LockScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) | |
EncryptionScreen() | |
} | |
} | |
} | |
} | |
@Composable | |
private fun EncryptionScreen() { | |
Box( | |
modifier = Modifier | |
.fillMaxSize() | |
.background(Color.Black) | |
) { | |
var animated by remember { mutableStateOf(false) } | |
val configuration = LocalConfiguration.current | |
val density = LocalDensity.current | |
val centerX by remember { mutableStateOf(density.run { configuration.screenWidthDp.dp.toPx() / 2f }) } | |
val passportWidth by remember { mutableStateOf(configuration.screenWidthDp.dp * passportWidthMultiplier) } | |
val passportWidthPx by remember { mutableStateOf(density.run { passportWidth.toPx() }) } | |
val textXDelayMillis by remember { derivedStateOf { (centerX * xDurationMillis / (centerX + passportWidthPx)) + delayMillis } } | |
val passportXAnimation by animateFloatAsState( | |
targetValue = if (animated) centerX else -passportWidthPx, | |
animationSpec = tween( | |
durationMillis = xDurationMillis, | |
delayMillis = delayMillis, | |
easing = LinearEasing, | |
) | |
) | |
val textXAnimation by animateFloatAsState( | |
targetValue = if (animated) centerX else -passportWidthPx, | |
animationSpec = tween( | |
durationMillis = xDurationMillis, | |
delayMillis = textXDelayMillis.roundToInt(), | |
easing = LinearEasing, | |
) | |
) | |
val encryptionDotsXAnimation by animateFloatAsState( | |
targetValue = if (animated) centerX + 100 else -passportWidthPx - (centerX * 0.03f), | |
animationSpec = tween( | |
durationMillis = (xDurationMillis * 1.03f).roundToInt(), | |
delayMillis = (delayMillis * 1.03f).roundToInt(), | |
easing = LinearEasing, | |
) | |
) | |
BackgroundDots() | |
EncryptionDots( | |
encryptionDotsXAnimation = encryptionDotsXAnimation, | |
passportWidthPx = passportWidthPx, | |
passportWidthDp = passportWidth, | |
) | |
PassportCard( | |
width = passportWidth, | |
passportXAnimation = passportXAnimation, | |
) | |
EncryptedText( | |
width = passportWidth, | |
textXAnimation = textXAnimation, | |
textXDelayMillis = textXDelayMillis.toLong() + 200L, | |
) | |
BackgroundDim() | |
OvalGradient(centerX) | |
animated = true | |
} | |
} | |
@Composable | |
private fun BackgroundDots() { | |
val numberOfDots = 220 | |
val configuration = LocalConfiguration.current | |
val density = LocalDensity.current | |
val screenWidth = density.run { configuration.screenWidthDp.dp.toPx() } | |
val screenHeight = density.run { configuration.screenHeightDp.dp.toPx() } | |
var animated by remember { mutableStateOf(false) } | |
val animationList = List(numberOfDots) { | |
val randomInitialX by remember { mutableStateOf(Random.nextFloat()) } | |
val randomInitialY by remember { mutableStateOf(Random.nextFloat()) } | |
val randomFinalX by remember { mutableStateOf(Random.nextFloat()) } | |
val initialX = | |
randomInitialX * (screenWidth * 0.9f - (screenWidth * -0.5f)) + (screenWidth * -0.5f) | |
val initialY = randomInitialY * screenHeight | |
val finalX = randomFinalX * (screenWidth * 1.1f - initialX + 200f) + initialX + 200f | |
animateOffsetAsState( | |
targetValue = if (animated) Offset(finalX, initialY) else Offset(initialX, initialY), | |
animationSpec = tween( | |
durationMillis = 15500, | |
delayMillis = 0, | |
easing = LinearEasing, | |
) | |
) | |
} | |
Canvas(Modifier.fillMaxSize()) { | |
animationList.forEach { animation -> | |
val alpha = Random.nextInt() * (0xFF - 0x08) + 0x08 | |
val red = Random.nextInt() * 0xC8 | |
val green = Random.nextInt() * 0xE0 | |
val radius = Random.nextFloat() * (6.5f - 0.25f) + 0.25f | |
drawCircle( | |
color = Color(alpha = alpha, red = red, green = green, blue = 0xFF), | |
radius = radius, | |
center = animation.value, | |
) | |
} | |
} | |
animated = true | |
} | |
@Composable | |
private fun BoxScope.EncryptionDots( | |
encryptionDotsXAnimation: Float, | |
passportWidthPx: Float, | |
passportWidthDp: Dp, | |
) { | |
val numberOfDots = 2220 | |
val configuration = LocalConfiguration.current | |
val density = LocalDensity.current | |
val screenHeight = density.run { configuration.screenHeightDp.dp.toPx() } | |
var animated by remember { mutableStateOf(false) } | |
val animationList = List(numberOfDots) { | |
val randomInitialY by remember { mutableStateOf(Random.nextFloat() * (0 - screenHeight * lineHeightMultiplier) + screenHeight * lineHeightMultiplier) } | |
val randomInitialX by remember { mutableStateOf(Random.nextFloat() * passportWidthPx) } | |
val randomAlpha by remember { mutableStateOf(Random.nextFloat() * (0.6f - 0.1f) + 0.1f) } | |
Triple(randomInitialY, randomInitialX, randomAlpha) | |
} | |
Box( | |
modifier = Modifier | |
.fillMaxWidth(0.52f) | |
.fillMaxHeight(lineHeightMultiplier) | |
.align(Alignment.CenterStart) | |
.clipToBounds(), | |
contentAlignment = Alignment.CenterStart, | |
) { | |
Canvas( | |
Modifier | |
.width(passportWidthDp) | |
.fillMaxHeight() | |
.graphicsLayer { translationX = encryptionDotsXAnimation } | |
) { | |
animationList.forEach { pair -> | |
val radius = Random.nextFloat() * (16.5f - 1f) + 1f | |
drawCircle( | |
color = AlphaBlue, | |
radius = radius, | |
center = Offset(pair.second, pair.first), | |
alpha = pair.third | |
) | |
} | |
} | |
} | |
animated = true | |
} | |
fun Modifier.dashedBorder(strokeWidth: Dp, color: Color, cornerRadiusDp: Dp) = composed( | |
factory = { | |
val density = LocalDensity.current | |
val strokeWidthPx = density.run { strokeWidth.toPx() } | |
val cornerRadiusPx = density.run { cornerRadiusDp.toPx() } | |
this.then( | |
Modifier.drawWithCache { | |
onDrawBehind { | |
val stroke = Stroke( | |
width = strokeWidthPx, | |
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) | |
) | |
drawRoundRect( | |
color = color, | |
style = stroke, | |
cornerRadius = CornerRadius(cornerRadiusPx) | |
) | |
} | |
} | |
) | |
} | |
) | |
@Composable | |
private fun BoxScope.PassportCard(width: Dp, passportXAnimation: Float) { | |
Box( | |
modifier = Modifier | |
.fillMaxWidth(0.5f) | |
.fillMaxHeight() | |
.align(Alignment.CenterStart) | |
.clipToBounds(), | |
contentAlignment = Alignment.CenterStart, | |
) { | |
Column( | |
modifier = Modifier | |
.width(width) | |
.fillMaxHeight(lineHeightMultiplier) | |
.graphicsLayer { translationX = passportXAnimation } | |
.background(Color.White, RoundedCornerShape(8.dp)) | |
.padding(12.dp) | |
) { | |
Text( | |
text = "PASSPORT", | |
style = TextStyle(fontSize = 19.sp, fontWeight = FontWeight.Bold), | |
) | |
Spacer(modifier = Modifier.size(14.dp)) | |
Row(modifier = Modifier.fillMaxWidth()) { | |
Box( | |
modifier = Modifier | |
.fillMaxHeight(0.7f) | |
.weight(0.4f) | |
.background(Color.Black, RoundedCornerShape(20.dp)) | |
.padding(8.dp) | |
.dashedBorder(1.dp, Color.White, 12.dp), | |
contentAlignment = Alignment.Center | |
) { | |
Icon( | |
modifier = Modifier.size(48.dp), | |
painter = painterResource(id = R.drawable.ic_camera), | |
tint = Color.White, | |
contentDescription = "Passport image", | |
) | |
} | |
Spacer(modifier = Modifier.size(14.dp)) | |
Column( | |
modifier = Modifier | |
.fillMaxHeight(0.7f) | |
.weight(0.3f), | |
) { | |
Text( | |
text = "NAME", | |
style = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.SemiBold), | |
color = Color.Gray, | |
maxLines = 1, | |
) | |
Text( | |
text = "MANUEL", | |
style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Bold), | |
maxLines = 1 | |
) | |
Spacer(modifier = Modifier.size(8.dp)) | |
Text( | |
text = "SURNAME", | |
style = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.SemiBold), | |
color = Color.Gray, | |
maxLines = 1, | |
) | |
Text( | |
text = "PERERA", | |
style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Bold), | |
maxLines = 1 | |
) | |
Spacer(modifier = Modifier.size(8.dp)) | |
Text( | |
text = "NATIONALITY", | |
style = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.SemiBold), | |
color = Color.Gray, | |
maxLines = 1, | |
) | |
Text( | |
text = "VENEZUELAN", | |
style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Bold), | |
maxLines = 1 | |
) | |
} | |
Column( | |
modifier = Modifier | |
.fillMaxHeight(0.7f) | |
.weight(0.3f), | |
) { | |
Text( | |
text = "CARD NUMBER", | |
style = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.SemiBold), | |
color = Color.Gray, | |
maxLines = 1, | |
) | |
Text( | |
text = "838194940", | |
style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Bold), | |
maxLines = 1 | |
) | |
Spacer(modifier = Modifier.size(8.dp)) | |
Text( | |
text = "DATE OF BIRTH", | |
style = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.SemiBold), | |
color = Color.Gray, | |
maxLines = 1, | |
) | |
Text( | |
text = "01/01/1992", | |
style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Bold), | |
maxLines = 1 | |
) | |
Spacer(modifier = Modifier.size(8.dp)) | |
Text( | |
text = "EXPIRATION", | |
style = TextStyle(fontSize = 10.sp, fontWeight = FontWeight.SemiBold), | |
color = Color.Gray, | |
maxLines = 1, | |
) | |
Text( | |
text = "31/12/2030", | |
style = TextStyle(fontSize = 13.sp, fontWeight = FontWeight.Bold), | |
maxLines = 1 | |
) | |
} | |
} | |
Spacer(modifier = Modifier.size(14.dp)) | |
Text( | |
text = "PIN156891561<<<<MANUEL<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<BHASD<<<PERERA<<<<<<<<<<<<V<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", | |
color = Color.Gray, | |
maxLines = 2, | |
style = TextStyle(fontSize = 12.sp), | |
) | |
} | |
} | |
} | |
@Composable | |
private fun BoxScope.EncryptedText( | |
width: Dp, | |
textXAnimation: Float, | |
textXDelayMillis: Long, | |
) { | |
var currentText by remember { mutableStateOf(text1) } | |
val scope = rememberCoroutineScope() | |
Box( | |
modifier = Modifier | |
.fillMaxWidth(0.5f) | |
.fillMaxHeight() | |
.align(Alignment.CenterEnd) | |
.clipToBounds(), | |
contentAlignment = Alignment.CenterStart, | |
) { | |
Text( | |
modifier = Modifier | |
.width(width) | |
.fillMaxHeight(lineHeightMultiplier) | |
.graphicsLayer { translationX = textXAnimation }, | |
text = currentText, | |
style = TextStyle( | |
fontSize = 14.sp, | |
lineHeight = 26.sp, | |
textAlign = TextAlign.Justify, | |
), | |
) | |
} | |
LaunchedEffect(Unit) { | |
scope.launch { | |
delay(textXDelayMillis) | |
currentText = text2 | |
delay(textChangeDelayMillis) | |
currentText = text3 | |
delay(textChangeDelayMillis) | |
currentText = text1 | |
delay(textChangeDelayMillis) | |
currentText = text3 | |
delay(textChangeDelayMillis) | |
currentText = text2 | |
delay(textChangeDelayMillis) | |
currentText = text1 | |
delay(textChangeDelayMillis) | |
currentText = text2 | |
delay(textChangeDelayMillis) | |
currentText = text3 | |
delay(textChangeDelayMillis) | |
currentText = text1 | |
} | |
} | |
} | |
@Composable | |
private fun BoxScope.BackgroundDim() { | |
Box( | |
Modifier | |
.fillMaxSize() | |
.background( | |
Brush.horizontalGradient( | |
0f to Color.Black.copy(0.9f), | |
0.4f to Color.Transparent, | |
0.6f to Color.Transparent, | |
1f to Color.Black.copy(0.9f), | |
) | |
) | |
) | |
Box( | |
Modifier | |
.fillMaxHeight(lineHeightMultiplier) | |
.fillMaxWidth() | |
.align(Alignment.CenterStart) | |
.background( | |
Brush.horizontalGradient( | |
0f to Color.Transparent, | |
0.5f to Color.Black.copy(alpha = 0.2f), | |
1f to Color.Transparent, | |
) | |
) | |
) | |
} | |
@Composable | |
private fun OvalGradient(centerX: Float) { | |
Box( | |
modifier = Modifier.fillMaxSize(), | |
) { | |
val ovalShape = GenericShape { size, _ -> | |
addOval( | |
Rect( | |
offset = Offset( | |
centerX - (lineWidth / 2), | |
size.height * remainingLineHeightMultiplier | |
), | |
size = Size(width = lineWidth, height = size.height * lineHeightMultiplier), | |
) | |
) | |
} | |
Box( | |
Modifier | |
.width(4.dp) | |
.fillMaxHeight() | |
.background(Brush.radialGradient(listOf(Lighter, Light)), ovalShape) | |
) | |
val gradientHeight = LocalConfiguration.current.screenHeightDp.dp * 0.85f | |
val width = 45.dp | |
val aspectRatio = width / gradientHeight | |
Box( | |
Modifier | |
.width(width) | |
.fillMaxHeight() | |
.scale(maxOf(aspectRatio, 1f), maxOf(1 / aspectRatio, 1f)) | |
.background(Brush.radialGradient(listOf(AlphaBlue, Color.Transparent))) | |
.align(Alignment.Center) | |
) | |
} | |
} | |
@Preview( | |
showSystemUi = true, | |
device = "spec:width=411dp,height=891dp,dpi=420,isRound=false,chinSize=0dp,orientation=landscape", | |
) | |
@Composable | |
fun Preview() { | |
PassportEncryptionTheme { | |
EncryptionScreen() | |
} | |
} | |
@Preview( | |
showSystemUi = true, | |
device = "spec:width=411dp,height=891dp,dpi=420,isRound=false,chinSize=0dp,orientation=landscape", | |
) | |
@Composable | |
fun PreviewPassport() { | |
PassportEncryptionTheme { | |
Box(contentAlignment = Alignment.Center) { | |
PassportCard( | |
width = 400.dp, | |
passportXAnimation = 0f, | |
) | |
} | |
} | |
} | |
val Light = Color(0xFF9AC7F5) | |
val Lighter = Color(0xFF789ABD) | |
val AlphaBlue = Color(0x3B00BCD4) | |
const val delayMillis = 800 | |
const val passportWidthMultiplier = 0.45f | |
const val xDurationMillis = 6800 | |
const val textChangeDelayMillis = 750L | |
const val lineWidth = 7.5f | |
const val lineHeightMultiplier = 0.67f | |
const val remainingLineHeightMultiplier = (1 - lineHeightMultiplier) / 2 | |
fun AnnotatedString.Builder.white(text: String) = | |
withStyle(style = SpanStyle(color = Color.White.copy(alpha = 0.4f))) { | |
append(text) | |
} | |
fun AnnotatedString.Builder.darkGray(text: String) = | |
withStyle(style = SpanStyle(color = Light.copy(alpha = 0.40f))) { | |
append(text) | |
} | |
val text1 = buildAnnotatedString { | |
white("A ") | |
darkGray("1 S 5 6 D F A 8 E 1 D 9 8 F W 1 1 R V E Q 7 8 R 1 ") | |
white("! ") | |
darkGray("8 Q E 9 R 1 F V 7 8 E T 1 V 8 9 E 1 7 8 Q 1 E R 8 ") | |
white("3 ") | |
darkGray("1 V Q E 9 8 R 1 V 7 F E Q 9 4 R F 8 9 E Q F 4 1 8 ") | |
white("= ") | |
darkGray("E R 4 1 9 F ") | |
white("^ ") | |
darkGray("E R 4 1 9 F 8 1 E Q 8 1 V R W 1 V 8 9 E Q R 1 V 7 ") | |
white("2 ") | |
white("! ") | |
darkGray("E Q 8 1 V R W 1 V 8 9 E Q R 1 V E R 4 1 9 F 8 1 7 ") | |
white("0 ") | |
white("? ") | |
darkGray("8 1 V 9 ") | |
darkGray("8 1 E Q 8 1 V R W 1 V 8 9 E Q R 1 V 7 ") | |
white("2 ") | |
darkGray("8 1 V 9 8 W 1 V R 8 Q 1 E R 9 8 V 1 Q 9 8 R 1 Q 8 ") | |
white("B ") | |
darkGray("V R W 1 V 8 9 E Q R 1 V 7 E R 4 1 9 F 8 1 E Q 8 1 ") | |
white("* ") | |
darkGray("E Q 8 1 V R W 1 V 8 9 E Q R 1 V E R 4 1 9 F 8 1 7 ") | |
white("0 ") | |
white("^ ") | |
darkGray("8 1 E Q 8 1 V R W 1 V 8 9 E Q R 1 V 7 ") | |
white("2 ") | |
darkGray("8 1 V 9 8 W 1 V R 8 Q 1 E R 9 8 V 1 Q 9 8 R 1 Q 8 ") | |
white("B ") | |
} | |
val text2 = buildAnnotatedString { | |
darkGray("E R 4 1 9 F ") | |
white("P ") | |
darkGray(" 8 1 E Q 8 1 V R W 1 V 8 9 E Q R 1 V 7 ") | |
white("3 ") | |
darkGray("E R 4 1 9 F 8 1 E Q 8 1 V R W 1 V 8 9 E Q R 1 V 7 ") | |
white("2 ") | |
white("! ") | |
darkGray("E Q 8 1 V R W 1 V 8 9 E Q R 1 V E R 4 1 9 F 8 1 7 ") | |
white("0 ") | |
white("? ") | |
darkGray("8 1 V 9 ") | |
darkGray("8 1 V 9 8 W 1 V R 8 Q ") | |
white("¿ ") | |
darkGray("1 E R 9 8 V 1 Q 9 8 R 1 Q 8 ") | |
white("2 ") | |
darkGray("1 V Q E 9 8 R 1 V 7 F E Q 9 4 R F 8 9 E Q F 4 1 8 ") | |
white("^ ") | |
darkGray("1 S 5 6 D F A 8 E 1 D 9 8 F W 1 1 R V E Q 7 8 R 1 ") | |
white("B ") | |
darkGray("8 Q E 9 R 1 F V 7 8 E T 1 V 8 9 E 1 7 8 Q 1 E R 8 ") | |
white("G ") | |
darkGray("E Q 8 1 V R W ") | |
white("Z ") | |
darkGray("1 V 8 9 E Q R 1 V E R 4 1 9 F 8 1 7 ") | |
white("H ") | |
darkGray("V R W 1 V 8 9 E Q R 1 V 7 E R 4 1 9 F 8 1 E Q 8 1 ") | |
white("! ") | |
white("^ ") | |
darkGray("1 S 5 6 D F A 8 E 1 D 9 8 F W 1 1 R V E Q 7 8 R 1 ") | |
white("B ") | |
darkGray("8 Q E 9 R 1 F V 7 8 E T 1 V 8 9 E 1 7 8 Q 1 E R 8 ") | |
white("G ") | |
} | |
val text3 = buildAnnotatedString { | |
darkGray("1 V Q ") | |
white("7 ") | |
darkGray("E 9 8 R 1 V 7 F E Q 9 4 R F 8 9 E Q F 4 1 8 ") | |
white("! ") | |
darkGray("1 S 5 6 D F A 8 E 1 D 9 8 F W 1 1 R V E Q 7 8 R 1 ") | |
white("¿ ") | |
darkGray("8 Q E 9 R 1 F V 7 8 E T 1 V 8 9 E 1 7 8 Q 1 E R 8 ") | |
white("= ") | |
darkGray("E R 4 1 9 F 8 1 E Q 8 1 V R W 1 V 8 9 E Q R 1 V 7 ") | |
white("2 ") | |
white("! ") | |
darkGray("E Q 8 1 V R W 1 V 8 9 E Q R 1 V E R 4 1 9 F 8 1 7 ") | |
white("0 ") | |
white("? ") | |
darkGray("8 1 V 9 ") | |
white("* ") | |
darkGray("8 W 1 V R 8 Q 1 E R 9 8 V 1 Q 9 8 R 1 Q 8 ") | |
white("0 ") | |
darkGray("V R W 1 V ") | |
white("/ ") | |
darkGray("8 9 E Q R 1 V 7 E R 4 1 9 F 8 1 E Q 8 1 ") | |
white("= ") | |
darkGray("E R 4 1 9 F 8 1 E Q 8 1 V R W 1 V 8 9 E Q R 1 V 7 ") | |
white("2 ") | |
white("! ") | |
darkGray("E Q 8 1 V R W 1 V 8 9 E Q R 1 V E R 4 1 9 F 8 1 7 ") | |
white("0 ") | |
darkGray("E R 4 1 9 F 8 1 E Q 8 1 V R W 1 V 8 9 E Q R 1 V 7 ") | |
white("2 ") | |
white("! ") | |
darkGray("E Q 8 1 V R W 1 V 8 9 E Q R 1 V E R 4 1 9 F 8 1 7 ") | |
white("0 ") | |
white("? ") | |
darkGray("8 1 V 9 ") | |
white("? ") | |
} | |
@Composable | |
fun LockScreenOrientation(orientation: Int) { | |
val context = LocalContext.current | |
DisposableEffect(orientation) { | |
val activity = context.findActivity() ?: return@DisposableEffect onDispose {} | |
val originalOrientation = activity.requestedOrientation | |
activity.requestedOrientation = orientation | |
onDispose { | |
// restore original orientation when view disappears | |
activity.requestedOrientation = originalOrientation | |
} | |
} | |
} | |
fun Context.findActivity(): Activity? = when (this) { | |
is Activity -> this | |
is ContextWrapper -> baseContext.findActivity() | |
else -> null | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment