Fixes for responsive ui

squashed commit

closes: #24 and #34
This commit is contained in:
Sad Ellie 2023-03-30 12:17:33 +03:00
parent 6cf49812b7
commit 4c20c6c0b8
9 changed files with 243 additions and 201 deletions

View File

@ -27,11 +27,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -41,7 +39,6 @@ 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.LocalConfiguration
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
@Composable @Composable
fun BasicKeyboardButton( fun BasicKeyboardButton(
@ -133,16 +130,14 @@ fun KeyboardButtonAdditional(
onClick: () -> Unit onClick: () -> Unit
) { ) {
BasicKeyboardButton( BasicKeyboardButton(
modifier = modifier modifier = modifier,
.minimumInteractiveComponentSize()
.heightIn(max = 48.dp),
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
containerColor = Color.Transparent, containerColor = Color.Transparent,
icon = icon, icon = icon,
iconColor = MaterialTheme.colorScheme.onSurfaceVariant, iconColor = MaterialTheme.colorScheme.onSurfaceVariant,
allowVibration = allowVibration, allowVibration = allowVibration,
contentHeight = if (isPortrait()) 0.9f else 0.85f contentHeight = if (isPortrait()) 0.8f else 0.85f
) )
} }

View File

@ -27,6 +27,7 @@ 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].
@ -41,13 +42,19 @@ fun PortraitLandscape(
) { ) {
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
ColumnWithConstraints(modifier) { ColumnWithConstraints(modifier) {
content1(Modifier.fillMaxHeight(0.34f).padding(horizontal = it.maxWidth * 0.03f)) content1(Modifier.fillMaxHeight(0.38f).padding(horizontal = it.maxWidth * 0.03f))
content2(Modifier.fillMaxSize().padding(horizontal = it.maxWidth * 0.03f, vertical = it.maxHeight * 0.015f)) content2(Modifier.fillMaxSize().padding(it.maxWidth * 0.03f, it.maxHeight * 0.015f))
} }
} else { } else {
RowWithConstraints(modifier) { RowWithConstraints(modifier) {
content1(Modifier.weight(1f).fillMaxHeight().padding(bottom = it.maxWidth * 0.015f, start = it.maxWidth * 0.015f, end = it.maxWidth * 0.015f)) val contentModifier = Modifier
content2(Modifier.weight(1f).fillMaxSize().padding(bottom = it.maxWidth * 0.015f, start = it.maxWidth * 0.015f, end = it.maxWidth * 0.015f)) .weight(1f)
.fillMaxSize()
.padding(
it.maxWidth * 0.015f, 0.dp,
it.maxHeight * 0.03f, it.maxHeight * 0.03f)
content1(contentModifier)
content2(contentModifier)
} }
} }
} }

View File

