Merge branch 'feature/responsive-ui'

This commit is contained in:
Sad Ellie 2023-04-02 19:46:49 +03:00
commit 3a620452c2
17 changed files with 664 additions and 373 deletions

View File

@ -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
@ -26,11 +27,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -38,8 +37,8 @@ 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
@Composable
fun BasicKeyboardButton(
@ -50,7 +49,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 +65,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 +97,7 @@ fun KeyboardButtonLight(
icon = icon,
iconColor = MaterialTheme.colorScheme.onSurfaceVariant,
allowVibration = allowVibration,
contentHeight = if (isPortrait()) 0.5f else 0.85f
)
}
@ -111,7 +116,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 +129,17 @@ fun KeyboardButtonAdditional(
onLongClick: (() -> Unit)? = null,
onClick: () -> Unit
) {
BasicKeyboardButton(
modifier = modifier
.minimumInteractiveComponentSize()
.heightIn(max = 48.dp),
modifier = modifier,
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.8f else 0.85f
)
}
@Composable
private fun isPortrait() = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT

View File

@ -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
@ -42,33 +41,20 @@ 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.38f).padding(horizontal = it.maxWidth * 0.03f))
content2(Modifier.fillMaxSize().padding(it.maxWidth * 0.03f, 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) {
val contentModifier = Modifier
.weight(1f)
.fillMaxSize()
.padding(
it.maxWidth * 0.015f, 0.dp,
it.maxHeight * 0.03f, it.maxHeight * 0.03f)
content1(contentModifier)
content2(contentModifier)
}
}
}

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/>.
*/
package com.sadellie.unitto.feature.calculator.components
package com.sadellie.unitto.core.ui.common.textfield
import android.view.ActionMode
import android.view.Menu

View File

@ -0,0 +1,267 @@
/*
* 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.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.ClipboardManager
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.TextRange
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.copyWithoutGrouping(value)
val textToolbar = UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyCallback,
pasteCallback = {
pasteCallback(
Formatter.toSeparator(
clipboardManager.getText()?.text ?: "", Separator.COMMA
)
)
},
cutCallback = {
copyCallback()
cutCallback()
onCursorChange(value.selection.end..value.selection.end)
}
)
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides textToolbar
) {
AutoSizableTextField(
modifier = modifier,
value = value,
textStyle = textStyle.copy(color = textColor),
minRatio = minRatio,
onValueChange = {
onCursorChange(it.selection.start..it.selection.end)
},
showToolbar = textToolbar::showMenu,
hideToolbar = textToolbar::hide
)
}
}
@Composable
fun InputTextField(
modifier: Modifier = Modifier,
value: String,
textStyle: TextStyle = NumbersTextStyleDisplayLarge,
minRatio: Float = 1f,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
var textFieldValue by remember(value) {
mutableStateOf(TextFieldValue(value, selection = TextRange(value.length)))
}
val clipboardManager = LocalClipboardManager.current
fun copyCallback() {
clipboardManager.copyWithoutGrouping(textFieldValue)
textFieldValue = textFieldValue.copy(selection = TextRange(textFieldValue.selection.end))
}
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyCallback,
)
) {
AutoSizableTextField(
modifier = modifier,
value = textFieldValue,
onValueChange = { textFieldValue = it },
textStyle = textStyle.copy(color = textColor),
minRatio = minRatio,
readOnly = true,
interactionSource = interactionSource
)
}
}
@Composable
private fun AutoSizableTextField(
modifier: Modifier = Modifier,
value: TextFieldValue,
textStyle: TextStyle = TextStyle(),
scaleFactor: Float = 0.95f,
minRatio: Float = 1f,
onValueChange: (TextFieldValue) -> Unit,
readOnly: Boolean = false,
showToolbar: (rect: Rect) -> Unit = {},
hideToolbar: () -> Unit = {},
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
val focusRequester = remember { FocusRequester() }
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,
fontSize = nFontSize
)
var offset = Offset.Zero
BasicTextField(
value = value,
onValueChange = {
showToolbar(Rect(offset, 0f))
hideToolbar()
onValueChange(it)
},
modifier = Modifier
.focusRequester(focusRequester)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
hideToolbar()
focusRequester.requestFocus()
onValueChange(value.copy(selection = TextRange.Zero))
showToolbar(Rect(offset, 0f))
}
)
.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()
)
}
}
.onGloballyPositioned { layoutCoords -> offset = layoutCoords.positionInWindow() },
textStyle = nTextStyle,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
singleLine = true,
readOnly = readOnly,
interactionSource = interactionSource
)
}
}
/**
* Copy value to clipboard without grouping symbols.
*
* Example:
* "123.456,789" will be copied as "123456,789"
*
* @param value Formatted value that has grouping symbols.
*/
fun ClipboardManager.copyWithoutGrouping(value: TextFieldValue) = this.setText(
AnnotatedString(
Formatter.removeGrouping(value.annotatedString.subSequence(value.selection).text)
)
)

