Skip to content

Instantly share code, notes, and snippets.

@mapm14
Last active June 14, 2023 16:56
Show Gist options
  • Save mapm14/86630ba8644218bf00a76e1a9041bb01 to your computer and use it in GitHub Desktop.
Save mapm14/86630ba8644218bf00a76e1a9041bb01 to your computer and use it in GitHub Desktop.
Passport Encryption Composable
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