@ -18,6 +18,8 @@
package com.sadellie.unitto.core.ui.common.textfield 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.BoxWithConstraints
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
@ -30,10 +32,16 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush 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.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.layout 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.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@ -42,6 +50,7 @@ import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Paragraph import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
@ -66,19 +75,9 @@ fun InputTextField(
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
) { ) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
fun copyCallback() { fun copyCallback() = clipboardManager.copyWithoutGrouping(value)
clipboardManager.setText(
AnnotatedString(
Formatter.removeGrouping(
value.annotatedString.subSequence(value.selection).text
)
)
)
}
CompositionLocalProvider( val textToolbar = UnittoTextToolbar(
LocalTextInputService provides null,
LocalTextToolbar provides UnittoTextToolbar(
view = LocalView.current, view = LocalView.current,
copyCallback = ::copyCallback, copyCallback = ::copyCallback,
pasteCallback = { pasteCallback = {
@ -88,18 +87,27 @@ fun InputTextField(
) )
) )
}, },
cutCallback = { copyCallback(); cutCallback() } cutCallback = {
copyCallback()
cutCallback()
onCursorChange(value.selection.end..value.selection.end)
}
) )
CompositionLocalProvider(
LocalTextInputService provides null,
LocalTextToolbar provides textToolbar
) { ) {
AutoSizableTextField( AutoSizableTextField(
modifier = modifier, modifier = modifier,
value = value, value = value,
textStyle = textStyle.copy(color = textColor),
minRatio = minRatio,
onValueChange = { onValueChange = {
onCursorChange(it.selection.start..it.selection.end) onCursorChange(it.selection.start..it.selection.end)
}, },
textStyle = textStyle, showToolbar = textToolbar::showMenu,
minRatio = minRatio, hideToolbar = textToolbar::hide
readOnly = false
) )
} }
} }
@ -107,34 +115,54 @@ fun InputTextField(
@Composable @Composable
fun InputTextField( fun InputTextField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
value: TextFieldValue, value: String,
textStyle: TextStyle = NumbersTextStyleDisplayLarge, textStyle: TextStyle = NumbersTextStyleDisplayLarge,
minRatio: Float = 1f, minRatio: Float = 1f,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, 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( AutoSizableTextField(
modifier = modifier, modifier = modifier,
value = value, value = textFieldValue,
textStyle = textStyle, onValueChange = { textFieldValue = it },
textStyle = textStyle.copy(color = textColor),
minRatio = minRatio, minRatio = minRatio,
readOnly = true readOnly = true,
interactionSource = interactionSource
) )
} }
}
@Composable @Composable
private fun AutoSizableTextField( private fun AutoSizableTextField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
value: TextFieldValue, value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit = {},
textStyle: TextStyle = TextStyle(), textStyle: TextStyle = TextStyle(),
scaleFactor: Float = 0.95f, scaleFactor: Float = 0.95f,
minRatio: Float = 1f, minRatio: Float = 1f,
onValueChange: (TextFieldValue) -> Unit,
readOnly: Boolean = false, readOnly: Boolean = false,
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, showToolbar: (rect: Rect) -> Unit = {},
cursorBrush: Brush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) hideToolbar: () -> Unit = {},
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) { ) {
// FIXME Acts strange when minRatio is set to 0 (still scales down). val focusRequester = remember { FocusRequester() }
val density = LocalDensity.current val density = LocalDensity.current
var nFontSize: TextUnit by remember { mutableStateOf(0.sp) } var nFontSize: TextUnit by remember { mutableStateOf(0.sp) }
@ -178,18 +206,30 @@ private fun AutoSizableTextField(
val nTextStyle = textStyle.copy( val nTextStyle = textStyle.copy(
// https://issuetracker.google.com/issues/266470454 // https://issuetracker.google.com/issues/266470454
// textAlign = TextAlign.End, // textAlign = TextAlign.End,
color = textColor,
fontSize = nFontSize fontSize = nFontSize
) )
var offset = Offset.Zero
BasicTextField( BasicTextField(
value = value, value = value,
singleLine = true, onValueChange = {
onValueChange = onValueChange, showToolbar(Rect(offset, 0f))
hideToolbar()
onValueChange(it)
},
modifier = Modifier modifier = Modifier
.widthIn( .focusRequester(focusRequester)
max = with(density) { intrinsics.width.toDp() } .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 -> .layout { measurable, constraints ->
val placeable = measurable.measure(constraints) val placeable = measurable.measure(constraints)
// TextField size is changed with a delay (text jumps). Here we correct it. // TextField size is changed with a delay (text jumps). Here we correct it.
@ -201,10 +241,27 @@ private fun AutoSizableTextField(
y = (placeable.height - intrinsics.height).roundToInt() y = (placeable.height - intrinsics.height).roundToInt()
) )
} }
}, }
.onGloballyPositioned { layoutCoords -> offset = layoutCoords.positionInWindow() },
textStyle = nTextStyle, textStyle = nTextStyle,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
singleLine = true,
readOnly = readOnly, readOnly = readOnly,
cursorBrush = cursorBrush 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

@ -45,10 +45,8 @@ class UnittoTextToolbar(
onSelectAllRequested: (() -> Unit)? onSelectAllRequested: (() -> Unit)?
) { ) {
textActionModeCallback.rect = rect textActionModeCallback.rect = rect
textActionModeCallback.onCopyRequested = { onCopyRequested?.invoke(); copyCallback.invoke() } textActionModeCallback.onCopyRequested = copyCallback
textActionModeCallback.onCutRequested = cutCallback?.let { textActionModeCallback.onCutRequested = cutCallback
{ it.invoke(); onCutRequested?.invoke() }
}
textActionModeCallback.onPasteRequested = pasteCallback textActionModeCallback.onPasteRequested = pasteCallback
textActionModeCallback.onSelectAllRequested = onSelectAllRequested textActionModeCallback.onSelectAllRequested = onSelectAllRequested
if (actionMode == null) { if (actionMode == null) {

View File

@ -35,7 +35,6 @@ 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.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.MoreVert
@ -47,7 +46,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue 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
@ -57,12 +55,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalConfiguration 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.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
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
@ -73,7 +67,6 @@ 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.common.textfield.InputTextField 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
@ -90,26 +83,8 @@ internal fun CalculatorRoute(
navigateToSettings: () -> Unit, navigateToSettings: () -> Unit,
viewModel: CalculatorViewModel = hiltViewModel() viewModel: CalculatorViewModel = hiltViewModel()
) { ) {
val clipboardManager = LocalClipboardManager.current
val uiState = viewModel.uiState.collectAsStateWithLifecycle() 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( CalculatorScreen(
uiState = uiState.value, uiState = uiState.value,
navigateToMenu = navigateToMenu, navigateToMenu = navigateToMenu,
@ -123,7 +98,6 @@ internal fun CalculatorRoute(
clearHistory = viewModel::clearHistory clearHistory = viewModel::clearHistory
) )
} }
}
@Composable @Composable
private fun CalculatorScreen( private fun CalculatorScreen(
@ -186,7 +160,6 @@ private fun CalculatorScreen(
.fillMaxSize(), .fillMaxSize(),
historyItems = uiState.history, historyItems = uiState.history,
historyItemHeightCallback = { historyItemHeight = it }, historyItemHeightCallback = { historyItemHeight = it },
onTextClick = addSymbol
) )
}, },
textFields = { maxDragAmount -> textFields = { maxDragAmount ->
@ -243,18 +216,15 @@ private fun CalculatorScreen(
onCursorChange = onCursorChange onCursorChange = onCursorChange
) )
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
SelectionContainer(Modifier.weight(1f)) {
InputTextField( InputTextField(
modifier = Modifier modifier = Modifier
.weight(1f)
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
value = TextFieldValue( value = Formatter.format(uiState.output),
Formatter.fromSeparator(uiState.output, Separator.COMMA)
),
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f) textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f)
) )
} }
}
// Handle // Handle
Box( Box(
Modifier Modifier

View File

@ -33,13 +33,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.minimumInteractiveComponentSize
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
@ -164,13 +164,12 @@ private fun PortraitKeyboard(
val mainButtonModifier = Modifier val mainButtonModifier = Modifier
.fillMaxSize() .fillMaxSize()
.weight(1f) .weight(1f)
.padding(horizontalFraction(0.015f), verticalFraction(0.008f)) .padding(horizontalFraction(0.015f), verticalFraction(0.009f))
val additionalButtonModifier = Modifier val additionalButtonModifier = Modifier
.minimumInteractiveComponentSize()
.weight(1f) .weight(1f)
.height(verticalFraction(0.075f)) .height(verticalFraction(0.09f))
Spacer(modifier = Modifier.height(verticalFraction(0.015f))) Spacer(modifier = Modifier.height(verticalFraction(0.025f)))
Row( Row(
modifier = Modifier, modifier = Modifier,
@ -201,7 +200,10 @@ private fun PortraitKeyboard(
} }
} }
Box(modifier = Modifier.height(verticalFraction(0.075f)), contentAlignment = Alignment.Center) { Box(
modifier = Modifier.size(verticalFraction(0.09f)),
contentAlignment = Alignment.Center
) {
// Expand/Collapse // Expand/Collapse
IconButton( IconButton(
onClick = { showAdditional = !showAdditional }, onClick = { showAdditional = !showAdditional },
@ -212,7 +214,7 @@ private fun PortraitKeyboard(
} }
} }
Spacer(modifier = Modifier.height(verticalFraction(0.015f))) Spacer(modifier = Modifier.height(verticalFraction(0.025f)))
Row(weightModifier) { Row(weightModifier) {
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) }
@ -220,7 +222,6 @@ private fun PortraitKeyboard(
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Percent, allowVibration) { addSymbol(Token.percent) } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Percent, allowVibration) { addSymbol(Token.percent) }
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Divide, allowVibration) { addSymbol(Token.divideDisplay) } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Divide, allowVibration) { addSymbol(Token.divideDisplay) }
} }
Row(weightModifier) { Row(weightModifier) {
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key7, allowVibration) { addSymbol(Token._7) } KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key7, allowVibration) { addSymbol(Token._7) }
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key8, allowVibration) { addSymbol(Token._8) } KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key8, allowVibration) { addSymbol(Token._8) }