View File

@ -16,7 +16,7 @@
* 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.Menu

View File

@ -16,7 +16,7 @@
* 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.Menu

View File

@ -16,7 +16,7 @@
* 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.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,
@ -45,10 +45,8 @@ internal class UnittoTextToolbar(
onSelectAllRequested: (() -> Unit)?
) {
textActionModeCallback.rect = rect
textActionModeCallback.onCopyRequested = { onCopyRequested?.invoke(); copyCallback.invoke() }
textActionModeCallback.onCutRequested = cutCallback?.let {
{ it.invoke(); onCutRequested?.invoke() }
}
textActionModeCallback.onCopyRequested = copyCallback
textActionModeCallback.onCutRequested = cutCallback
textActionModeCallback.onPasteRequested = pasteCallback
textActionModeCallback.onSelectAllRequested = onSelectAllRequested
if (actionMode == 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.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,
)

View File

@ -26,17 +26,15 @@ 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
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.MoreVert
@ -48,7 +46,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
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
@ -58,28 +55,22 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalTextToolbar
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.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.*
@ -92,39 +83,20 @@ internal fun CalculatorRoute(
navigateToSettings: () -> Unit,
viewModel: CalculatorViewModel = hiltViewModel()
) {
val clipboardManager = LocalClipboardManager.current
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
fun copyToClipboard() {
val clipboardText = clipboardManager.getText() ?: return
// This method is called immediately after copying formatted text, we replace it with the
// the unformatted version.
clipboardManager.setText(
AnnotatedString(
clipboardText.text.replace(Formatter.grouping, "")
)
)
}
CompositionLocalProvider(
LocalTextToolbar provides UnittoTextToolbar(
view = LocalView.current,
copyCallback = ::copyToClipboard
)
) {
CalculatorScreen(
uiState = uiState.value,
navigateToMenu = navigateToMenu,
navigateToSettings = navigateToSettings,
addSymbol = viewModel::addSymbol,
clearSymbols = viewModel::clearSymbols,
deleteSymbol = viewModel::deleteSymbol,
onCursorChange = viewModel::onCursorChange,
toggleAngleMode = viewModel::toggleCalculatorMode,
evaluate = viewModel::evaluate,
clearHistory = viewModel::clearHistory
)
}
CalculatorScreen(
uiState = uiState.value,
navigateToMenu = navigateToMenu,
navigateToSettings = navigateToSettings,
addSymbol = viewModel::addSymbol,
clearSymbols = viewModel::clearSymbols,
deleteSymbol = viewModel::deleteSymbol,
onCursorChange = viewModel::onCursorChange,
toggleAngleMode = viewModel::toggleCalculatorMode,
evaluate = viewModel::evaluate,
clearHistory = viewModel::clearHistory
)
}
@Composable
@ -188,18 +160,18 @@ private fun CalculatorScreen(
.fillMaxSize(),
historyItems = uiState.history,
historyItemHeightCallback = { historyItemHeight = it },
onTextClick = addSymbol
)
},
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,33 +198,32 @@ 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(
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,
)
}
InputTextField(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 8.dp),
value = Formatter.format(uiState.output),
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f)
)
}
// Handle
Box(
@ -268,7 +239,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),
radianMode = uiState.radianMode,
allowVibration = uiState.allowVibration,
addSymbol = addSymbol,
@ -314,7 +285,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 +308,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 = {},

View File

