From 07fdb03ab37e165a62d5977ab7731a267e08bd9f Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Fri, 3 Mar 2023 23:17:28 +0400 Subject: [PATCH] The most responsive UI - Auto-size text field (animated) - Auto-size keyboard - Auto-size buttons - Fix cursor --- .../unitto/core/ui/common/KeyboardButton.kt | 25 ++- .../core/ui/common/PortraitLandscape.kt | 33 +-- .../unitto/core/ui/common/WithConstraints.kt | 56 +++++ .../FloatingTextActionModeCallback.kt | 2 +- .../ui/common/textfield/InputTextField.kt | 210 ++++++++++++++++++ .../textfield}/UnittoActionModeCallback.kt | 2 +- .../UnittoPrimaryTextActionModeCallback.kt | 2 +- .../ui/common/textfield}/UnittoTextToolbar.kt | 4 +- .../com/sadellie/unitto/core/ui/theme/Type.kt | 3 +- .../feature/calculator/CalculatorScreen.kt | 58 ++--- .../components/CalculatorKeyboard.kt | 60 +++-- .../calculator/components/InputTextField.kt | 100 --------- .../feature/converter/ConverterScreen.kt | 26 ++- .../feature/converter/components/Keyboard.kt | 43 ++-- .../converter/components/MyTextField.kt | 37 +-- .../feature/converter/components/TopScreen.kt | 90 ++++++-- 16 files changed, 500 insertions(+), 251 deletions(-) create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/WithConstraints.kt rename {feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components => core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield}/FloatingTextActionModeCallback.kt (97%) create mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt rename {feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components => core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield}/UnittoActionModeCallback.kt (97%) rename {feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components => core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield}/UnittoPrimaryTextActionModeCallback.kt (96%) rename {feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components => core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield}/UnittoTextToolbar.kt (96%) delete mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt index acbf9a08..58ab5577 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.core.ui.common +import android.content.res.Configuration import android.view.HapticFeedbackConstants import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateIntAsState @@ -38,6 +39,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp @@ -50,7 +52,7 @@ fun BasicKeyboardButton( icon: ImageVector, iconColor: Color, allowVibration: Boolean, - contentPadding: PaddingValues = PaddingValues(24.dp, 8.dp) + contentHeight: Float ) { val view = LocalView.current val interactionSource = remember { MutableInteractionSource() } @@ -66,10 +68,15 @@ fun BasicKeyboardButton( onLongClick = onLongClick, shape = RoundedCornerShape(cornerRadius), containerColor = containerColor, - contentPadding = contentPadding, + contentPadding = PaddingValues(), interactionSource = interactionSource ) { - Icon(icon, null, modifier = Modifier.fillMaxHeight(), tint = iconColor) + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.fillMaxHeight(contentHeight), + tint = iconColor + ) } LaunchedEffect(key1 = isPressed) { @@ -93,6 +100,7 @@ fun KeyboardButtonLight( icon = icon, iconColor = MaterialTheme.colorScheme.onSurfaceVariant, allowVibration = allowVibration, + contentHeight = if (isPortrait()) 0.5f else 0.85f ) } @@ -111,7 +119,8 @@ fun KeyboardButtonFilled( containerColor = MaterialTheme.colorScheme.primaryContainer, icon = icon, iconColor = MaterialTheme.colorScheme.onSecondaryContainer, - allowVibration = allowVibration + allowVibration = allowVibration, + contentHeight = if (isPortrait()) 0.5f else 0.85f ) } @@ -123,17 +132,19 @@ fun KeyboardButtonAdditional( onLongClick: (() -> Unit)? = null, onClick: () -> Unit ) { - BasicKeyboardButton( modifier = modifier .minimumInteractiveComponentSize() .heightIn(max = 48.dp), onClick = onClick, + onLongClick = onLongClick, containerColor = Color.Transparent, icon = icon, iconColor = MaterialTheme.colorScheme.onSurfaceVariant, allowVibration = allowVibration, - contentPadding = PaddingValues(12.dp, 2.dp), - onLongClick = onLongClick + contentHeight = if (isPortrait()) 0.9f else 0.85f ) } + +@Composable +private fun isPortrait() = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/PortraitLandscape.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/PortraitLandscape.kt index abf7d731..0e5847f3 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/PortraitLandscape.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/PortraitLandscape.kt @@ -19,7 +19,6 @@ package com.sadellie.unitto.core.ui.common import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -28,7 +27,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.dp /** * When Portrait mode will place [content1] and [content2] in a [Column]. @@ -42,33 +40,14 @@ fun PortraitLandscape( content2: @Composable (Modifier) -> Unit, ) { if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { - Column( - modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - content1(Modifier) - content2( - Modifier - .fillMaxSize() - .padding(horizontal = 8.dp) - ) + ColumnWithConstraints(modifier) { + content1(Modifier.fillMaxHeight(0.34f).padding(horizontal = it.maxWidth * 0.03f)) + content2(Modifier.fillMaxSize().padding(horizontal = it.maxWidth * 0.03f, vertical = it.maxHeight * 0.015f)) } } else { - Row( - modifier = modifier.fillMaxSize(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - content1( - Modifier - .weight(1f) - .fillMaxHeight() - ) - content2( - Modifier - .weight(1f) - .fillMaxSize() - .padding(horizontal = 8.dp) - ) + RowWithConstraints(modifier) { + content1(Modifier.weight(1f).fillMaxHeight().padding(bottom = it.maxWidth * 0.015f, start = it.maxWidth * 0.015f, end = it.maxWidth * 0.015f)) + content2(Modifier.weight(1f).fillMaxSize().padding(bottom = it.maxWidth * 0.015f, start = it.maxWidth * 0.015f, end = it.maxWidth * 0.015f)) } } } diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/WithConstraints.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/WithConstraints.kt new file mode 100644 index 00000000..09dc46d0 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/WithConstraints.kt @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +package com.sadellie.unitto.core.ui.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun ColumnWithConstraints( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + content: @Composable (ColumnScope.(BoxWithConstraintsScope)-> Unit) +) = BoxWithConstraints(modifier) { + Column( + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment + ) { content(this@BoxWithConstraints) } +} + +@Composable +fun RowWithConstraints( + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, + content: @Composable (RowScope.(BoxWithConstraintsScope)-> Unit) +) = BoxWithConstraints(modifier) { + Row( + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment + ) { content(this@BoxWithConstraints) } +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt similarity index 97% rename from feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt index ab6711d8..07bbac80 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.calculator.components +package com.sadellie.unitto.core.ui.common.textfield import android.view.ActionMode import android.view.Menu diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt new file mode 100644 index 00000000..0d735ad6 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt @@ -0,0 +1,210 @@ +/* + * 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 . + */ + +package com.sadellie.unitto.core.ui.common.textfield + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +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.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalTextInputService +import androidx.compose.ui.platform.LocalTextToolbar +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Paragraph +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import com.sadellie.unitto.core.base.Separator +import com.sadellie.unitto.core.ui.Formatter +import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge +import kotlin.math.ceil +import kotlin.math.roundToInt + +@Composable +fun InputTextField( + modifier: Modifier, + value: TextFieldValue, + textStyle: TextStyle = NumbersTextStyleDisplayLarge, + minRatio: Float = 1f, + cutCallback: () -> Unit, + pasteCallback: (String) -> Unit, + onCursorChange: (IntRange) -> Unit, + textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, +) { + val clipboardManager = LocalClipboardManager.current + fun copyCallback() { + clipboardManager.setText( + AnnotatedString( + Formatter.removeGrouping( + value.annotatedString.subSequence(value.selection).text + ) + ) + ) + } + + CompositionLocalProvider( + LocalTextInputService provides null, + LocalTextToolbar provides UnittoTextToolbar( + view = LocalView.current, + copyCallback = ::copyCallback, + pasteCallback = { + pasteCallback( + Formatter.toSeparator( + clipboardManager.getText()?.text ?: "", Separator.COMMA + ) + ) + }, + cutCallback = { copyCallback(); cutCallback() } + ) + ) { + AutoSizableTextField( + modifier = modifier, + value = value, + onValueChange = { + onCursorChange(it.selection.start..it.selection.end) + }, + textStyle = textStyle, + minRatio = minRatio, + readOnly = false + ) + } +} + +@Composable +fun InputTextField( + modifier: Modifier = Modifier, + value: TextFieldValue, + textStyle: TextStyle = NumbersTextStyleDisplayLarge, + minRatio: Float = 1f, + textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, +) { + AutoSizableTextField( + modifier = modifier, + value = value, + textStyle = textStyle, + minRatio = minRatio, + readOnly = true + ) +} + +@Composable +private fun AutoSizableTextField( + modifier: Modifier = Modifier, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit = {}, + textStyle: TextStyle = TextStyle(), + scaleFactor: Float = 0.95f, + minRatio: Float = 1f, + readOnly: Boolean = false, + textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + cursorBrush: Brush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) +) { + // FIXME Acts strange when minRatio is set to 0 (still scales down). + + val density = LocalDensity.current + + var nFontSize: TextUnit by remember { mutableStateOf(0.sp) } + var minFontSize: TextUnit + + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.BottomStart + ) { + with(density) { + // Cursor handle is not visible without this, 0.836f is the minimum required factor here + nFontSize = maxHeight.toSp() * 0.836f + minFontSize = nFontSize * minRatio + } + + // Modified: https://blog.canopas.com/autosizing-textfield-in-jetpack-compose-7a80f0270853 + val calculateParagraph = @Composable { + Paragraph( + text = value.text, + style = textStyle.copy(fontSize = nFontSize), + constraints = Constraints( + maxWidth = ceil(with(density) { maxWidth.toPx() }).toInt() + ), + density = density, + fontFamilyResolver = createFontFamilyResolver(LocalContext.current), + spanStyles = listOf(), + placeholders = listOf(), + maxLines = 1, + ellipsis = false + ) + } + + var intrinsics = calculateParagraph() + with(density) { + while ((intrinsics.maxIntrinsicWidth > maxWidth.toPx()) && nFontSize >= minFontSize) { + nFontSize *= scaleFactor + intrinsics = calculateParagraph() + } + } + + val nTextStyle = textStyle.copy( + // https://issuetracker.google.com/issues/266470454 + // textAlign = TextAlign.End, + color = textColor, + fontSize = nFontSize + ) + + BasicTextField( + value = value, + singleLine = true, + onValueChange = onValueChange, + modifier = Modifier + .widthIn( + max = with(density) { intrinsics.width.toDp() } + ) + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + // TextField size is changed with a delay (text jumps). Here we correct it. + layout(placeable.width, placeable.height) { + placeable.place( + x = (intrinsics.width - intrinsics.maxIntrinsicWidth) + .coerceAtLeast(0f) + .roundToInt(), + y = (placeable.height - intrinsics.height).roundToInt() + ) + } + }, + textStyle = nTextStyle, + readOnly = readOnly, + cursorBrush = cursorBrush + ) + } +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt similarity index 97% rename from feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt index bb18c16f..f6b8b480 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.calculator.components +package com.sadellie.unitto.core.ui.common.textfield import android.view.ActionMode import android.view.Menu diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt similarity index 96% rename from feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt index 21750f3a..391c6687 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.calculator.components +package com.sadellie.unitto.core.ui.common.textfield import android.view.ActionMode import android.view.Menu diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt similarity index 96% rename from feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt index 46d32b8c..d5edd057 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.calculator.components +package com.sadellie.unitto.core.ui.common.textfield import android.os.Build import android.view.ActionMode @@ -25,7 +25,7 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.platform.TextToolbar import androidx.compose.ui.platform.TextToolbarStatus -internal class UnittoTextToolbar( +class UnittoTextToolbar( private val view: View, private val copyCallback: () -> Unit, private val pasteCallback: (() -> Unit)? = null, diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt index 19dc898a..5b90a70e 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.sadellie.unitto.core.ui.R @@ -42,7 +43,7 @@ val NumbersTextStyleDisplayLarge = TextStyle( fontFamily = Lato, fontWeight = FontWeight.W400, fontSize = 57.sp, - lineHeight = 64.sp, + lineHeight = (1.4).em, letterSpacing = (-0.25).sp, ) diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt index 01b8b82f..afd4c138 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt @@ -26,15 +26,14 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons @@ -65,21 +64,20 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString 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.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sadellie.unitto.core.base.Separator import com.sadellie.unitto.core.ui.Formatter import com.sadellie.unitto.core.ui.common.MenuButton import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar -import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium +import com.sadellie.unitto.core.ui.common.textfield.InputTextField +import com.sadellie.unitto.core.ui.common.textfield.UnittoTextToolbar import com.sadellie.unitto.data.model.HistoryItem import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard import com.sadellie.unitto.feature.calculator.components.DragDownView import com.sadellie.unitto.feature.calculator.components.HistoryList -import com.sadellie.unitto.feature.calculator.components.InputTextField -import com.sadellie.unitto.feature.calculator.components.UnittoTextToolbar import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.* @@ -194,12 +192,13 @@ private fun CalculatorScreen( textFields = { maxDragAmount -> Column( Modifier + .fillMaxHeight(0.25f) .onPlaced { textThingyHeight = it.size.height } .background( MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape( - topStart = 0.dp, topEnd = 0.dp, - bottomStart = 32.dp, bottomEnd = 32.dp + topStartPercent = 0, topEndPercent = 0, + bottomStartPercent = 20, bottomEndPercent = 20 ) ) .draggable( @@ -226,31 +225,33 @@ private fun CalculatorScreen( } } ) - .padding(top = 8.dp), + .padding(top = 12.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { InputTextField( modifier = Modifier + .weight(2f) .fillMaxWidth() .padding(horizontal = 8.dp), - value = uiState.input, - onCursorChange = onCursorChange, + value = uiState.input.copy( + Formatter.fromSeparator(uiState.input.text, Separator.COMMA) + ), + minRatio = 0.5f, + cutCallback = deleteSymbol, pasteCallback = addSymbol, - cutCallback = deleteSymbol + onCursorChange = onCursorChange ) if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { - SelectionContainer { - Text( + SelectionContainer(Modifier.weight(1f)) { + InputTextField( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp) - .horizontalScroll(rememberScrollState(), reverseScrolling = true), - text = Formatter.format(uiState.output), - textAlign = TextAlign.End, - softWrap = false, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - style = NumbersTextStyleDisplayMedium, + .padding(horizontal = 8.dp), + value = TextFieldValue( + Formatter.fromSeparator(uiState.output, Separator.COMMA) + ), + textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f) ) } } @@ -268,7 +269,7 @@ private fun CalculatorScreen( }, numPad = { CalculatorKeyboard( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 4.dp), angleMode = uiState.angleMode, allowVibration = uiState.allowVibration, addSymbol = addSymbol, @@ -314,7 +315,12 @@ private fun CalculatorScreen( } } -@Preview +@Preview(widthDp = 432, heightDp = 1008, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 432, heightDp = 864, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 597, heightDp = 1393, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(heightDp = 432, widthDp = 1008, device = "spec:parent=pixel_5,orientation=landscape") +@Preview(heightDp = 432, widthDp = 864, device = "spec:parent=pixel_5,orientation=landscape") +@Preview(heightDp = 597, widthDp = 1393, device = "spec:parent=pixel_5,orientation=landscape") @Composable private fun PreviewCalculatorScreen() { val dtf = SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.getDefault()) @@ -332,14 +338,14 @@ private fun PreviewCalculatorScreen() { HistoryItem( date = dtf.parse(it)!!, expression = "12345".repeat(10), - result = "67890" + result = "1234" ) } CalculatorScreen( uiState = CalculatorUIState( - input = TextFieldValue("12345"), - output = "12345", + input = TextFieldValue("1.2345"), + output = "1234", history = historyItems ), navigateToMenu = {}, diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt index 37af7526..978bf912 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt @@ -25,11 +25,13 @@ import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess @@ -43,16 +45,19 @@ 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.rotate import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Dp import com.sadellie.unitto.core.base.Token import com.sadellie.unitto.core.ui.Formatter +import com.sadellie.unitto.core.ui.common.ColumnWithConstraints import com.sadellie.unitto.core.ui.common.KeyboardButtonAdditional import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled import com.sadellie.unitto.core.ui.common.KeyboardButtonLight +import com.sadellie.unitto.core.ui.common.RowWithConstraints import com.sadellie.unitto.core.ui.common.key.UnittoIcons import com.sadellie.unitto.core.ui.common.key.unittoicons.AcTan import com.sadellie.unitto.core.ui.common.key.unittoicons.ArCos @@ -150,22 +155,27 @@ private fun PortraitKeyboard( animationSpec = tween(easing = FastOutSlowInEasing) ) - Column( + ColumnWithConstraints( modifier = modifier - ) { + ) { constraints -> + fun verticalFraction(fraction: Float): Dp = constraints.maxHeight * fraction + fun horizontalFraction(fraction: Float): Dp = constraints.maxWidth * fraction + val weightModifier = Modifier.weight(1f) val mainButtonModifier = Modifier .fillMaxSize() .weight(1f) - .padding(4.dp) + .padding(horizontalFraction(0.015f), verticalFraction(0.008f)) val additionalButtonModifier = Modifier .minimumInteractiveComponentSize() .weight(1f) - .heightIn(max = 48.dp) + .height(verticalFraction(0.075f)) + + Spacer(modifier = Modifier.height(verticalFraction(0.015f))) Row( - modifier = Modifier.padding(vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp) + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(horizontalFraction(0.03f)) ) { // Additional buttons Crossfade(invMode, weightModifier) { @@ -192,14 +202,18 @@ private fun PortraitKeyboard( } } - // Expand/Collapse - IconButton( - onClick = { showAdditional = !showAdditional }, - colors = IconButtonDefaults.iconButtonColors(containerColor = MaterialTheme.colorScheme.inverseOnSurface) - ) { - Icon(Icons.Default.ExpandLess, null, Modifier.rotate(expandRotation)) + Box(modifier = Modifier.height(verticalFraction(0.075f)), contentAlignment = Alignment.Center) { + // Expand/Collapse + IconButton( + onClick = { showAdditional = !showAdditional }, + colors = IconButtonDefaults.iconButtonColors(containerColor = MaterialTheme.colorScheme.inverseOnSurface) + ) { + Icon(Icons.Default.ExpandLess, null, Modifier.rotate(expandRotation)) + } } } + + Spacer(modifier = Modifier.height(verticalFraction(0.015f))) Row(weightModifier) { KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) } @@ -232,6 +246,8 @@ private fun PortraitKeyboard( KeyboardButtonLight(mainButtonModifier, UnittoIcons.Backspace, allowVibration, clearSymbols) { deleteSymbol() } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Equal, allowVibration) { evaluate() } } + + Spacer(modifier = Modifier.height(verticalFraction(0.015f))) } } @@ -246,7 +262,7 @@ private fun AdditionalButtonsPortrait( toggleInvMode: () -> Unit ) { Column { - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, UnittoIcons.SquareRootWide, allowVibration) { addSymbol(Token.sqrt) } KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) } KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) } @@ -254,13 +270,13 @@ private fun AdditionalButtonsPortrait( } AnimatedVisibility(showAdditional) { Column { - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, if (angleMode == AngleMode.DEG) UnittoIcons.Deg else UnittoIcons.Rad, allowVibration) { toggleAngleMode() } KeyboardButtonAdditional(modifier, UnittoIcons.Sin, allowVibration) { addSymbol(Token.sin) } KeyboardButtonAdditional(modifier, UnittoIcons.Cos, allowVibration) { addSymbol(Token.cos) } KeyboardButtonAdditional(modifier, UnittoIcons.Tan, allowVibration) { addSymbol(Token.tan) } } - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() } KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) } KeyboardButtonAdditional(modifier, UnittoIcons.Ln, allowVibration) { addSymbol(Token.ln) } @@ -282,7 +298,7 @@ private fun AdditionalButtonsPortraitInverse( toggleInvMode: () -> Unit ) { Column { - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, UnittoIcons.Modulo, allowVibration) { addSymbol(Token.modulo) } KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) } KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) } @@ -290,13 +306,13 @@ private fun AdditionalButtonsPortraitInverse( } AnimatedVisibility(showAdditional) { Column { - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, if (angleMode == AngleMode.DEG) UnittoIcons.Deg else UnittoIcons.Rad, allowVibration) { toggleAngleMode() } KeyboardButtonAdditional(modifier, UnittoIcons.ArSin, allowVibration) { addSymbol(Token.arSin) } KeyboardButtonAdditional(modifier, UnittoIcons.ArCos, allowVibration) { addSymbol(Token.arCos) } KeyboardButtonAdditional(modifier, UnittoIcons.AcTan, allowVibration) { addSymbol(Token.acTan) } } - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() } KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) } KeyboardButtonAdditional(modifier, UnittoIcons.Exp, allowVibration) { addSymbol(Token.exp) } @@ -321,11 +337,11 @@ private fun LandscapeKeyboard( val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma } var invMode: Boolean by remember { mutableStateOf(false) } - Row(modifier) { + RowWithConstraints(modifier) { constraints -> val buttonModifier = Modifier .fillMaxWidth() .weight(1f) - .padding(4.dp) + .padding(constraints.maxWidth * 0.005f, constraints.maxHeight * 0.02f) Crossfade(invMode, Modifier.weight(3f)) { Row { diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt deleted file mode 100644 index e225f790..00000000 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt +++ /dev/null @@ -1,100 +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 . - */ - -package com.sadellie.unitto.feature.calculator.components - -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalTextInputService -import androidx.compose.ui.platform.LocalTextToolbar -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import com.sadellie.unitto.core.base.Separator -import com.sadellie.unitto.core.ui.Formatter -import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge - -@Composable -internal fun InputTextField( - modifier: Modifier, - value: TextFieldValue, - onCursorChange: (IntRange) -> Unit, - pasteCallback: (String) -> Unit, - cutCallback: () -> Unit -) { - val clipboardManager = LocalClipboardManager.current - - val formattedInput: TextFieldValue by remember(value) { - derivedStateOf { - value.copy( - // We replace this because internally input value is already formatted, but uses - // comma as separator. - Formatter.fromSeparator(value.text, Separator.COMMA) - ) - } - } - - fun copyToClipboard() = clipboardManager.setText( - AnnotatedString( - Formatter.removeGrouping( - formattedInput.annotatedString.subSequence(formattedInput.selection).text - ) - ) - ) - - CompositionLocalProvider( - LocalTextInputService provides null, - LocalTextToolbar provides UnittoTextToolbar( - view = LocalView.current, - copyCallback = ::copyToClipboard, - pasteCallback = { - pasteCallback( - Formatter.toSeparator( - clipboardManager.getText()?.text ?: "", Separator.COMMA - ) - ) - }, - cutCallback = { copyToClipboard(); cutCallback() } - ) - ) { - BasicTextField( - modifier = modifier, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), - singleLine = true, - value = formattedInput, - onValueChange = { - onCursorChange(it.selection.start..it.selection.end) - }, - textStyle = NumbersTextStyleDisplayLarge.copy( - textAlign = TextAlign.End, - color = MaterialTheme.colorScheme.onSurfaceVariant - ), - minLines = 1, - maxLines = 1, - ) - } -} diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt index f2fe804c..697ba53e 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.feature.converter +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.MoreVert @@ -30,6 +31,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sadellie.unitto.core.ui.R @@ -91,7 +94,7 @@ private fun ConverterScreen( .centerAlignedTopAppBarColors(containerColor = Color.Transparent), content = { padding -> PortraitLandscape( - modifier = Modifier.padding(padding), + modifier = Modifier.padding(padding).fillMaxSize(), content1 = { TopScreenPart( modifier = it, @@ -125,11 +128,26 @@ private fun ConverterScreen( ) } -@Preview +class PreviewUIState: PreviewParameterProvider { + override val values: Sequence + get() = listOf( + ConverterUIState(inputValue = "1234", calculatedValue = null, resultValue = "5678", showLoading = false), + ConverterUIState(inputValue = "1234", calculatedValue = "234", resultValue = "5678", showLoading = false), + ).asSequence() +} + +@Preview(widthDp = 432, heightDp = 1008, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 432, heightDp = 864, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 597, heightDp = 1393, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(heightDp = 432, widthDp = 1008, device = "spec:parent=pixel_5,orientation=landscape") +@Preview(heightDp = 432, widthDp = 864, device = "spec:parent=pixel_5,orientation=landscape") +@Preview(heightDp = 597, widthDp = 1393, device = "spec:parent=pixel_5,orientation=landscape") @Composable -private fun PreviewConverterScreen() { +private fun PreviewConverterScreen( + @PreviewParameter(PreviewUIState::class) uiState: ConverterUIState +) { ConverterScreen( - uiState = ConverterUIState(), + uiState = ConverterUIState(inputValue = "1234", calculatedValue = null, resultValue = "5678", showLoading = false), navigateToLeftScreen = {}, navigateToRightScreen = {_, _, _ -> }, navigateToSettings = {}, diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt index 7c7bbd63..e999fe20 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt @@ -19,16 +19,16 @@ package com.sadellie.unitto.feature.converter.components import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.sadellie.unitto.core.base.Token import com.sadellie.unitto.core.ui.Formatter +import com.sadellie.unitto.core.ui.common.ColumnWithConstraints import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled import com.sadellie.unitto.core.ui.common.KeyboardButtonLight import com.sadellie.unitto.core.ui.common.key.UnittoIcons @@ -85,7 +85,6 @@ internal fun Keyboard( ConverterMode.BASE -> BaseKeyboard(addDigit, clearInput, deleteDigit, allowVibration) } } - } @Composable @@ -96,39 +95,39 @@ private fun DefaultKeyboard( allowVibration: Boolean ) { val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma } - Column { + ColumnWithConstraints { // Button modifier val bModifier = Modifier .fillMaxSize() .weight(1f) - .padding(4.dp) // Column modifier - val cModifier = Modifier.weight(1f) - Row(cModifier) { + val cModifier = Modifier.weight(1f).padding(vertical = it.maxHeight * 0.01f) + val horizontalArrangement = Arrangement.spacedBy(it.maxWidth * 0.03f) + Row(cModifier, horizontalArrangement) { KeyboardButtonFilled(bModifier, UnittoIcons.LeftBracket, allowVibration) { addDigit(Token.leftBracket) } KeyboardButtonFilled(bModifier, UnittoIcons.RightBracket, allowVibration) { addDigit(Token.rightBracket) } KeyboardButtonFilled(bModifier, UnittoIcons.Exponent, allowVibration) { addDigit(Token.exponent) } KeyboardButtonFilled(bModifier, UnittoIcons.SquareRoot, allowVibration) { addDigit(Token.sqrt) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) } KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) } KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) } KeyboardButtonFilled(bModifier, UnittoIcons.Divide, allowVibration) { addDigit(Token.divide) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) } KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) } KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) } KeyboardButtonFilled(bModifier, UnittoIcons.Multiply, allowVibration) { addDigit(Token.multiply) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) } KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) } KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) } KeyboardButtonFilled(bModifier, UnittoIcons.Minus, allowVibration) { addDigit(Token.minus) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) } KeyboardButtonLight(bModifier, fractionalIcon, allowVibration) { addDigit(Token.dot) } KeyboardButtonLight(bModifier, UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() } @@ -144,43 +143,43 @@ private fun BaseKeyboard( deleteDigit: () -> Unit, allowVibration: Boolean ) { - Column { + ColumnWithConstraints { // Button modifier val bModifier = Modifier .fillMaxSize() .weight(1f) - .padding(4.dp) // Column modifier - val cModifier = Modifier.weight(1f) - - Row(cModifier) { + val cModifier = Modifier.weight(1f).padding(vertical = it.maxHeight * 0.01f) + val horizontalArrangement = Arrangement.spacedBy(it.maxWidth * 0.03f) + Row(cModifier, horizontalArrangement) { KeyboardButtonFilled(bModifier, UnittoIcons.KeyA, allowVibration) { addDigit(Token.baseA) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyB, allowVibration) { addDigit(Token.baseB) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyC, allowVibration) { addDigit(Token.baseC) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonFilled(bModifier, UnittoIcons.KeyD, allowVibration) { addDigit(Token.baseD) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyE, allowVibration) { addDigit(Token.baseE) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyF, allowVibration) { addDigit(Token.baseF) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) } KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) } KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) } KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) } KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) } KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) } KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) } - KeyboardButtonLight(Modifier.fillMaxSize().weight(2f).padding(4.dp), UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() } + KeyboardButtonLight( + Modifier.fillMaxSize().weight(2f).padding(it.maxWidth * 0.015f, it.maxHeight * 0.008f), UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() } } } } diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt index 675dbba3..94ca5b8d 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt @@ -48,11 +48,12 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge -import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium import com.sadellie.unitto.core.ui.R +import com.sadellie.unitto.core.ui.common.textfield.InputTextField +import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge /** * Component for input and output @@ -94,7 +95,8 @@ internal fun MyTextField( ) { LazyRow( modifier = modifier - .wrapContentHeight(), + .wrapContentHeight() + .weight(2f), reverseLayout = true, horizontalArrangement = Arrangement.End, contentPadding = PaddingValues(horizontal = 8.dp) @@ -110,19 +112,17 @@ internal fun MyTextField( .using(SizeTransform(clip = false)) } ) { - Text( + InputTextField( modifier = Modifier.fillMaxWidth(), - // Quick fix to prevent the UI from crashing - text = it.take(1000), - textAlign = TextAlign.End, - softWrap = false, - style = NumbersTextStyleDisplayLarge + value = TextFieldValue(it.take(1000)), + textStyle = NumbersTextStyleDisplayLarge.copy(textAlign = TextAlign.End) ) } } } AnimatedVisibility( + modifier = Modifier.weight(1f), visible = !secondaryText.isNullOrEmpty(), enter = expandVertically(), exit = shrinkVertically() @@ -145,14 +145,14 @@ internal fun MyTextField( .using(SizeTransform(clip = false)) } ) { - Text( - modifier = Modifier, - // Quick fix to prevent the UI from crashing - text = it?.take(1000) ?: "", - textAlign = TextAlign.End, - softWrap = false, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - style = NumbersTextStyleDisplayMedium + InputTextField( + modifier = Modifier.fillMaxWidth(), + value = TextFieldValue(it?.take(1000) ?: ""), + textStyle = NumbersTextStyleDisplayLarge.copy( + textAlign = TextAlign.End, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ), + minRatio = 0.7f ) } } @@ -162,7 +162,8 @@ internal fun MyTextField( AnimatedContent( modifier = Modifier .align(Alignment.End) - .padding(horizontal = 8.dp), + .padding(horizontal = 8.dp) + .weight(1f), targetState = helperText ) { Text( diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt index 26a5d7d9..95cbfcab 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt @@ -18,18 +18,29 @@ package com.sadellie.unitto.feature.converter.components +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.with +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.SwapHoriz import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +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 @@ -40,9 +51,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sadellie.unitto.core.ui.Formatter import com.sadellie.unitto.core.ui.R +import com.sadellie.unitto.core.ui.common.textfield.InputTextField import com.sadellie.unitto.data.model.AbstractUnit import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.feature.converter.ConverterMode @@ -95,22 +109,49 @@ internal fun TopScreenPart( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - MyTextField( - modifier = Modifier.fillMaxWidth(), - primaryText = { + InputTextField( + modifier = Modifier.weight(2f), + value = TextFieldValue( when (converterMode) { ConverterMode.BASE -> inputValue.uppercase() else -> Formatter.format(inputValue) } - }, - secondaryText = calculatedValue?.let { Formatter.format(it) }, - helperText = stringResource(unitFrom?.shortName ?: R.string.loading_label), - textToCopy = calculatedValue ?: inputValue, + ), + minRatio = 0.7f ) - MyTextField( + AnimatedVisibility( + visible = !calculatedValue.isNullOrEmpty(), + modifier = Modifier.weight(1f), + enter = expandVertically(clip = false), + exit = shrinkVertically(clip = false) + ) { + InputTextField( + value = TextFieldValue(calculatedValue?.let { Formatter.format(it) } ?: ""), + minRatio = 0.7f + ) + } + AnimatedContent( modifier = Modifier.fillMaxWidth(), - onClick = onOutputTextFieldClick, - primaryText = { + targetState = stringResource(unitFrom?.shortName ?: R.string.loading_label), + transitionSpec = { + // Enter animation + (expandHorizontally(clip = false, expandFrom = Alignment.Start) + fadeIn() + // Exit animation + with fadeOut()) + .using(SizeTransform(clip = false)) + } + ) { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End) + ) + } + + InputTextField( + modifier = Modifier + .weight(2f) + .clickable { onOutputTextFieldClick() }, + value = TextFieldValue( when { networkLoading -> stringResource(R.string.loading_label) networkError -> stringResource(R.string.error_label) @@ -124,16 +165,27 @@ internal fun TopScreenPart( } else -> Formatter.format(outputValue) } - }, - secondaryText = null, - helperText = stringResource(unitTo?.shortName ?: R.string.loading_label), - textToCopy = outputValue, + ), + minRatio = 0.7f ) - // Unit selection buttons - Row( - modifier = Modifier.padding(horizontal = 8.dp), - verticalAlignment = Alignment.Bottom, + AnimatedContent( + modifier = Modifier.fillMaxWidth(), + targetState = stringResource(unitTo?.shortName ?: R.string.loading_label), + transitionSpec = { + // Enter animation + (expandHorizontally(clip = false, expandFrom = Alignment.Start) + fadeIn() + // Exit animation + with fadeOut()) + .using(SizeTransform(clip = false)) + } ) { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End) + ) + } + + Row(verticalAlignment = Alignment.CenterVertically) { UnitSelectionButton( modifier = Modifier .fillMaxWidth()