Created
March 7, 2025 02:40
-
-
Save Faisal-FS/09be37156517799302df816cfe15dbda to your computer and use it in GitHub Desktop.
Jetpack Compose OTP Screen With Animations – Code & Resources
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
| /* 10 Min To Create The PERFECT Jetpack Compose OTP Screen With Animations – Code & Resources | |
| This Gist contains the essential code and resources mentioned in my YouTube tutorial: | |
| Watch the full video tutorial here: https://bit.ly/4ia6Dvq | |
| */ | |
| import android.os.Bundle | |
| import androidx.activity.ComponentActivity | |
| import androidx.activity.compose.setContent | |
| import androidx.compose.animation.AnimatedVisibility | |
| import androidx.compose.foundation.background | |
| import androidx.compose.foundation.border | |
| import androidx.compose.foundation.layout.Arrangement | |
| import androidx.compose.foundation.layout.Box | |
| import androidx.compose.foundation.layout.Column | |
| import androidx.compose.foundation.layout.Row | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.foundation.layout.fillMaxWidth | |
| import androidx.compose.foundation.layout.height | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.foundation.layout.width | |
| import androidx.compose.foundation.shape.RoundedCornerShape | |
| import androidx.compose.foundation.text.BasicTextField | |
| import androidx.compose.foundation.text.KeyboardOptions | |
| import androidx.compose.material3.Button | |
| import androidx.compose.material3.MaterialTheme | |
| import androidx.compose.material3.Scaffold | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.LaunchedEffect | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.focus.FocusRequester | |
| import androidx.compose.ui.focus.focusRequester | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.layout.ContentScale | |
| import androidx.compose.ui.text.TextRange | |
| import androidx.compose.ui.text.input.ImeAction | |
| import androidx.compose.ui.text.input.KeyboardType | |
| import androidx.compose.ui.text.input.TextFieldValue | |
| import androidx.compose.ui.text.style.TextAlign | |
| import androidx.compose.ui.tooling.preview.Preview | |
| import androidx.compose.ui.unit.dp | |
| import com.codingreel.otpcompose.ui.theme.OTPComposeTheme | |
| import kotlinx.coroutines.delay | |
| class MainActivity : ComponentActivity() { | |
| override fun onCreate(savedInstanceState: Bundle?) { | |
| super.onCreate(savedInstanceState) | |
| setContent { | |
| OTPComposeTheme { | |
| Content() | |
| } | |
| } | |
| } | |
| } | |
| @Preview | |
| @Composable | |
| fun Content() { | |
| val focusRequester = remember { FocusRequester() } | |
| var otpText by remember { | |
| mutableStateOf("") | |
| } | |
| Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> | |
| Column( | |
| modifier = Modifier | |
| .padding(innerPadding) | |
| .padding(20.dp), | |
| horizontalAlignment = Alignment.CenterHorizontally, | |
| verticalArrangement = Arrangement.spacedBy(50.dp) | |
| ) { | |
| Text( | |
| modifier = Modifier, | |
| text = "Jetpack Compose OTP", | |
| style = MaterialTheme.typography.headlineLarge | |
| ) | |
| Column( | |
| modifier = Modifier.fillMaxSize(), | |
| horizontalAlignment = Alignment.CenterHorizontally, | |
| verticalArrangement = Arrangement.spacedBy(10.dp) | |
| ) { | |
| Text( | |
| modifier = Modifier, | |
| text = "Verify Code", | |
| style = MaterialTheme.typography.titleMedium | |
| ) | |
| Text( | |
| modifier = Modifier, | |
| text = "Enter six digits code sent to your phone number", | |
| style = MaterialTheme.typography.bodyMedium | |
| ) | |
| OtpInputField(modifier = Modifier.focusRequester(focusRequester), | |
| otpText = otpText, | |
| shouldShowCursor = true, | |
| shouldCursorBlink = true, | |
| onOtpModified = { value, otpFilled -> | |
| otpText = value | |
| if (otpFilled) { | |
| print("OTP filled") | |
| } | |
| }) | |
| Button(onClick = {}) { | |
| Text( | |
| text = "Verify Code" | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| @Composable | |
| fun OtpInputField( | |
| modifier: Modifier = Modifier, | |
| otpText: String = "123456", | |
| otpLength: Int = 6, | |
| shouldShowCursor: Boolean, | |
| shouldCursorBlink: Boolean, | |
| onOtpModified: (String, Boolean) -> Unit = { a, b -> } | |
| ) { | |
| BasicTextField(modifier = modifier, | |
| value = TextFieldValue(otpText, selection = TextRange(otpText.length)), | |
| onValueChange = { | |
| if (it.text.length <= otpLength) { | |
| onOtpModified(it.text, it.text.length == otpLength) | |
| } | |
| }, | |
| keyboardOptions = KeyboardOptions( | |
| keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done | |
| ), | |
| decorationBox = { | |
| Row( | |
| modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween | |
| ) { | |
| repeat(otpLength) { index -> | |
| CharacterContainer( | |
| index, otpText, shouldShowCursor, shouldCursorBlink | |
| ) | |
| } | |
| } | |
| }) | |
| } | |
| @Composable | |
| fun CharacterContainer( | |
| index: Int, | |
| otpText: String, | |
| shouldShowCursor: Boolean, | |
| shouldCursorBlink: Boolean, | |
| transformationChar: String = "*" | |
| ) { | |
| val isFocused = otpText.length == index | |
| val character = when { | |
| index < otpText.length -> { | |
| transformationChar | |
| } | |
| else -> "" | |
| } | |
| val cursorVisible = remember { | |
| mutableStateOf(shouldShowCursor) | |
| } | |
| LaunchedEffect( | |
| key1 = isFocused | |
| ) { | |
| if (isFocused && shouldShowCursor && shouldCursorBlink) { | |
| while (true) { | |
| delay(500) //Adjust blinking speed here | |
| cursorVisible.value = !cursorVisible.value | |
| } | |
| } | |
| } | |
| Box( | |
| modifier = Modifier.border(1.dp, Color.Red, RoundedCornerShape(10.dp)), | |
| contentAlignment = Alignment.Center | |
| ) { | |
| Text( | |
| modifier = Modifier.width(55.dp), | |
| text = character, | |
| style = MaterialTheme.typography.displayMedium, | |
| color = Color.Black, | |
| textAlign = TextAlign.Center | |
| ) | |
| AnimatedVisibility( | |
| visible = isFocused && cursorVisible.value | |
| ) { | |
| Box( | |
| modifier = Modifier | |
| .align(Alignment.Center) | |
| .width(2.dp) | |
| .height(25.dp) | |
| .background(Color.Red) | |
| ) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment