Actual text fields for epoch converter

This commit is contained in:
Sad Ellie 2023-02-04 00:25:20 +04:00
parent 52d7db579c
commit 7f664c21fd
8 changed files with 202 additions and 303 deletions

View File

@ -18,24 +18,22 @@
package com.sadellie.unitto.feature.epoch package com.sadellie.unitto.feature.epoch
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.ui.common.UnittoTopAppBar import com.sadellie.unitto.core.ui.common.UnittoTopAppBar
import com.sadellie.unitto.core.ui.common.PortraitLandscape import com.sadellie.unitto.core.ui.common.PortraitLandscape
import com.sadellie.unitto.feature.epoch.component.DateTextField
import com.sadellie.unitto.feature.epoch.component.EpochKeyboard import com.sadellie.unitto.feature.epoch.component.EpochKeyboard
import com.sadellie.unitto.feature.epoch.component.TopPart import com.sadellie.unitto.feature.epoch.component.TopPart
import com.sadellie.unitto.feature.epoch.component.UnixTextField
@Composable @Composable
internal fun EpochRoute( internal fun EpochRoute(
@ -57,8 +55,8 @@ internal fun EpochRoute(
private fun EpochScreen( private fun EpochScreen(
navigateUpAction: () -> Unit, navigateUpAction: () -> Unit,
uiState: EpochUIState, uiState: EpochUIState,
addSymbol: (String) -> Unit, addSymbol: (String, Int) -> Unit,
deleteSymbol: () -> Unit, deleteSymbol: (Int) -> Unit,
clearSymbols: () -> Unit, clearSymbols: () -> Unit,
swap: () -> Unit swap: () -> Unit
) { ) {
@ -66,55 +64,41 @@ private fun EpochScreen(
title = stringResource(R.string.epoch_converter), title = stringResource(R.string.epoch_converter),
navigateUpAction = navigateUpAction navigateUpAction = navigateUpAction
) { padding -> ) { padding ->
var selection: TextRange by remember { mutableStateOf(TextRange.Zero) }
PortraitLandscape( PortraitLandscape(
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding),
content1 = { content1 = { topContentModifier ->
TopPart( TopPart(
modifier = it, modifier = topContentModifier.padding(horizontal = 8.dp),
swap = swap, dateToUnix = uiState.dateToUnix,
unixToDate = !uiState.dateToUnix, dateValue = uiState.dateField,
dateField = { unixValue = uiState.unixField,
Column( swap = {
modifier = Modifier swap()
.background(MaterialTheme.colorScheme.background) selection = TextRange.Zero
.animateItemPlacement()
) {
DateTextField(
modifier = Modifier.fillMaxWidth(),
date = uiState.dateField
)
Text(
text = "date",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End
)
}
}, },
unixField = { selection = selection,
Column( onCursorChange = { selection = it.selection }
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.animateItemPlacement()
) {
UnixTextField(
modifier = Modifier.fillMaxWidth(),
unix = uiState.unixField
)
Text(
text = "unix",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End
)
}
}
) )
}, },
content2 = { content2 = { bottomModifier ->
EpochKeyboard( EpochKeyboard(
modifier = it, modifier = bottomModifier,
addSymbol = addSymbol, addSymbol = {
clearSymbols = clearSymbols, addSymbol(it, selection.start)
deleteSymbol = deleteSymbol selection = TextRange(selection.start + 1)
},
clearSymbols = {
clearSymbols()
selection = TextRange.Zero
},
deleteSymbol = {
if (selection.start != 0) {
deleteSymbol(selection.start - 1)
selection = TextRange(selection.start - 1)
}
}
) )
} }
) )

View File

@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import java.lang.Integer.min
import javax.inject.Inject import javax.inject.Inject
data class EpochUIState( data class EpochUIState(
@ -68,12 +69,17 @@ class EpochViewModel @Inject constructor() : ViewModel() {
viewModelScope, SharingStarted.WhileSubscribed(5000L), EpochUIState() viewModelScope, SharingStarted.WhileSubscribed(5000L), EpochUIState()
) )
fun addSymbol(symbol: String) { fun addSymbol(symbol: String, position: Int) {
val maxSymbols = if (_fromDateToUnix.value) 14 else 10 val maxSymbols = if (_fromDateToUnix.value) 14 else 10
if (_input.value.length < maxSymbols) _input.update { it + symbol } if (_input.value.length >= maxSymbols) return
_input.update { it.replaceRange(position, position, symbol) }
} }
fun deleteSymbol() = _input.update { it.dropLast(1) } fun deleteSymbol(position: Int) {
_input.update {
it.removeRange(position, min(position + 1, _input.value.length))
}
}
fun clearSymbols() = _input.update { "" } fun clearSymbols() = _input.update { "" }

View File

@ -1,154 +0,0 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.epoch.component
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.with
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.MaterialTheme
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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
import com.sadellie.unitto.feature.epoch.R
import kotlinx.coroutines.delay
@Composable
fun DateTextField(
modifier: Modifier,
date: String
) {
val inputWithPadding: String = date.padEnd(14, '0')
fun inFocus(range: Int): Boolean = date.length > range
Column(
modifier = modifier
.height(IntrinsicSize.Min)
.horizontalScroll(rememberScrollState()),
horizontalAlignment = Alignment.End
) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
// Hour
AnimatedText(text = inputWithPadding[0].toString(), focus = inFocus(0))
AnimatedText(text = inputWithPadding[1].toString(), focus = inFocus(1))
// Divider
SimpleText(text = ":", focus = inFocus(2))
// Minute
AnimatedText(text = inputWithPadding[2].toString(), focus = inFocus(2))
AnimatedText(text = inputWithPadding[3].toString(), focus = inFocus(3))
// Divider
SimpleText(text = ":", focus = inFocus(4))
// Second
AnimatedText(text = inputWithPadding[4].toString(), focus = inFocus(4))
AnimatedText(text = inputWithPadding[5].toString(), focus = inFocus(5))
}
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
Row {
AnimatedText(text = inputWithPadding[6].toString(), focus = inFocus(6))
AnimatedText(text = inputWithPadding[7].toString(), focus = inFocus(7))
SimpleText(text = stringResource(R.string.day_short), focus = inFocus(6))
}
Row {
AnimatedText(text = inputWithPadding[8].toString(), focus = inFocus(8))
AnimatedText(text = inputWithPadding[9].toString(), focus = inFocus(9))
SimpleText(text = stringResource(R.string.month_short), focus = inFocus(8))
}
Row {
AnimatedText(text = inputWithPadding[10].toString(), focus = inFocus(10))
AnimatedText(text = inputWithPadding[11].toString(), focus = inFocus(11))
AnimatedText(text = inputWithPadding[12].toString(), focus = inFocus(12))
AnimatedText(text = inputWithPadding[13].toString(), focus = inFocus(13))
SimpleText(text = stringResource(R.string.year_short), focus = inFocus(10))
}
}
}
}
@Composable
private fun AnimatedText(text: String, focus: Boolean) {
AnimatedContent(
targetState = text,
transitionSpec = {
if (targetState.toInt() > initialState.toInt()) {
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
SizeTransform(clip = false)
)
}
) {
SimpleText(text = text, focus = focus)
}
}
@Composable
private fun SimpleText(text: String, focus: Boolean) {
val color = animateColorAsState(
if (focus) MaterialTheme.colorScheme.onBackground else MaterialTheme.colorScheme.outline
)
Text(
text = text,
style = NumbersTextStyleDisplayMedium,
color = color.value
)
}
@Preview
@Composable
private fun PreviewDateTextField() {
var date: String by remember { mutableStateOf("2") }
DateTextField(modifier = Modifier, date = date)
LaunchedEffect(Unit) {
"3550002011999".forEach {
date += it.toString()
delay(2500L)
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.epoch.component
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
internal object DateVisTrans : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText = TransformedText(
text = AnnotatedString(text.text.toDateMask()),
offsetMapping = offsetMapping
)
private val offsetMapping = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (offset <= 2) return offset
if (offset <= 4) return offset + 1
if (offset <= 6) return offset + 2
if (offset <= 8) return offset + 3
if (offset <= 10) return offset + 4
if (offset <= 14) return offset + 5
return 20
}
override fun transformedToOriginal(offset: Int): Int {
if (offset <= 2) return offset
if (offset <= 5) return offset - 1
if (offset <= 9) return offset - 2
if (offset <= 11) return offset - 3
if (offset <= 14) return offset - 4
if (offset <= 19) return offset - 5
return 14
}
}
}
internal fun String.toDateMask(): String {
var maskedText = this
if (maskedText.length > 2) maskedText = maskedText.replaceRange(2, 2, ":")
if (maskedText.length > 5) maskedText = maskedText.replaceRange(5, 5, ":")
if (maskedText.length > 8) maskedText = maskedText.replaceRange(8, 8, "\n")
if (maskedText.length > 10) maskedText = maskedText.replaceRange(11, 11, "d")
if (maskedText.length > 13) maskedText = maskedText.replaceRange(14, 14, "m")
if (maskedText.length > 18) maskedText = maskedText.replaceRange(19, 19, "y")
return maskedText
}

View File

@ -25,8 +25,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -41,7 +39,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
internal fun SwapButton(swap: () -> Unit) { internal fun SwapButton(
modifier: Modifier,
swap: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState() val isPressed by interactionSource.collectIsPressedAsState()
val cornerRadius: Int by animateIntAsState( val cornerRadius: Int by animateIntAsState(
@ -52,9 +53,7 @@ internal fun SwapButton(swap: () -> Unit) {
Button( Button(
onClick = swap, onClick = swap,
shape = RoundedCornerShape(cornerRadius), shape = RoundedCornerShape(cornerRadius),
modifier = Modifier modifier = modifier,
.fillMaxWidth()
.padding(horizontal = 8.dp),
contentPadding = PaddingValues(vertical = 26.dp, horizontal = 8.dp), contentPadding = PaddingValues(vertical = 26.dp, horizontal = 8.dp),
interactionSource = interactionSource interactionSource = interactionSource
) { ) {

View File

@ -18,37 +18,108 @@
package com.sadellie.unitto.feature.epoch.component package com.sadellie.unitto.feature.epoch.component
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
@Composable @Composable
fun TopPart( fun TopPart(
modifier: Modifier, modifier: Modifier,
unixToDate: Boolean, dateToUnix: Boolean,
swap: () -> Unit, swap: () -> Unit,
dateField: @Composable() (LazyItemScope.() -> Unit), dateValue: String,
unixField: @Composable() (LazyItemScope.() -> Unit), unixValue: String,
selection: TextRange,
onCursorChange: (TextFieldValue) -> Unit
) { ) {
Column( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
LazyColumn( Crossfade(dateToUnix) {
verticalArrangement = Arrangement.Bottom if (it) {
) { DateUnixTextFields(
if (unixToDate) { fromTextFieldValue = TextFieldValue(text = dateValue, selection = selection),
item("unix") { unixField() } onCursorChange = onCursorChange,
item("date") { dateField() } fromSupportText = "date",
toTextValue = unixValue,
toSupportText = "unix",
visualTransformation = DateVisTrans,
fromPlaceholderText = dateValue.padEnd(14, '0').toDateMask(),
toPlaceholderText = "0"
)
} else { } else {
item("date") { dateField() } val dateMasked = dateValue.padEnd(14, '0').toDateMask()
item("unix") { unixField() } DateUnixTextFields(
fromTextFieldValue = TextFieldValue(text = unixValue, selection = selection),
onCursorChange = onCursorChange,
fromSupportText = "unix",
toTextValue = dateMasked,
toSupportText = "date",
visualTransformation = VisualTransformation.None,
fromPlaceholderText = if (unixValue.isEmpty()) "0" else "",
toPlaceholderText = dateMasked,
)
} }
} }
SwapButton(swap) SwapButton(modifier = Modifier.fillMaxWidth(), swap = swap)
}
}
@Composable
fun DateUnixTextFields(
fromTextFieldValue: TextFieldValue,
onCursorChange: (TextFieldValue) -> Unit,
fromSupportText: String,
toTextValue: String,
toSupportText: String,
visualTransformation: VisualTransformation,
fromPlaceholderText: String,
toPlaceholderText: String
) {
Column {
CompositionLocalProvider(
LocalTextInputService provides null
) {
BasicTextField(
value = fromTextFieldValue,
onValueChange = onCursorChange,
textStyle = NumbersTextStyleDisplayMedium.copy(textAlign = TextAlign.Start),
minLines = 1,
maxLines = 2,
visualTransformation = visualTransformation,
decorationBox = { innerTextField ->
Text(
text = fromPlaceholderText,
minLines = 1,
maxLines = 2,
style = NumbersTextStyleDisplayMedium,
color = MaterialTheme.colorScheme.outline,
textAlign = TextAlign.Start
)
innerTextField()
}
)
}
Text(text = fromSupportText)
Text(
text = toTextValue.ifEmpty { toPlaceholderText },
style = NumbersTextStyleDisplayMedium,
)
Text(text = toSupportText)
} }
} }

View File

@ -1,73 +0,0 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.epoch.component
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.with
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
@Composable
fun UnixTextField(
modifier: Modifier,
unix: String
) {
Row(
modifier = modifier
.height(IntrinsicSize.Min)
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.End
) {
AnimatedContent(
targetState = unix.ifEmpty { "0" },
transitionSpec = {
if (targetState.toBigDecimal() > initialState.toBigDecimal()) {
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
SizeTransform(clip = false)
)
}
) {
Text(
text = it,
modifier = modifier,
style = NumbersTextStyleDisplayMedium,
textAlign = TextAlign.End
)
}
}
}

View File

@ -10,7 +10,7 @@ androidxTestRunner = "1.5.1"
androidxTestRules = "1.5.0" androidxTestRules = "1.5.0"
orgRobolectric = "4.9" orgRobolectric = "4.9"
orgJetbrainsKotlinxCoroutinesTest = "1.6.4" orgJetbrainsKotlinxCoroutinesTest = "1.6.4"
androidxCompose = "1.4.0-alpha02" androidxCompose = "1.4.0-alpha05"
androidxComposeCompiler = "1.4.0" androidxComposeCompiler = "1.4.0"
androidxComposeUi = "1.4.0-alpha05" androidxComposeUi = "1.4.0-alpha05"
androidxNavigation = "2.5.3" androidxNavigation = "2.5.3"