@ -25,34 +25,39 @@ 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.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.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
@ -149,22 +154,26 @@ 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.009f))
val additionalButtonModifier = Modifier
.minimumInteractiveComponentSize()
.weight(1f)
.heightIn(max = 48.dp)
.height(verticalFraction(0.09f))
Spacer(modifier = Modifier.height(verticalFraction(0.025f)))
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) {
@ -191,22 +200,28 @@ private fun PortraitKeyboard(
}
}
// Expand/Collapse
IconButton(
onClick = { showAdditional = !showAdditional },
colors = IconButtonDefaults.iconButtonColors(containerColor = MaterialTheme.colorScheme.inverseOnSurface)
Box(
modifier = Modifier.size(verticalFraction(0.09f)),
contentAlignment = Alignment.Center
) {
Icon(Icons.Default.ExpandLess, null, Modifier.rotate(expandRotation))
// 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.025f)))
Row(weightModifier) {
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) }
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.RightBracket, allowVibration) { addSymbol(Token.rightBracket) }
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Percent, allowVibration) { addSymbol(Token.percent) }
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Divide, allowVibration) { addSymbol(Token.divideDisplay) }
}
Row(weightModifier) {
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key7, allowVibration) { addSymbol(Token._7) }
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key8, allowVibration) { addSymbol(Token._8) }
@ -231,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)))
}
}
@ -245,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) }
@ -253,13 +270,13 @@ private fun AdditionalButtonsPortrait(
}
AnimatedVisibility(showAdditional) {
Column {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
Row {
KeyboardButtonAdditional(modifier, if (radianMode) UnittoIcons.Rad else UnittoIcons.Deg, 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) }
@ -281,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) }
@ -289,13 +306,13 @@ private fun AdditionalButtonsPortraitInverse(
}
AnimatedVisibility(showAdditional) {
Column {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
Row {
KeyboardButtonAdditional(modifier, if (radianMode) UnittoIcons.Rad else UnittoIcons.Deg, 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) }
@ -320,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 {

View File

@ -19,10 +19,8 @@
package com.sadellie.unitto.feature.calculator.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -30,24 +28,35 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf
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.layout.onPlaced
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.res.stringResource
import androidx.compose.ui.text.TextRange
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 com.sadellie.unitto.core.ui.Formatter
import com.sadellie.unitto.core.ui.common.textfield.UnittoTextToolbar
import com.sadellie.unitto.core.ui.common.textfield.copyWithoutGrouping
import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium
import com.sadellie.unitto.data.model.HistoryItem
import com.sadellie.unitto.feature.calculator.R
@ -59,7 +68,6 @@ internal fun HistoryList(
modifier: Modifier,
historyItems: List<HistoryItem>,
historyItemHeightCallback: (Int) -> Unit,
onTextClick: (String) -> Unit
) {
val verticalArrangement by remember(historyItems) {
derivedStateOf {
@ -95,15 +103,13 @@ internal fun HistoryList(
item {
HistoryListItem(
modifier = Modifier.onPlaced { historyItemHeightCallback(it.size.height) },
historyItem = historyItems.first(),
onTextClick = onTextClick
historyItem = historyItems.first()
)
}
items(historyItems.drop(1)) { historyItem ->
HistoryListItem(
modifier = Modifier,
historyItem = historyItem,
onTextClick = onTextClick
historyItem = historyItem
)
}
}
@ -114,40 +120,56 @@ internal fun HistoryList(
private fun HistoryListItem(
modifier: Modifier = Modifier,
historyItem: HistoryItem,
onTextClick: (String) -> Unit
) {
SelectionContainer {
Column(modifier = modifier) {
Box(
Modifier.clickable { onTextClick(historyItem.expression) }
) {
Text(
text = Formatter.format(historyItem.expression),
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.horizontalScroll(rememberScrollState(), reverseScrolling = true),
style = NumbersTextStyleDisplayMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.End
)
}
Box(
Modifier.clickable { onTextClick(historyItem.result) }
) {
Text(
text = Formatter.format(historyItem.result),
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.horizontalScroll(rememberScrollState(), reverseScrolling = true),
style = NumbersTextStyleDisplayMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
textAlign = TextAlign.End
)
}
val clipboardManager = LocalClipboardManager.current
val expr = Formatter.format(historyItem.expression)
var textFieldexpr by remember(expr) {
mutableStateOf(TextFieldValue(expr, selection = TextRange(expr.length)))
}
val res = Formatter.format(historyItem.expression)
var textFieldRes by remember(res) {
mutableStateOf(TextFieldValue(res, selection = TextRange(res.length)))
}
Column(modifier = modifier) {
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides UnittoTextToolbar(
view = LocalView.current,
copyCallback = { clipboardManager.copyWithoutGrouping(textFieldexpr) }
)
) {
BasicTextField(
value = textFieldexpr,
onValueChange = { textFieldexpr = it },
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.horizontalScroll(rememberScrollState(), reverseScrolling = true),
textStyle = NumbersTextStyleDisplayMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.End),
readOnly = true
)
}
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides UnittoTextToolbar(
view = LocalView.current,
copyCallback = { clipboardManager.copyWithoutGrouping(textFieldRes) }
)
) {
BasicTextField(
value = textFieldRes,
onValueChange = { textFieldRes = it },
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.horizontalScroll(rememberScrollState(), reverseScrolling = true),
textStyle = NumbersTextStyleDisplayMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), textAlign = TextAlign.End),
readOnly = true
)
}
}
}
@ -178,8 +200,6 @@ private fun PreviewHistoryList() {
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
.fillMaxSize(),
historyItems = historyItems,
historyItemHeightCallback = {},
onTextClick = {}
)
historyItems = historyItems
) {}
}

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
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<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
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 = {},

View File

@ -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() }
}
}
}

