The most responsive UI

- Auto-size text field (animated)
- Auto-size keyboard
- Auto-size buttons
- Fix cursor
This commit is contained in:
Sad Ellie 2023-03-03 23:17:28 +04:00 committed by Sad Ellie
parent bfafed4501
commit 07fdb03ab3
16 changed files with 500 additions and 251 deletions

View File

@ -18,6 +18,7 @@
package com.sadellie.unitto.core.ui.common package com.sadellie.unitto.core.ui.common
import android.content.res.Configuration
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.animateIntAsState
@ -38,6 +39,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -50,7 +52,7 @@ fun BasicKeyboardButton(
icon: ImageVector, icon: ImageVector,
iconColor: Color, iconColor: Color,
allowVibration: Boolean, allowVibration: Boolean,
contentPadding: PaddingValues = PaddingValues(24.dp, 8.dp) contentHeight: Float
) { ) {
val view = LocalView.current val view = LocalView.current
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
@ -66,10 +68,15 @@ fun BasicKeyboardButton(
onLongClick = onLongClick, onLongClick = onLongClick,
shape = RoundedCornerShape(cornerRadius), shape = RoundedCornerShape(cornerRadius),
containerColor = containerColor, containerColor = containerColor,
contentPadding = contentPadding, contentPadding = PaddingValues(),
interactionSource = interactionSource interactionSource = interactionSource
) { ) {
Icon(icon, null, modifier = Modifier.fillMaxHeight(), tint = iconColor) Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.fillMaxHeight(contentHeight),
tint = iconColor
)
} }
LaunchedEffect(key1 = isPressed) { LaunchedEffect(key1 = isPressed) {
@ -93,6 +100,7 @@ fun KeyboardButtonLight(
icon = icon, icon = icon,
iconColor = MaterialTheme.colorScheme.onSurfaceVariant, iconColor = MaterialTheme.colorScheme.onSurfaceVariant,
allowVibration = allowVibration, allowVibration = allowVibration,
contentHeight = if (isPortrait()) 0.5f else 0.85f
) )
} }
@ -111,7 +119,8 @@ fun KeyboardButtonFilled(
containerColor = MaterialTheme.colorScheme.primaryContainer, containerColor = MaterialTheme.colorScheme.primaryContainer,
icon = icon, icon = icon,
iconColor = MaterialTheme.colorScheme.onSecondaryContainer, 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, onLongClick: (() -> Unit)? = null,
onClick: () -> Unit onClick: () -> Unit
) { ) {
BasicKeyboardButton( BasicKeyboardButton(
modifier = modifier modifier = modifier
.minimumInteractiveComponentSize() .minimumInteractiveComponentSize()
.heightIn(max = 48.dp), .heightIn(max = 48.dp),
onClick = onClick, onClick = onClick,
onLongClick = onLongClick,
containerColor = Color.Transparent, containerColor = Color.Transparent,
icon = icon, icon = icon,
iconColor = MaterialTheme.colorScheme.onSurfaceVariant, iconColor = MaterialTheme.colorScheme.onSurfaceVariant,
allowVibration = allowVibration, allowVibration = allowVibration,
contentPadding = PaddingValues(12.dp, 2.dp), contentHeight = if (isPortrait()) 0.9f else 0.85f
onLongClick = onLongClick
) )
} }
@Composable
private fun isPortrait() = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT

View File