View File

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

View File

@ -48,7 +48,6 @@ 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.R import com.sadellie.unitto.core.ui.R
@ -114,7 +113,7 @@ internal fun MyTextField(
) { ) {
InputTextField( InputTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = TextFieldValue(it.take(1000)), value = it.take(1000),
textStyle = NumbersTextStyleDisplayLarge.copy(textAlign = TextAlign.End) textStyle = NumbersTextStyleDisplayLarge.copy(textAlign = TextAlign.End)
) )
} }
@ -147,7 +146,7 @@ internal fun MyTextField(
) { ) {
InputTextField( InputTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = TextFieldValue(it?.take(1000) ?: ""), value = it?.take(1000) ?: "",
textStyle = NumbersTextStyleDisplayLarge.copy( textStyle = NumbersTextStyleDisplayLarge.copy(
textAlign = TextAlign.End, textAlign = TextAlign.End,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)

View File

@ -30,11 +30,10 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.with import androidx.compose.animation.with
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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
@ -51,11 +50,10 @@ 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.text.style.TextAlign
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.ColumnWithConstraints
import com.sadellie.unitto.core.ui.common.textfield.InputTextField 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
@ -105,18 +103,15 @@ internal fun TopScreenPart(
) )
val mContext = LocalContext.current val mContext = LocalContext.current
Column( ColumnWithConstraints(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
InputTextField( InputTextField(
modifier = Modifier.weight(2f), modifier = Modifier.weight(2f),
value = TextFieldValue( value = when (converterMode) {
when (converterMode) {
ConverterMode.BASE -> inputValue.uppercase() ConverterMode.BASE -> inputValue.uppercase()
else -> Formatter.format(inputValue) else -> Formatter.format(inputValue)
} },
),
minRatio = 0.7f minRatio = 0.7f
) )
AnimatedVisibility( AnimatedVisibility(
@ -126,8 +121,9 @@ internal fun TopScreenPart(
exit = shrinkVertically(clip = false) exit = shrinkVertically(clip = false)
) { ) {
InputTextField( InputTextField(
value = TextFieldValue(calculatedValue?.let { Formatter.format(it) } ?: ""), value = calculatedValue?.let { value -> Formatter.format(value) } ?: "",
minRatio = 0.7f minRatio = 0.7f,
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
) )
} }
AnimatedContent( AnimatedContent(
@ -140,19 +136,17 @@ internal fun TopScreenPart(
with fadeOut()) with fadeOut())
.using(SizeTransform(clip = false)) .using(SizeTransform(clip = false))
} }
) { ) { value ->
Text( Text(
text = it, text = value,
style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End) style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End)
) )
} }
InputTextField( InputTextField(
modifier = Modifier modifier = Modifier
.weight(2f) .weight(2f),
.clickable { onOutputTextFieldClick() }, value = when {
value = TextFieldValue(
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)
converterMode == ConverterMode.BASE -> outputValue.uppercase() converterMode == ConverterMode.BASE -> outputValue.uppercase()
@ -164,9 +158,8 @@ internal fun TopScreenPart(
) )
} }
else -> Formatter.format(outputValue) else -> Formatter.format(outputValue)
} },
), minRatio = 0.7f,
minRatio = 0.7f
) )
AnimatedContent( AnimatedContent(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -178,19 +171,21 @@ internal fun TopScreenPart(
with fadeOut()) with fadeOut())
.using(SizeTransform(clip = false)) .using(SizeTransform(clip = false))
} }
) { ) { value ->
Text( Text(
text = it, text = value,
style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End) style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End)
) )
} }
Spacer(modifier = Modifier.height(it.maxHeight * 0.03f))
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
UnitSelectionButton( UnitSelectionButton(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f), .weight(1f),
onClick = { unitFrom?.let { navigateToLeftScreen(it.unitId) } }, onClick = { unitFrom?.let { unit -> navigateToLeftScreen(unit.unitId) } },
label = unitFrom?.displayName ?: R.string.loading_label, label = unitFrom?.displayName ?: R.string.loading_label,
) )
IconButton( IconButton(