diff --git a/README.md b/README.md index 2655aa5..ae7a7f0 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,9 @@ fun ExampleOtpScreen() { OtpInputField( otp = otpValue, count = 4, - otpBoxModifier = Modifier.border(1.dp, Color.Black).background(Color.White), + otpBoxModifier = Modifier + .border(3.pxToDp(), Color.Black) + .background(Color.White), otpTextType = KeyboardType.Number ) } @@ -47,12 +49,11 @@ Enable character masking for enhanced security, suitable for PINs or passwords: ```kotlin OtpInputField( otp = otpValue, - count = 5, + count = 4, otpTextType = KeyboardType.NumberPassword, otpBoxModifier = Modifier - .border(1.dp, Color.Gray) + .border(3.pxToDp(), Color.Gray) .background(Color.White) - .padding(4.dp) // Padding inside OTP boxes should be handled carefully ) ``` ![Secure Input](readmeassets/secure_input.png?raw=true "Secure Setup") @@ -69,8 +70,7 @@ OtpInputField( count = 5, textColor = Color.White, otpBoxModifier = Modifier - .size(50.dp) - .border(2.dp, Color(0xFF277F51), shape = RoundedCornerShape(4.dp)) + .border(7.pxToDp(), Color(0xFF277F51), shape = RoundedCornerShape(12.pxToDp())) ) ``` ![Boxy Design](readmeassets/boxy_otp_field.png?raw=true "Boxy Design") @@ -84,7 +84,7 @@ OtpInputField( otp = otpValue, count = 5, otpBoxModifier = Modifier - .bottomStroke(color = Color.DarkGray, strokeWidth = 2.dp) + .bottomStroke(color = Color.DarkGray, strokeWidth = 6.pxToDp()) ) ``` ![Underline Design](readmeassets/underline_otp_field.png?raw=true "Underline Design") @@ -102,4 +102,54 @@ OtpInputField( - **textColor**: A `Color` used to set the text color within each OTP box. This parameter provides the ability to customize the color of the text inside the OTP boxes, allowing for better integration with the overall design theme of the application. +### Rationale Behind Using `pxToDp` Instead of `.dp` Directly + +The OTP Field component was initially designed for Android using Jetpack Compose, but with an eye toward compatibility and ease of adaptation for Kotlin Multiplatform Mobile (KMM) projects. This foresight influenced the decision to use pxToDp instead of directly using .dp. The approach was chosen to ensure that the component could be ported to KMM projects with minimal changes, accommodating the unique rendering behaviors on different platforms, especially iOS. + +#### Issue with Standard `.dp` Usage + +While .dp (density-independent pixels) is effective for scaling UI elements appropriately on Android, it often leads to inconsistent sizing on iOS devices within KMM projects. This discrepancy occurs because .dp units do not automatically adjust to the screen densities of iOS devices, leading to UI elements that do not appear as intended when shared between Android and iOS. + +#### Implementing `pxToDp` + +To address these challenges and ensure a seamless user experience across both platforms, the `pxToDp` function was implemented. This custom function calculates density-independent pixels by explicitly considering the screen density at runtime, ensuring that dimensions remain consistent and visually proportionate across all devices. + +```kotlin +@Composable +fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.toPx() } + +@Composable +fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() } +``` + +The adoption of `pxToDp` in the OTP Field component thus addresses a critical challenge in multiplatform development by ensuring that all users, regardless of their device, experience the UI as designed. + +### Kotlin Multiplatform Mobile (KMM) Adaptation + +The OTP Field component, while initially designed for Android, can be adapted for use in Kotlin Multiplatform Mobile projects with some specific modifications. This ensures that the widget is functional and visually consistent on both Android and iOS platforms. + +#### Necessary Modifications for Porting + +To effectively port the OTP Field component to KMM, a key modification involves replacing `LocalConfiguration.current.screenWidthDp` with `LocalWindowInfo.current.containerSize.width`. This change ensures that dimensions are calculated based on the actual container size, which is critical for proper scaling on different devices: + +```kotlin +val screenWidth = LocalWindowInfo.current.containerSize.width +``` + +#### Known Issue on iOS + +There is a recognized issue with the `BasicTextField` on iOS, where the cursor in an empty `BasicTextField` aligns only to the left side (start) when `TextAlign` is set to right (end). This issue can affect the user experience on iOS, particularly in scenarios where text alignment is crucial. The problem is documented in the JetBrains Compose Multiplatform repository: + +- **Similar Issue Link**: [GitHub Issue #4611](https://github.com/JetBrains/compose-multiplatform/issues/4611) + + +

+ Demo +

+ + + + + + diff --git a/app/src/main/java/com/example/otpfield/OtpInputField.kt b/app/src/main/java/com/example/otpfield/OtpInputField.kt index 6bfcee2..65ab94d 100644 --- a/app/src/main/java/com/example/otpfield/OtpInputField.kt +++ b/app/src/main/java/com/example/otpfield/OtpInputField.kt @@ -352,7 +352,7 @@ fun OtpView_Preivew() { mutableStateOf("124") } Column( - modifier = Modifier.padding(20.pxToDp()), + modifier = Modifier.padding(40.pxToDp()), verticalArrangement = Arrangement.spacedBy(20.pxToDp()) ) { OtpInputField( @@ -366,12 +366,11 @@ fun OtpView_Preivew() { OtpInputField( otp = otpValue, - count = 5, + count = 4, otpTextType = KeyboardType.NumberPassword, otpBoxModifier = Modifier - .border(1.pxToDp(), Color.Gray) + .border(3.pxToDp(), Color.Gray) .background(Color.White) - .padding(4.pxToDp()) // Padding inside OTP boxes should be handled carefully ) OtpInputField( @@ -379,14 +378,14 @@ fun OtpView_Preivew() { count = 5, textColor = MaterialTheme.colorScheme.onBackground, otpBoxModifier = Modifier - .border(2.pxToDp(), Color(0xFF277F51), shape = RoundedCornerShape(4.pxToDp())) + .border(7.pxToDp(), Color(0xFF277F51), shape = RoundedCornerShape(12.pxToDp())) ) OtpInputField( otp = otpValue, count = 5, otpBoxModifier = Modifier - .bottomStroke(color = Color.DarkGray, strokeWidth = 2.pxToDp()) + .bottomStroke(color = Color.DarkGray, strokeWidth = 6.pxToDp()) ) } } diff --git a/readmeassets/basic_setup.png b/readmeassets/basic_setup.png index fc037c1..8a23b4c 100644 Binary files a/readmeassets/basic_setup.png and b/readmeassets/basic_setup.png differ diff --git a/readmeassets/boxy_otp_field.png b/readmeassets/boxy_otp_field.png index 2d25d83..6643816 100644 Binary files a/readmeassets/boxy_otp_field.png and b/readmeassets/boxy_otp_field.png differ diff --git a/readmeassets/ios_recording.gif b/readmeassets/ios_recording.gif new file mode 100644 index 0000000..d451909 Binary files /dev/null and b/readmeassets/ios_recording.gif differ diff --git a/readmeassets/secure_input.png b/readmeassets/secure_input.png index e34cf54..1c51f5c 100644 Binary files a/readmeassets/secure_input.png and b/readmeassets/secure_input.png differ diff --git a/readmeassets/underline_otp_field.png b/readmeassets/underline_otp_field.png index 7f0b9c7..032ded1 100644 Binary files a/readmeassets/underline_otp_field.png and b/readmeassets/underline_otp_field.png differ