@ -19,7 +19,6 @@
package com.sadellie.unitto.core.ui.common package com.sadellie.unitto.core.ui.common
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@ -28,7 +27,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
/** /**
* When Portrait mode will place [content1] and [content2] in a [Column]. * When Portrait mode will place [content1] and [content2] in a [Column].
@ -42,33 +40,14 @@ fun PortraitLandscape(
content2: @Composable (Modifier) -> Unit, content2: @Composable (Modifier) -> Unit,
) { ) {
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
Column( ColumnWithConstraints(modifier) {
modifier = modifier.fillMaxSize(), content1(Modifier.fillMaxHeight(0.34f).padding(horizontal = it.maxWidth * 0.03f))
verticalArrangement = Arrangement.spacedBy(8.dp), content2(Modifier.fillMaxSize().padding(horizontal = it.maxWidth * 0.03f, vertical = it.maxHeight * 0.015f))
) {
content1(Modifier)
content2(
Modifier
.fillMaxSize()
.padding(horizontal = 8.dp)
)
} }
} else { } else {
Row( RowWithConstraints(modifier) {
modifier = modifier.fillMaxSize(), content1(Modifier.weight(1f).fillMaxHeight().padding(bottom = it.maxWidth * 0.015f, start = it.maxWidth * 0.015f, end = it.maxWidth * 0.015f))
horizontalArrangement = Arrangement.spacedBy(8.dp) content2(Modifier.weight(1f).fillMaxSize().padding(bottom = it.maxWidth * 0.015f, start = it.maxWidth * 0.015f, end = it.maxWidth * 0.015f))
) {
content1(
Modifier
.weight(1f)
.fillMaxHeight()
)
content2(
Modifier
.weight(1f)
.fillMaxSize()
.padding(horizontal = 8.dp)
)
} }
} }
} }

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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) }
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.sadellie.unitto.feature.calculator.components package com.sadellie.unitto.core.ui.common.textfield
import android.view.ActionMode import android.view.ActionMode
import android.view.Menu import android.view.Menu

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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
)
}
}

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.sadellie.unitto.feature.calculator.components package com.sadellie.unitto.core.ui.common.textfield
import android.view.ActionMode import android.view.ActionMode
import android.view.Menu import android.view.Menu

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.sadellie.unitto.feature.calculator.components package com.sadellie.unitto.core.ui.common.textfield
import android.view.ActionMode import android.view.ActionMode
import android.view.Menu import android.view.Menu

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.sadellie.unitto.feature.calculator.components package com.sadellie.unitto.core.ui.common.textfield
import android.os.Build import android.os.Build
import android.view.ActionMode 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.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus import androidx.compose.ui.platform.TextToolbarStatus
internal class UnittoTextToolbar( class UnittoTextToolbar(
private val view: View, private val view: View,
private val copyCallback: () -> Unit, private val copyCallback: () -> Unit,
private val pasteCallback: (() -> Unit)? = null, private val pasteCallback: (() -> Unit)? = null,

View File

@ -23,6 +23,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.sadellie.unitto.core.ui.R import com.sadellie.unitto.core.ui.R
@ -42,7 +43,7 @@ val NumbersTextStyleDisplayLarge = TextStyle(
fontFamily = Lato, fontFamily = Lato,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
fontSize = 57.sp, fontSize = 57.sp,
lineHeight = 64.sp, lineHeight = (1.4).em,
letterSpacing = (-0.25).sp, letterSpacing = (-0.25).sp,
) )

View File

@ -26,15 +26,14 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons 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.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.TextFieldValue 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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.base.Separator
import com.sadellie.unitto.core.ui.Formatter import com.sadellie.unitto.core.ui.Formatter
import com.sadellie.unitto.core.ui.common.MenuButton import com.sadellie.unitto.core.ui.common.MenuButton
import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar 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.data.model.HistoryItem
import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard
import com.sadellie.unitto.feature.calculator.components.DragDownView import com.sadellie.unitto.feature.calculator.components.DragDownView
import com.sadellie.unitto.feature.calculator.components.HistoryList 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 kotlinx.coroutines.launch
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -194,12 +192,13 @@ private fun CalculatorScreen(
textFields = { maxDragAmount -> textFields = { maxDragAmount ->
Column( Column(
Modifier Modifier
.fillMaxHeight(0.25f)
.onPlaced { textThingyHeight = it.size.height } .onPlaced { textThingyHeight = it.size.height }
.background( .background(
MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.surfaceVariant,
RoundedCornerShape( RoundedCornerShape(
topStart = 0.dp, topEnd = 0.dp, topStartPercent = 0, topEndPercent = 0,
bottomStart = 32.dp, bottomEnd = 32.dp bottomStartPercent = 20, bottomEndPercent = 20
) )
) )
.draggable( .draggable(
@ -226,31 +225,33 @@ private fun CalculatorScreen(
} }
} }
) )
.padding(top = 8.dp), .padding(top = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
InputTextField( InputTextField(
modifier = Modifier modifier = Modifier
.weight(2f)
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = uiState.input, value = uiState.input.copy(
onCursorChange = onCursorChange, Formatter.fromSeparator(uiState.input.text, Separator.COMMA)
),
minRatio = 0.5f,
cutCallback = deleteSymbol,
pasteCallback = addSymbol, pasteCallback = addSymbol,
cutCallback = deleteSymbol onCursorChange = onCursorChange
) )
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
SelectionContainer { SelectionContainer(Modifier.weight(1f)) {
Text( InputTextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp),
.horizontalScroll(rememberScrollState(), reverseScrolling = true), value = TextFieldValue(
text = Formatter.format(uiState.output), Formatter.fromSeparator(uiState.output, Separator.COMMA)
textAlign = TextAlign.End, ),
softWrap = false, textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f)
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
style = NumbersTextStyleDisplayMedium,
) )
} }
} }
@ -268,7 +269,7 @@ private fun CalculatorScreen(
}, },
numPad = { numPad = {
CalculatorKeyboard( CalculatorKeyboard(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 4.dp),
angleMode = uiState.angleMode, angleMode = uiState.angleMode,
allowVibration = uiState.allowVibration, allowVibration = uiState.allowVibration,
addSymbol = addSymbol, 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 @Composable
private fun PreviewCalculatorScreen() { private fun PreviewCalculatorScreen() {
val dtf = SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.getDefault()) val dtf = SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.getDefault())
@ -332,14 +338,14 @@ private fun PreviewCalculatorScreen() {
HistoryItem( HistoryItem(
date = dtf.parse(it)!!, date = dtf.parse(it)!!,
expression = "12345".repeat(10), expression = "12345".repeat(10),
result = "67890" result = "1234"
) )
} }
CalculatorScreen( CalculatorScreen(
uiState = CalculatorUIState( uiState = CalculatorUIState(
input = TextFieldValue("12345"), input = TextFieldValue("1.2345"),
output = "12345", output = "1234",
history = historyItems history = historyItems
), ),
navigateToMenu = {}, navigateToMenu = {},

View File

@ -25,11 +25,13 @@ import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess 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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview 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.base.Token
import com.sadellie.unitto.core.ui.Formatter 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.KeyboardButtonAdditional
import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled
import com.sadellie.unitto.core.ui.common.KeyboardButtonLight 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
import com.sadellie.unitto.core.ui.common.key.unittoicons.AcTan import com.sadellie.unitto.core.ui.common.key.unittoicons.AcTan
import com.sadellie.unitto.core.ui.common.key.unittoicons.ArCos import com.sadellie.unitto.core.ui.common.key.unittoicons.ArCos
@ -150,22 +155,27 @@ private fun PortraitKeyboard(
animationSpec = tween(easing = FastOutSlowInEasing) animationSpec = tween(easing = FastOutSlowInEasing)
) )
Column( ColumnWithConstraints(
modifier = modifier 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 weightModifier = Modifier.weight(1f)
val mainButtonModifier = Modifier val mainButtonModifier = Modifier
.fillMaxSize() .fillMaxSize()
.weight(1f) .weight(1f)
.padding(4.dp) .padding(horizontalFraction(0.015f), verticalFraction(0.008f))
val additionalButtonModifier = Modifier val additionalButtonModifier = Modifier
.minimumInteractiveComponentSize() .minimumInteractiveComponentSize()
.weight(1f) .weight(1f)
.heightIn(max = 48.dp) .height(verticalFraction(0.075f))
Spacer(modifier = Modifier.height(verticalFraction(0.015f)))
Row( Row(
modifier = Modifier.padding(vertical = 8.dp), modifier = Modifier,
horizontalArrangement = Arrangement.spacedBy(2.dp) horizontalArrangement = Arrangement.spacedBy(horizontalFraction(0.03f))
) { ) {
// Additional buttons // Additional buttons
Crossfade(invMode, weightModifier) { Crossfade(invMode, weightModifier) {
@ -192,6 +202,7 @@ private fun PortraitKeyboard(
} }
} }
Box(modifier = Modifier.height(verticalFraction(0.075f)), contentAlignment = Alignment.Center) {
// Expand/Collapse // Expand/Collapse
IconButton( IconButton(
onClick = { showAdditional = !showAdditional }, onClick = { showAdditional = !showAdditional },
@ -200,6 +211,9 @@ private fun PortraitKeyboard(
Icon(Icons.Default.ExpandLess, null, Modifier.rotate(expandRotation)) Icon(Icons.Default.ExpandLess, null, Modifier.rotate(expandRotation))
} }
} }
}
Spacer(modifier = Modifier.height(verticalFraction(0.015f)))
Row(weightModifier) { Row(weightModifier) {
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) }
@ -232,6 +246,8 @@ private fun PortraitKeyboard(
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Backspace, allowVibration, clearSymbols) { deleteSymbol() } KeyboardButtonLight(mainButtonModifier, UnittoIcons.Backspace, allowVibration, clearSymbols) { deleteSymbol() }
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Equal, allowVibration) { evaluate() } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Equal, allowVibration) { evaluate() }
} }
Spacer(modifier = Modifier.height(verticalFraction(0.015f)))
} }
} }
@ -246,7 +262,7 @@ private fun AdditionalButtonsPortrait(
toggleInvMode: () -> Unit toggleInvMode: () -> Unit
) { ) {
Column { Column {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Row {
KeyboardButtonAdditional(modifier, UnittoIcons.SquareRootWide, allowVibration) { addSymbol(Token.sqrt) } KeyboardButtonAdditional(modifier, UnittoIcons.SquareRootWide, allowVibration) { addSymbol(Token.sqrt) }
KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) } KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) }
KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) } KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) }
@ -254,13 +270,13 @@ private fun AdditionalButtonsPortrait(
} }
AnimatedVisibility(showAdditional) { AnimatedVisibility(showAdditional) {
Column { Column {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Row {
KeyboardButtonAdditional(modifier, if (angleMode == AngleMode.DEG) UnittoIcons.Deg else UnittoIcons.Rad, allowVibration) { toggleAngleMode() } KeyboardButtonAdditional(modifier, if (angleMode == AngleMode.DEG) UnittoIcons.Deg else UnittoIcons.Rad, allowVibration) { toggleAngleMode() }
KeyboardButtonAdditional(modifier, UnittoIcons.Sin, allowVibration) { addSymbol(Token.sin) } KeyboardButtonAdditional(modifier, UnittoIcons.Sin, allowVibration) { addSymbol(Token.sin) }
KeyboardButtonAdditional(modifier, UnittoIcons.Cos, allowVibration) { addSymbol(Token.cos) } KeyboardButtonAdditional(modifier, UnittoIcons.Cos, allowVibration) { addSymbol(Token.cos) }
KeyboardButtonAdditional(modifier, UnittoIcons.Tan, allowVibration) { addSymbol(Token.tan) } KeyboardButtonAdditional(modifier, UnittoIcons.Tan, allowVibration) { addSymbol(Token.tan) }
} }
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Row {
KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() } KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() }
KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) } KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) }
KeyboardButtonAdditional(modifier, UnittoIcons.Ln, allowVibration) { addSymbol(Token.ln) } KeyboardButtonAdditional(modifier, UnittoIcons.Ln, allowVibration) { addSymbol(Token.ln) }
@ -282,7 +298,7 @@ private fun AdditionalButtonsPortraitInverse(
toggleInvMode: () -> Unit toggleInvMode: () -> Unit
) { ) {
Column { Column {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Row {
KeyboardButtonAdditional(modifier, UnittoIcons.Modulo, allowVibration) { addSymbol(Token.modulo) } KeyboardButtonAdditional(modifier, UnittoIcons.Modulo, allowVibration) { addSymbol(Token.modulo) }
KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) } KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) }
KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) } KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) }
@ -290,13 +306,13 @@ private fun AdditionalButtonsPortraitInverse(
} }
AnimatedVisibility(showAdditional) { AnimatedVisibility(showAdditional) {
Column { Column {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Row {
KeyboardButtonAdditional(modifier, if (angleMode == AngleMode.DEG) UnittoIcons.Deg else UnittoIcons.Rad, allowVibration) { toggleAngleMode() } KeyboardButtonAdditional(modifier, if (angleMode == AngleMode.DEG) UnittoIcons.Deg else UnittoIcons.Rad, allowVibration) { toggleAngleMode() }
KeyboardButtonAdditional(modifier, UnittoIcons.ArSin, allowVibration) { addSymbol(Token.arSin) } KeyboardButtonAdditional(modifier, UnittoIcons.ArSin, allowVibration) { addSymbol(Token.arSin) }
KeyboardButtonAdditional(modifier, UnittoIcons.ArCos, allowVibration) { addSymbol(Token.arCos) } KeyboardButtonAdditional(modifier, UnittoIcons.ArCos, allowVibration) { addSymbol(Token.arCos) }
KeyboardButtonAdditional(modifier, UnittoIcons.AcTan, allowVibration) { addSymbol(Token.acTan) } KeyboardButtonAdditional(modifier, UnittoIcons.AcTan, allowVibration) { addSymbol(Token.acTan) }
} }
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Row {
KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() } KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() }
KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) } KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) }
KeyboardButtonAdditional(modifier, UnittoIcons.Exp, allowVibration) { addSymbol(Token.exp) } 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 } val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma }
var invMode: Boolean by remember { mutableStateOf(false) } var invMode: Boolean by remember { mutableStateOf(false) }
Row(modifier) { RowWithConstraints(modifier) { constraints ->
val buttonModifier = Modifier val buttonModifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
.padding(4.dp) .padding(constraints.maxWidth * 0.005f, constraints.maxHeight * 0.02f)
Crossfade(invMode, Modifier.weight(3f)) { Crossfade(invMode, Modifier.weight(3f)) {
Row { Row {

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
}
}

View File

@ -18,6 +18,7 @@
package com.sadellie.unitto.feature.converter package com.sadellie.unitto.feature.converter
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.MoreVert 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.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview 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.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.ui.R import com.sadellie.unitto.core.ui.R
@ -91,7 +94,7 @@ private fun ConverterScreen(
.centerAlignedTopAppBarColors(containerColor = Color.Transparent), .centerAlignedTopAppBarColors(containerColor = Color.Transparent),
content = { padding -> content = { padding ->
PortraitLandscape( PortraitLandscape(
modifier = Modifier.padding(padding), modifier = Modifier.padding(padding).fillMaxSize(),
content1 = { content1 = {
TopScreenPart( TopScreenPart(
modifier = it, modifier = it,
@ -125,11 +128,26 @@ private fun ConverterScreen(
) )
} }
@Preview class PreviewUIState: PreviewParameterProvider<ConverterUIState> {
override val values: Sequence<ConverterUIState>
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 @Composable
private fun PreviewConverterScreen() { private fun PreviewConverterScreen(
@PreviewParameter(PreviewUIState::class) uiState: ConverterUIState
) {
ConverterScreen( ConverterScreen(
uiState = ConverterUIState(), uiState = ConverterUIState(inputValue = "1234", calculatedValue = null, resultValue = "5678", showLoading = false),
navigateToLeftScreen = {}, navigateToLeftScreen = {},
navigateToRightScreen = {_, _, _ -> }, navigateToRightScreen = {_, _, _ -> },
navigateToSettings = {}, navigateToSettings = {},

View File

@ -19,16 +19,16 @@
package com.sadellie.unitto.feature.converter.components package com.sadellie.unitto.feature.converter.components
import androidx.compose.animation.Crossfade 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.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.base.Token import com.sadellie.unitto.core.base.Token
import com.sadellie.unitto.core.ui.Formatter 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.KeyboardButtonFilled
import com.sadellie.unitto.core.ui.common.KeyboardButtonLight import com.sadellie.unitto.core.ui.common.KeyboardButtonLight
import com.sadellie.unitto.core.ui.common.key.UnittoIcons import com.sadellie.unitto.core.ui.common.key.UnittoIcons
@ -85,7 +85,6 @@ internal fun Keyboard(
ConverterMode.BASE -> BaseKeyboard(addDigit, clearInput, deleteDigit, allowVibration) ConverterMode.BASE -> BaseKeyboard(addDigit, clearInput, deleteDigit, allowVibration)
} }
} }
} }
@Composable @Composable
@ -96,39 +95,39 @@ private fun DefaultKeyboard(
allowVibration: Boolean allowVibration: Boolean
) { ) {
val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma } val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma }
Column { ColumnWithConstraints {
// Button modifier // Button modifier
val bModifier = Modifier val bModifier = Modifier
.fillMaxSize() .fillMaxSize()
.weight(1f) .weight(1f)
.padding(4.dp)
// Column modifier // Column modifier
val cModifier = Modifier.weight(1f) val cModifier = Modifier.weight(1f).padding(vertical = it.maxHeight * 0.01f)
Row(cModifier) { val horizontalArrangement = Arrangement.spacedBy(it.maxWidth * 0.03f)
Row(cModifier, horizontalArrangement) {
KeyboardButtonFilled(bModifier, UnittoIcons.LeftBracket, allowVibration) { addDigit(Token.leftBracket) } KeyboardButtonFilled(bModifier, UnittoIcons.LeftBracket, allowVibration) { addDigit(Token.leftBracket) }
KeyboardButtonFilled(bModifier, UnittoIcons.RightBracket, allowVibration) { addDigit(Token.rightBracket) } KeyboardButtonFilled(bModifier, UnittoIcons.RightBracket, allowVibration) { addDigit(Token.rightBracket) }
KeyboardButtonFilled(bModifier, UnittoIcons.Exponent, allowVibration) { addDigit(Token.exponent) } KeyboardButtonFilled(bModifier, UnittoIcons.Exponent, allowVibration) { addDigit(Token.exponent) }
KeyboardButtonFilled(bModifier, UnittoIcons.SquareRoot, allowVibration) { addDigit(Token.sqrt) } KeyboardButtonFilled(bModifier, UnittoIcons.SquareRoot, allowVibration) { addDigit(Token.sqrt) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) } KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) }
KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) } KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) }
KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) } KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) }
KeyboardButtonFilled(bModifier, UnittoIcons.Divide, allowVibration) { addDigit(Token.divide) } KeyboardButtonFilled(bModifier, UnittoIcons.Divide, allowVibration) { addDigit(Token.divide) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) } KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) }
KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) } KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) }
KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) } KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) }
KeyboardButtonFilled(bModifier, UnittoIcons.Multiply, allowVibration) { addDigit(Token.multiply) } KeyboardButtonFilled(bModifier, UnittoIcons.Multiply, allowVibration) { addDigit(Token.multiply) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) } KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) }
KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) } KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) }
KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) } KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) }
KeyboardButtonFilled(bModifier, UnittoIcons.Minus, allowVibration) { addDigit(Token.minus) } KeyboardButtonFilled(bModifier, UnittoIcons.Minus, allowVibration) { addDigit(Token.minus) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) } KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) }
KeyboardButtonLight(bModifier, fractionalIcon, allowVibration) { addDigit(Token.dot) } KeyboardButtonLight(bModifier, fractionalIcon, allowVibration) { addDigit(Token.dot) }
KeyboardButtonLight(bModifier, UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() } KeyboardButtonLight(bModifier, UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() }
@ -144,43 +143,43 @@ private fun BaseKeyboard(
deleteDigit: () -> Unit, deleteDigit: () -> Unit,
allowVibration: Boolean allowVibration: Boolean
) { ) {
Column { ColumnWithConstraints {
// Button modifier // Button modifier
val bModifier = Modifier val bModifier = Modifier
.fillMaxSize() .fillMaxSize()
.weight(1f) .weight(1f)
.padding(4.dp)
// Column modifier // Column modifier
val cModifier = Modifier.weight(1f) val cModifier = Modifier.weight(1f).padding(vertical = it.maxHeight * 0.01f)
val horizontalArrangement = Arrangement.spacedBy(it.maxWidth * 0.03f)
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonFilled(bModifier, UnittoIcons.KeyA, allowVibration) { addDigit(Token.baseA) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyA, allowVibration) { addDigit(Token.baseA) }
KeyboardButtonFilled(bModifier, UnittoIcons.KeyB, allowVibration) { addDigit(Token.baseB) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyB, allowVibration) { addDigit(Token.baseB) }
KeyboardButtonFilled(bModifier, UnittoIcons.KeyC, allowVibration) { addDigit(Token.baseC) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyC, allowVibration) { addDigit(Token.baseC) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonFilled(bModifier, UnittoIcons.KeyD, allowVibration) { addDigit(Token.baseD) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyD, allowVibration) { addDigit(Token.baseD) }
KeyboardButtonFilled(bModifier, UnittoIcons.KeyE, allowVibration) { addDigit(Token.baseE) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyE, allowVibration) { addDigit(Token.baseE) }
KeyboardButtonFilled(bModifier, UnittoIcons.KeyF, allowVibration) { addDigit(Token.baseF) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyF, allowVibration) { addDigit(Token.baseF) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) } KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) }
KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) } KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) }
KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) } KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) } KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) }
KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) } KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) }
KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) } KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) } KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) }
KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) } KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) }
KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) } KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) }
} }
Row(cModifier) { Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) } 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() }
} }
} }
} }

View File

@ -48,11 +48,12 @@ import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign 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.NumbersTextStyleDisplayLarge
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
import com.sadellie.unitto.core.ui.R 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 * Component for input and output
@ -94,7 +95,8 @@ internal fun MyTextField(
) { ) {
LazyRow( LazyRow(
modifier = modifier modifier = modifier
.wrapContentHeight(), .wrapContentHeight()
.weight(2f),
reverseLayout = true, reverseLayout = true,
horizontalArrangement = Arrangement.End, horizontalArrangement = Arrangement.End,
contentPadding = PaddingValues(horizontal = 8.dp) contentPadding = PaddingValues(horizontal = 8.dp)
@ -110,19 +112,17 @@ internal fun MyTextField(
.using(SizeTransform(clip = false)) .using(SizeTransform(clip = false))
} }
) { ) {
Text( InputTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
// Quick fix to prevent the UI from crashing value = TextFieldValue(it.take(1000)),
text = it.take(1000), textStyle = NumbersTextStyleDisplayLarge.copy(textAlign = TextAlign.End)
textAlign = TextAlign.End,
softWrap = false,
style = NumbersTextStyleDisplayLarge
) )
} }
} }
} }
AnimatedVisibility( AnimatedVisibility(
modifier = Modifier.weight(1f),
visible = !secondaryText.isNullOrEmpty(), visible = !secondaryText.isNullOrEmpty(),
enter = expandVertically(), enter = expandVertically(),
exit = shrinkVertically() exit = shrinkVertically()
@ -145,14 +145,14 @@ internal fun MyTextField(
.using(SizeTransform(clip = false)) .using(SizeTransform(clip = false))
} }
) { ) {
Text( InputTextField(
modifier = Modifier, modifier = Modifier.fillMaxWidth(),
// Quick fix to prevent the UI from crashing value = TextFieldValue(it?.take(1000) ?: ""),
text = it?.take(1000) ?: "", textStyle = NumbersTextStyleDisplayLarge.copy(
textAlign = TextAlign.End, textAlign = TextAlign.End,
softWrap = false, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), ),
style = NumbersTextStyleDisplayMedium minRatio = 0.7f
) )
} }
} }
@ -162,7 +162,8 @@ internal fun MyTextField(
AnimatedContent( AnimatedContent(
modifier = Modifier modifier = Modifier
.align(Alignment.End) .align(Alignment.End)
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp)
.weight(1f),
targetState = helperText targetState = helperText
) { ) {
Text( Text(

View File

@ -18,18 +18,29 @@
package com.sadellie.unitto.feature.converter.components 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.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SwapHoriz import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -40,9 +51,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.Formatter import com.sadellie.unitto.core.ui.Formatter
import com.sadellie.unitto.core.ui.R 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.AbstractUnit
import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.data.model.UnitGroup
import com.sadellie.unitto.feature.converter.ConverterMode import com.sadellie.unitto.feature.converter.ConverterMode
@ -95,22 +109,49 @@ internal fun TopScreenPart(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
MyTextField( InputTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.weight(2f),
primaryText = { value = TextFieldValue(
when (converterMode) { when (converterMode) {
ConverterMode.BASE -> inputValue.uppercase() ConverterMode.BASE -> inputValue.uppercase()
else -> Formatter.format(inputValue) else -> Formatter.format(inputValue)
} }
}, ),
secondaryText = calculatedValue?.let { Formatter.format(it) }, minRatio = 0.7f
helperText = stringResource(unitFrom?.shortName ?: R.string.loading_label),
textToCopy = calculatedValue ?: inputValue,
) )
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(), modifier = Modifier.fillMaxWidth(),
onClick = onOutputTextFieldClick, targetState = stringResource(unitFrom?.shortName ?: R.string.loading_label),
primaryText = { 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 { when {
networkLoading -> stringResource(R.string.loading_label) networkLoading -> stringResource(R.string.loading_label)
networkError -> stringResource(R.string.error_label) networkError -> stringResource(R.string.error_label)
@ -124,16 +165,27 @@ internal fun TopScreenPart(
} }
else -> Formatter.format(outputValue) else -> Formatter.format(outputValue)
} }
}, ),
secondaryText = null, minRatio = 0.7f
helperText = stringResource(unitTo?.shortName ?: R.string.loading_label),
textToCopy = outputValue,
) )
// Unit selection buttons AnimatedContent(
Row( modifier = Modifier.fillMaxWidth(),
modifier = Modifier.padding(horizontal = 8.dp), targetState = stringResource(unitTo?.shortName ?: R.string.loading_label),
verticalAlignment = Alignment.Bottom, 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( UnitSelectionButton(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()