View File

@ -50,9 +50,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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 +94,8 @@ internal fun MyTextField(
) {
LazyRow(
modifier = modifier
.wrapContentHeight(),
.wrapContentHeight()
.weight(2f),
reverseLayout = true,
horizontalArrangement = Arrangement.End,
contentPadding = PaddingValues(horizontal = 8.dp)
@ -110,19 +111,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 = it.take(1000),
textStyle = NumbersTextStyleDisplayLarge.copy(textAlign = TextAlign.End)
)
}
}
}
AnimatedVisibility(
modifier = Modifier.weight(1f),
visible = !secondaryText.isNullOrEmpty(),
enter = expandVertically(),
exit = shrinkVertically()
@ -145,14 +144,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 = it?.take(1000) ?: "",
textStyle = NumbersTextStyleDisplayLarge.copy(
textAlign = TextAlign.End,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
),
minRatio = 0.7f
)
}
}
@ -162,7 +161,8 @@ internal fun MyTextField(
AnimatedContent(
modifier = Modifier
.align(Alignment.End)
.padding(horizontal = 8.dp),
.padding(horizontal = 8.dp)
.weight(1f),
targetState = helperText
) {
Text(

View File

@ -18,18 +18,28 @@
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.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
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 +50,11 @@ 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.unit.dp
import androidx.compose.ui.text.style.TextAlign
import com.sadellie.unitto.core.ui.Formatter
import com.sadellie.unitto.core.ui.R
import com.sadellie.unitto.core.ui.common.ColumnWithConstraints
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
@ -91,54 +103,89 @@ internal fun TopScreenPart(
)
val mContext = LocalContext.current
Column(
ColumnWithConstraints(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
MyTextField(
modifier = Modifier.fillMaxWidth(),
primaryText = {
when (converterMode) {
ConverterMode.BASE -> inputValue.uppercase()
else -> Formatter.format(inputValue)
}
InputTextField(
modifier = Modifier.weight(2f),
value = 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(
modifier = Modifier.fillMaxWidth(),
onClick = onOutputTextFieldClick,
primaryText = {
when {
networkLoading -> stringResource(R.string.loading_label)
networkError -> stringResource(R.string.error_label)
converterMode == ConverterMode.BASE -> outputValue.uppercase()
formatTime and (unitTo?.group == UnitGroup.TIME) -> {
Formatter.formatTime(
context = mContext,
input = calculatedValue ?: inputValue,
basicUnit = unitFrom?.basicUnit
)
}
else -> Formatter.format(outputValue)
}
},
secondaryText = null,
helperText = stringResource(unitTo?.shortName ?: R.string.loading_label),
textToCopy = outputValue,
)
// Unit selection buttons
Row(
modifier = Modifier.padding(horizontal = 8.dp),
verticalAlignment = Alignment.Bottom,
AnimatedVisibility(
visible = !calculatedValue.isNullOrEmpty(),
modifier = Modifier.weight(1f),
enter = expandVertically(clip = false),
exit = shrinkVertically(clip = false)
) {
InputTextField(
value = calculatedValue?.let { value -> Formatter.format(value) } ?: "",
minRatio = 0.7f,
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
}
AnimatedContent(
modifier = Modifier.fillMaxWidth(),
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))
}
) { value ->
Text(
text = value,
style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End)
)
}
InputTextField(
modifier = Modifier
.weight(2f),
value = when {
networkLoading -> stringResource(R.string.loading_label)
networkError -> stringResource(R.string.error_label)
converterMode == ConverterMode.BASE -> outputValue.uppercase()
formatTime and (unitTo?.group == UnitGroup.TIME) -> {
Formatter.formatTime(
context = mContext,
input = calculatedValue ?: inputValue,
basicUnit = unitFrom?.basicUnit
)
}
else -> Formatter.format(outputValue)
},
minRatio = 0.7f,
)
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))
}
) { value ->
Text(
text = value,
style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End)
)
}
Spacer(modifier = Modifier.height(it.maxHeight * 0.03f))
Row(verticalAlignment = Alignment.CenterVertically) {
UnitSelectionButton(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
onClick = { unitFrom?.let { navigateToLeftScreen(it.unitId) } },
onClick = { unitFrom?.let { unit -> navigateToLeftScreen(unit.unitId) } },
label = unitFrom?.displayName ?: R.string.loading_label,
)
IconButton(