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
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.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.Modifier
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.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.ui.common.UnittoTopAppBar
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.TopPart
import com.sadellie.unitto.feature.epoch.component.UnixTextField
@Composable
internal fun EpochRoute(
@ -57,8 +55,8 @@ internal fun EpochRoute(
private fun EpochScreen(
navigateUpAction: () -> Unit,
uiState: EpochUIState,
addSymbol: (String) -> Unit,
deleteSymbol: () -> Unit,
addSymbol: (String, Int) -> Unit,
deleteSymbol: (Int) -> Unit,
clearSymbols: () -> Unit,
swap: () -> Unit
) {
@ -66,55 +64,41 @@ private fun EpochScreen(
title = stringResource(R.string.epoch_converter),
navigateUpAction = navigateUpAction
) { padding ->
var selection: TextRange by remember { mutableStateOf(TextRange.Zero) }
PortraitLandscape(
modifier = Modifier.padding(padding),
content1 = {
content1 = { topContentModifier ->
TopPart(
modifier = it,
swap = swap,
unixToDate = !uiState.dateToUnix,
dateField = {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.animateItemPlacement()
) {
DateTextField(
modifier = Modifier.fillMaxWidth(),
date = uiState.dateField
)
Text(
text = "date",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End
)
}
modifier = topContentModifier.padding(horizontal = 8.dp),
dateToUnix = uiState.dateToUnix,
dateValue = uiState.dateField,
unixValue = uiState.unixField,
swap = {
swap()
selection = TextRange.Zero
},
unixField = {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.animateItemPlacement()
) {
UnixTextField(
modifier = Modifier.fillMaxWidth(),
unix = uiState.unixField
)
Text(
text = "unix",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End
)
}
}
selection = selection,
onCursorChange = { selection = it.selection }
)
},
content2 = {
content2 = { bottomModifier ->
EpochKeyboard(
modifier = it,
addSymbol = addSymbol,
clearSymbols = clearSymbols,
deleteSymbol = deleteSymbol
modifier = bottomModifier,
addSymbol = {
addSymbol(it, selection.start)
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.stateIn
import kotlinx.coroutines.flow.update
import java.lang.Integer.min
import javax.inject.Inject
data class EpochUIState(
@ -68,12 +69,17 @@ class EpochViewModel @Inject constructor() : ViewModel() {
viewModelScope, SharingStarted.WhileSubscribed(5000L), EpochUIState()
)
fun addSymbol(symbol: String) {
fun addSymbol(symbol: String, position: Int) {
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 { "" }

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

View File

@ -18,37 +18,108 @@
package com.sadellie.unitto.feature.epoch.component
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.layout.fillMaxWidth
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.CompositionLocalProvider
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 com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
@Composable
fun TopPart(
modifier: Modifier,
unixToDate: Boolean,
dateToUnix: Boolean,
swap: () -> Unit,
dateField: @Composable() (LazyItemScope.() -> Unit),
unixField: @Composable() (LazyItemScope.() -> Unit),
dateValue: String,
unixValue: String,
selection: TextRange,
onCursorChange: (TextFieldValue) -> Unit
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
LazyColumn(
verticalArrangement = Arrangement.Bottom
) {
if (unixToDate) {
item("unix") { unixField() }
item("date") { dateField() }
Crossfade(dateToUnix) {
if (it) {
DateUnixTextFields(
fromTextFieldValue = TextFieldValue(text = dateValue, selection = selection),
onCursorChange = onCursorChange,
fromSupportText = "date",
toTextValue = unixValue,
toSupportText = "unix",
visualTransformation = DateVisTrans,
fromPlaceholderText = dateValue.padEnd(14, '0').toDateMask(),
toPlaceholderText = "0"
)
} else {
item("date") { dateField() }
item("unix") { unixField() }
val dateMasked = dateValue.padEnd(14, '0').toDateMask()
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"
orgRobolectric = "4.9"
orgJetbrainsKotlinxCoroutinesTest = "1.6.4"
androidxCompose = "1.4.0-alpha02"
androidxCompose = "1.4.0-alpha05"
androidxComposeCompiler = "1.4.0"
androidxComposeUi = "1.4.0-alpha05"
androidxNavigation = "2.5.3"