diff --git a/design/api/current.api b/design/api/current.api index 0e9da44d..29aa3ce7 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -203,6 +203,89 @@ package com.urlaunched.android.design.ui.modifiers { } +package com.urlaunched.android.design.ui.otpcontainer { + + public final class OTPCellScope { + ctor public OTPCellScope(Character? char, boolean isFocused); + method public Character? component1(); + method public boolean component2(); + method public com.urlaunched.android.design.ui.otpcontainer.OTPCellScope copy(Character? char, boolean isFocused); + method public Character? getChar(); + method public boolean isFocused(); + property public final Character? char; + property public final boolean isFocused; + } + + public final class OTPContainerKt { + method @androidx.compose.runtime.Composable public static void OTPContainer(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional androidx.compose.ui.text.TextStyle textStyle, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional boolean isError, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle cellStyle, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors cellColors, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions); + method @androidx.compose.runtime.Composable public static void OTPContainerWithError(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional String? error, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.text.TextStyle errorTextStyle, optional androidx.compose.foundation.layout.PaddingValues errorContentPadding, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle cellStyle, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors cellColors, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions); + } + +} + +package com.urlaunched.android.design.ui.otpcontainer.constants { + + public final class OTPContainerDefaults { + method public androidx.compose.foundation.text.KeyboardOptions getDefaultKeyboardOptions(); + property public final androidx.compose.foundation.text.KeyboardOptions DefaultKeyboardOptions; + field public static final com.urlaunched.android.design.ui.otpcontainer.constants.OTPContainerDefaults INSTANCE; + } + +} + +package com.urlaunched.android.design.ui.otpcontainer.models { + + public final class OTPCellColors { + ctor public OTPCellColors(optional long backgroundColor, optional long emptyBackgroundColor, optional long focusedBackgroundColor, optional long borderColor, optional long emptyBorderColor, optional long focusedBorderColor, optional long errorBorderColor); + method public long component1-0d7_KjU(); + method public long component2-0d7_KjU(); + method public long component3-0d7_KjU(); + method public long component4-0d7_KjU(); + method public long component5-0d7_KjU(); + method public long component6-0d7_KjU(); + method public long component7-0d7_KjU(); + method public com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors copy-4JmcsL4(long backgroundColor, long emptyBackgroundColor, long focusedBackgroundColor, long borderColor, long emptyBorderColor, long focusedBorderColor, long errorBorderColor); + method public long getBackgroundColor(); + method public long getBorderColor(); + method public long getEmptyBackgroundColor(); + method public long getEmptyBorderColor(); + method public long getErrorBorderColor(); + method public long getFocusedBackgroundColor(); + method public long getFocusedBorderColor(); + property public final long backgroundColor; + property public final long borderColor; + property public final long emptyBackgroundColor; + property public final long emptyBorderColor; + property public final long errorBorderColor; + property public final long focusedBackgroundColor; + property public final long focusedBorderColor; + } + + public final class OTPCellStyle { + ctor public OTPCellStyle(optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadowStyle); + method public androidx.compose.ui.graphics.Shape component1(); + method public float component2-D9Ej5fM(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public float component5-D9Ej5fM(); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle? component6(); + method public com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle copy-YLPp7PM(androidx.compose.ui.graphics.Shape shape, float width, float height, float focusedBorderWidth, float unfocusedBorderWidth, com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadowStyle); + method public float getFocusedBorderWidth(); + method public float getHeight(); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle? getShadowStyle(); + method public androidx.compose.ui.graphics.Shape getShape(); + method public float getUnfocusedBorderWidth(); + method public float getWidth(); + property public final float focusedBorderWidth; + property public final float height; + property public final com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadowStyle; + property public final androidx.compose.ui.graphics.Shape shape; + property public final float unfocusedBorderWidth; + property public final float width; + } + +} + package com.urlaunched.android.design.ui.paging { public final class PagingColumnKt { @@ -428,11 +511,36 @@ package com.urlaunched.android.design.ui.scrollbar.controller { package com.urlaunched.android.design.ui.shadow { public final class ShadowKt { + method public static androidx.compose.ui.Modifier shadow(androidx.compose.ui.Modifier, com.urlaunched.android.design.ui.shadow.models.ShadowStyle style); method public static androidx.compose.ui.Modifier shadow(androidx.compose.ui.Modifier, optional long color, optional float alpha, optional float cornersRadius, optional float shadowBlurRadius, optional long offset); } } +package com.urlaunched.android.design.ui.shadow.models { + + public final class ShadowStyle { + ctor public ShadowStyle(optional long color, optional float alpha, optional float cornersRadius, optional float blurRadius, optional long offset); + method public long component1-0d7_KjU(); + method public float component2(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public long component5-RKDOV3M(); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle copy-w1ByDHw(long color, float alpha, float cornersRadius, float blurRadius, long offset); + method public float getAlpha(); + method public float getBlurRadius(); + method public long getColor(); + method public float getCornersRadius(); + method public long getOffset(); + property public final float alpha; + property public final float blurRadius; + property public final long color; + property public final float cornersRadius; + property public final long offset; + } + +} + package com.urlaunched.android.design.ui.shimmer { public final class ShimmerKt { diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt new file mode 100644 index 00000000..d78ac651 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt @@ -0,0 +1,6 @@ +package com.urlaunched.android.design.ui.otpcontainer + +data class OTPCellScope( + val char: Char?, + val isFocused: Boolean +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt new file mode 100644 index 00000000..c54dae37 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt @@ -0,0 +1,274 @@ +package com.urlaunched.android.design.ui.otpcontainer + +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.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +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.DpOffset +import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.modifiers.ifNotNull +import com.urlaunched.android.design.ui.otpcontainer.constants.OTPContainerDefaults +import com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors +import com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle +import com.urlaunched.android.design.ui.shadow.shadow + +private const val DEFAULT_OTP_LENGTH = 6 + +@Composable +fun OTPContainerWithError( + modifier: Modifier = Modifier, + otpText: String, + onOtpTextChange: (text: String) -> Unit, + error: String? = null, + textStyle: TextStyle = LocalTextStyle.current, + errorTextStyle: TextStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.error), + errorContentPadding: PaddingValues = PaddingValues(top = Dimens.spacingTiny), + otpLength: Int = DEFAULT_OTP_LENGTH, + cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), + cellStyle: OTPCellStyle = OTPCellStyle(), + cellColors: OTPCellColors = OTPCellColors(), + keyboardOptions: KeyboardOptions = OTPContainerDefaults.DefaultKeyboardOptions, + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + Column( + modifier = modifier + ) { + OTPContainer( + modifier = Modifier.fillMaxWidth(), + otpText = otpText, + onOtpTextChange = onOtpTextChange, + otpLength = otpLength, + cellsArrangement = cellsArrangement, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + cellContent = { + OTPCell( + isError = error != null, + style = cellStyle, + colors = cellColors, + textStyle = textStyle + ) + } + ) + + if (!error.isNullOrEmpty()) { + Text( + text = error, + style = errorTextStyle, + modifier = Modifier.padding(errorContentPadding) + ) + } + } +} + +@Composable +fun OTPContainer( + modifier: Modifier = Modifier, + otpText: String, + onOtpTextChange: (text: String) -> Unit, + textStyle: TextStyle = LocalTextStyle.current, + otpLength: Int = DEFAULT_OTP_LENGTH, + cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), + isError: Boolean = false, + cellStyle: OTPCellStyle = OTPCellStyle(), + cellColors: OTPCellColors = OTPCellColors(), + keyboardOptions: KeyboardOptions = OTPContainerDefaults.DefaultKeyboardOptions, + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + OTPContainer( + modifier = modifier, + otpText = otpText, + onOtpTextChange = onOtpTextChange, + otpLength = otpLength, + cellsArrangement = cellsArrangement, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + cellContent = { + OTPCell( + isError = isError, + style = cellStyle, + colors = cellColors, + textStyle = textStyle + ) + } + ) +} + +@Composable +private fun OTPContainer( + modifier: Modifier = Modifier, + otpText: String, + onOtpTextChange: (text: String) -> Unit, + otpLength: Int = DEFAULT_OTP_LENGTH, + cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), + keyboardOptions: KeyboardOptions = OTPContainerDefaults.DefaultKeyboardOptions, + keyboardActions: KeyboardActions = KeyboardActions.Default, + cellContent: @Composable OTPCellScope.() -> Unit +) { + BasicTextField( + modifier = modifier, + value = TextFieldValue(otpText, selection = TextRange(otpText.length)), + onValueChange = { + if (it.text.length <= otpLength) { + onOtpTextChange(it.text) + } + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + decorationBox = { + Row( + horizontalArrangement = cellsArrangement, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(otpLength) { index -> + cellContent( + OTPCellScope( + char = otpText.getOrNull(index), + isFocused = otpText.length == index + ) + ) + } + } + } + ) +} + +@Composable +private fun OTPCellScope.OTPCell( + modifier: Modifier = Modifier, + isError: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + style: OTPCellStyle = OTPCellStyle(), + colors: OTPCellColors = OTPCellColors() +) { + val char = char?.toString().orEmpty() + + val currentBorderColor = when { + isError -> colors.errorBorderColor + isFocused -> colors.focusedBorderColor + char.isNotBlank() -> colors.borderColor + else -> colors.emptyBorderColor + } + + val borderWidth = if (isFocused) { + style.focusedBorderWidth + } else { + style.unfocusedBorderWidth + } + + val backgroundColor = when { + isFocused -> colors.focusedBackgroundColor + char.isNotBlank() -> colors.backgroundColor + else -> colors.emptyBackgroundColor + } + + Box( + modifier = modifier + .size(width = style.width, height = style.height) + .ifNotNull(style.shadowStyle) { Modifier.shadow(it) } + .clip(style.shape) + .background(backgroundColor) + .border(borderWidth, currentBorderColor, style.shape), + contentAlignment = Alignment.Center + ) { + Text( + text = char, + style = textStyle, + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +private fun OTPContainerPreview() { + var textFieldValue by remember { mutableStateOf("1234") } + + Box( + modifier = Modifier.background(Color.LightGray) + ) { + OTPContainer( + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), + otpText = textFieldValue, + onOtpTextChange = { textFieldValue = it }, + cellsArrangement = Arrangement.SpaceBetween + ) + } +} + +@Preview +@Composable +private fun OTPContainerShadowPreview() { + var textFieldValue by remember { mutableStateOf("12") } + + Box( + modifier = Modifier.background(Color.LightGray) + ) { + OTPContainer( + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), + otpText = textFieldValue, + cellStyle = OTPCellStyle( + shadowStyle = ShadowStyle( + color = Color.Black, + blurRadius = 6.dp, + offset = DpOffset(x = 0.dp, y = 2.dp), + alpha = 0.20f + ) + ), + onOtpTextChange = { textFieldValue = it }, + cellsArrangement = Arrangement.SpaceBetween + ) + } +} + +@Preview +@Composable +private fun OTPContainerErrorPreview() { + var textFieldValue by remember { mutableStateOf("123456") } + + Box( + modifier = Modifier.background(Color.LightGray) + ) { + OTPContainerWithError( + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), + otpText = textFieldValue, + error = "Something went wrong, try another one", + onOtpTextChange = { textFieldValue = it }, + cellsArrangement = Arrangement.SpaceBetween + ) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPCellDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPCellDimens.kt new file mode 100644 index 00000000..bbe7241a --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPCellDimens.kt @@ -0,0 +1,9 @@ +package com.urlaunched.android.design.ui.otpcontainer.constants + +import androidx.compose.ui.unit.dp + +internal object OTPCellDimens { + val defaultOtpCellHeight = 48.dp + val defaultOtpCellWidth = 40.dp + val defaultBorderWidth = 1.dp +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPContainerDefaults.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPContainerDefaults.kt new file mode 100644 index 00000000..635cb792 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPContainerDefaults.kt @@ -0,0 +1,12 @@ +package com.urlaunched.android.design.ui.otpcontainer.constants + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType + +object OTPContainerDefaults { + val DefaultKeyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.NumberPassword, + imeAction = ImeAction.Done + ) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellColors.kt new file mode 100644 index 00000000..e85d180f --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellColors.kt @@ -0,0 +1,13 @@ +package com.urlaunched.android.design.ui.otpcontainer.models + +import androidx.compose.ui.graphics.Color + +data class OTPCellColors( + val backgroundColor: Color = Color.White, + val emptyBackgroundColor: Color = backgroundColor, + val focusedBackgroundColor: Color = backgroundColor, + val borderColor: Color = Color.LightGray, + val emptyBorderColor: Color = borderColor, + val focusedBorderColor: Color = borderColor, + val errorBorderColor: Color = Color.Red +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellStyle.kt new file mode 100644 index 00000000..b05af9f7 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellStyle.kt @@ -0,0 +1,17 @@ +package com.urlaunched.android.design.ui.otpcontainer.models + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.otpcontainer.constants.OTPCellDimens +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle + +data class OTPCellStyle( + val shape: Shape = RoundedCornerShape(Dimens.cornerRadiusSmall), + val width: Dp = OTPCellDimens.defaultOtpCellWidth, + val height: Dp = OTPCellDimens.defaultOtpCellHeight, + val focusedBorderWidth: Dp = OTPCellDimens.defaultBorderWidth, + val unfocusedBorderWidth: Dp = OTPCellDimens.defaultBorderWidth, + val shadowStyle: ShadowStyle? = null +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt b/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt index 1a82bf7b..afb142e9 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt @@ -9,6 +9,15 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle + +fun Modifier.shadow(style: ShadowStyle) = this.shadow( + color = style.color, + alpha = style.alpha, + cornersRadius = style.cornersRadius, + shadowBlurRadius = style.blurRadius, + offset = style.offset +) fun Modifier.shadow( color: Color = Color.Black, diff --git a/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt new file mode 100644 index 00000000..ed8979bb --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt @@ -0,0 +1,14 @@ +package com.urlaunched.android.design.ui.shadow.models + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +data class ShadowStyle( + val color: Color = Color.Black, + val alpha: Float = 1f, + val cornersRadius: Dp = 0.dp, + val blurRadius: Dp = 0.dp, + val offset: DpOffset = DpOffset.Zero +) \ No newline at end of file