Create pin : start handling the text field

This commit is contained in:
ganfra
2023-10-18 21:20:47 +02:00
parent 53feff04a1
commit f07a687630
9 changed files with 230 additions and 2 deletions

View File

@@ -17,5 +17,5 @@
package io.element.android.features.lockscreen.impl.create
sealed interface CreatePinEvents {
object MyEvent : CreatePinEvents
data class OnPinEntryChanged(val entryAsText: String) : CreatePinEvents
}

View File

@@ -38,6 +38,7 @@ class CreatePinNode @AssistedInject constructor(
val state = presenter.present()
CreatePinView(
state = state,
onBackClicked = { },
modifier = modifier
)
}

View File

@@ -17,6 +17,10 @@
package io.element.android.features.lockscreen.impl.create
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import io.element.android.features.lockscreen.impl.create.model.PinEntry
import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject
@@ -24,14 +28,20 @@ class CreatePinPresenter @Inject constructor() : Presenter<CreatePinState> {
@Composable
override fun present(): CreatePinState {
val pinEntry by remember {
mutableStateOf(PinEntry.empty(4))
}
fun handleEvents(event: CreatePinEvents) {
when (event) {
CreatePinEvents.MyEvent -> Unit
is CreatePinEvents.OnPinEntryChanged -> {
pinEntry.fillWith(event.entryAsText)
}
}
}
return CreatePinState(
pinEntry = pinEntry,
eventSink = ::handleEvents
)
}

View File

@@ -16,6 +16,9 @@
package io.element.android.features.lockscreen.impl.create
import io.element.android.features.lockscreen.impl.create.model.PinEntry
data class CreatePinState(
val pinEntry: PinEntry,
val eventSink: (CreatePinEvents) -> Unit
)

View File

@@ -17,6 +17,9 @@
package io.element.android.features.lockscreen.impl.create
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.lockscreen.impl.create.model.PinDigit
import io.element.android.features.lockscreen.impl.create.model.PinEntry
import kotlinx.collections.immutable.persistentListOf
open class CreatePinStateProvider : PreviewParameterProvider<CreatePinState> {
override val values: Sequence<CreatePinState>
@@ -27,5 +30,13 @@ open class CreatePinStateProvider : PreviewParameterProvider<CreatePinState> {
}
fun aCreatePinState() = CreatePinState(
pinEntry = PinEntry(
digits = persistentListOf(
PinDigit.Filled('1'),
PinDigit.Filled('2'),
PinDigit.Empty,
PinDigit.Empty,
)
),
eventSink = {}
)

View File

@@ -18,15 +18,30 @@
package io.element.android.features.lockscreen.impl.create
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.Row
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.lockscreen.impl.create.model.PinDigit
import io.element.android.features.lockscreen.impl.create.model.PinEntry
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -34,7 +49,10 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.pinDigitBg
import io.element.android.libraries.theme.ElementTheme
@Composable
fun CreatePinView(
@@ -59,6 +77,7 @@ fun CreatePinView(
.consumeWindowInsets(padding),
header = { CreatePinHeader() },
footer = { CreatePinFooter() },
content = { CreatePinContent(state) }
)
}
)
@@ -87,6 +106,89 @@ private fun CreatePinFooter() {
)
}
@Composable
private fun CreatePinContent(
state: CreatePinState,
modifier: Modifier = Modifier,
) {
PinEntryTextField(
state.pinEntry,
onValueChange = {
state.eventSink(CreatePinEvents.OnPinEntryChanged(it))
},
modifier = modifier
.padding(top = 36.dp)
.fillMaxWidth()
)
}
@Composable
fun PinEntryTextField(
pinEntry: PinEntry,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
BasicTextField(
modifier = modifier,
value = TextFieldValue(pinEntry.toText()),
onValueChange = {
onValueChange(it.text)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
decorationBox = {
PinEntryRow(pinEntry = pinEntry)
}
)
}
@Composable
private fun PinEntryRow(
pinEntry: PinEntry,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
for (digit in pinEntry.digits) {
PinDigitView(digit = digit)
}
}
}
@Composable
private fun PinDigitView(
digit: PinDigit,
modifier: Modifier = Modifier,
) {
val shape = RoundedCornerShape(8.dp)
val appearanceModifier = when (digit) {
PinDigit.Empty -> {
Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape)
}
is PinDigit.Filled -> {
Modifier.background(ElementTheme.colors.pinDigitBg, shape)
}
}
Box(
modifier = modifier
.size(40.dp, 50.dp)
.then(appearanceModifier),
contentAlignment = Alignment.Center,
) {
if (digit is PinDigit.Filled) {
Text(
text = digit.toText(),
style = ElementTheme.typography.fontHeadingMdBold
)
}
}
}
@Composable
@PreviewsDayNight
internal fun CreatePinViewPreview(@PreviewParameter(CreatePinStateProvider::class) state: CreatePinState) {

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.create.model
sealed interface PinDigit {
data object Empty : PinDigit
data class Filled(val value: Char) : PinDigit
fun toText(): String {
return when (this) {
is Empty -> ""
is Filled -> value.toString()
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.lockscreen.impl.create.model
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
data class PinEntry(
val digits: ImmutableList<PinDigit>,
) {
companion object {
fun empty(size: Int): PinEntry {
val digits = List(size) { PinDigit.Empty }
return PinEntry(
digits = digits.toPersistentList()
)
}
}
private val size = digits.size
/**
* Fill the first digits with the given text.
* Can't be more than the size of the PinEntry
* Keep the Empty digits at the end
* @return the new PinEntry
*/
fun fillWith(text: String): PinEntry {
val newDigits = digits.toMutableList()
text.forEachIndexed { index, char ->
if (index < size) {
newDigits[index] = PinDigit.Filled(char)
}
}
return copy(digits = newDigits.toPersistentList())
}
fun isPinComplete(): Boolean {
return digits.all { it is PinDigit.Filled }
}
fun toText(): String {
return digits.joinToString("") {
it.toText()
}
}
}

View File

@@ -98,6 +98,16 @@ val SemanticColors.bgSubtleTertiary
val SemanticColors.temporaryColorBgSpecial
get() = if (isLight) Color(0xFFE4E8F0) else Color(0xFF3A4048)
// This color is not present in Semantic color, so put hard-coded value for now
val SemanticColors.pinDigitBg
get() = if (isLight) {
// We want LightDesignTokens.colorGray300
Color(0xFFF0F2F5)
} else {
// We want DarkDesignTokens.colorGray400
Color(0xFF26282D)
}
@PreviewsDayNight
@Composable
internal fun ColorAliasesPreview() = ElementPreview {