From c8b11a4b02059f8665012725f2d3ed8ae1513e47 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Wed, 27 Sep 2023 14:27:56 +0300 Subject: [PATCH] Refactor CalculatorScreen --- .../unitto/core/ui/common/KeyboardButton.kt | 24 ++- .../feature/calculator/CalculatorScreen.kt | 178 ++++++------------ .../unitto/feature/calculator/DragState.kt | 41 ---- .../calculator/components/HistoryList.kt | 43 ++--- .../feature/calculator/components/TextBox.kt | 136 +++++++++++++ 5 files changed, 225 insertions(+), 197 deletions(-) create mode 100644 feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt index 55eb2dfc..42997223 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt @@ -19,12 +19,16 @@ package com.sadellie.unitto.core.ui.common import android.view.HapticFeedbackConstants -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector @@ -52,12 +56,18 @@ fun BasicKeyboardButton( } } } - UnittoButton( - modifier = modifier, - onClick = { onClick(); vibrate() }, - onLongClick = if (onLongClick != null) { { onLongClick(); vibrate() } } else null, - containerColor = containerColor, - contentPadding = PaddingValues() + + Box( + modifier = modifier + .squashable( + onClick = { onClick(); vibrate() }, + onLongClick = if (onLongClick != null) { { onLongClick(); vibrate() } } else null, + interactionSource = remember { MutableInteractionSource() }, + cornerRadiusRange = 30..50, + ) + .background(containerColor) + , + contentAlignment = Alignment.Center ) { Icon( imageVector = icon, diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt index d50325d7..cd877b25 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt @@ -20,10 +20,12 @@ package com.sadellie.unitto.feature.calculator import android.content.res.Configuration import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.anchoredDraggable -import androidx.compose.foundation.gestures.snapTo import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -49,7 +51,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -70,13 +71,13 @@ import com.sadellie.unitto.core.base.R import com.sadellie.unitto.core.ui.common.MenuButton import com.sadellie.unitto.core.ui.common.SettingsButton import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar -import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField import com.sadellie.unitto.data.model.HistoryItem import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboardLoading +import com.sadellie.unitto.feature.calculator.components.HistoryItemHeight import com.sadellie.unitto.feature.calculator.components.HistoryList -import kotlinx.coroutines.launch +import com.sadellie.unitto.feature.calculator.components.TextBox import java.text.SimpleDateFormat import java.util.Locale @@ -179,146 +180,89 @@ private fun Ready( modifier = Modifier.padding(paddingValues), ) { val density = LocalDensity.current - var historyItemHeight by remember { mutableStateOf(0.dp) } - val textBoxHeight = maxHeight * 0.25f - var dragStateCurrentValue by rememberSaveable { mutableStateOf(DragState.CLOSED) } - val corScope = rememberCoroutineScope() - val dragState = rememberDragState( - historyItem = historyItemHeight, - max = maxHeight - textBoxHeight, - initialValue = dragStateCurrentValue, - enablePartialView = uiState.partialHistoryView - ) - val dragDp by remember(dragState) { + val textBoxHeight = maxHeight * 0.25f + + val dragState = remember { + AnchoredDraggableState( + initialValue = DragState.CLOSED, + positionalThreshold = { distance -> distance * 0.5f }, + velocityThreshold = { with(density) { HistoryItemHeight.toPx() } }, + animationSpec = tween() + ) + } + + var historyListHeight by remember { mutableStateOf(0.dp) } + val keyboardHeight by remember(historyListHeight) { derivedStateOf { - focusManager.clearFocus(true) - with(density) { - try { - dragState.requireOffset().toDp() - } catch (e: IllegalStateException) { - corScope.launch { dragState.snapTo(DragState.CLOSED) } - 0.dp + if (historyListHeight > HistoryItemHeight) { + maxHeight - textBoxHeight - HistoryItemHeight + } else { + maxHeight - textBoxHeight - historyListHeight + } + } + } + + LaunchedEffect(uiState.partialHistoryView) { + val anchors: DraggableAnchors = with(density) { + if (uiState.partialHistoryView) { + DraggableAnchors { + DragState.CLOSED at 0f + DragState.SMALL at HistoryItemHeight.toPx() + DragState.OPEN at (maxHeight * 0.75f).toPx() + } + } else { + DraggableAnchors { + DragState.CLOSED at 0f + DragState.OPEN at (maxHeight * 0.75f).toPx() } } } - } - val keyboardHeight by remember(dragState) { - derivedStateOf { - if (dragDp > historyItemHeight) { - maxHeight - textBoxHeight - historyItemHeight - } else { - maxHeight - textBoxHeight - dragDp - } - } + dragState.updateAnchors(anchors) } - LaunchedEffect(dragState.currentValue) { - dragStateCurrentValue = dragState.currentValue + LaunchedEffect(dragState.offset) { + with(density) { + if (!dragState.offset.isNaN()) { + historyListHeight = dragState.requireOffset().toDp() + } + } + + focusManager.clearFocus() showClearHistoryButton = dragState.currentValue == DragState.OPEN } - // History HistoryList( modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) .fillMaxWidth() - .height(dragDp), + .height(historyListHeight), historyItems = uiState.history, - heightCallback = { historyItemHeight = it }, formatterSymbols = uiState.formatterSymbols, - addTokens = addSymbol, + addTokens = addSymbol ) - // Input - Column( + TextBox( modifier = Modifier - .semantics { testTag = "inputBox" } - .offset(y = dragDp) + .offset(y = historyListHeight) .height(textBoxHeight) - .background( - MaterialTheme.colorScheme.surfaceVariant, - RoundedCornerShape( - topStartPercent = 0, topEndPercent = 0, - bottomStartPercent = 20, bottomEndPercent = 20 - ) - ) + .fillMaxWidth() .anchoredDraggable( state = dragState, orientation = Orientation.Vertical ) - .padding(top = 12.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - ExpressionTextField( - modifier = Modifier - .weight(2f) - .fillMaxWidth() - .padding(horizontal = 8.dp), - value = uiState.input, - minRatio = 0.5f, - cutCallback = deleteSymbol, - pasteCallback = addSymbol, - onCursorChange = onCursorChange, - formatterSymbols = uiState.formatterSymbols - ) - if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { - when (uiState.output) { - is CalculationResult.Default -> { - var output by remember(uiState.output) { - mutableStateOf(TextFieldValue(uiState.output.text)) - } + , + formatterSymbols = uiState.formatterSymbols, + input = uiState.input, + deleteSymbol = deleteSymbol, + addSymbol = addSymbol, + onCursorChange = onCursorChange, + output = uiState.output + ) - ExpressionTextField( - modifier = Modifier - .weight(1f) - .fillMaxWidth() - .padding(horizontal = 8.dp), - value = output, - minRatio = 1f, - onCursorChange = { output = output.copy(selection = it) }, - formatterSymbols = uiState.formatterSymbols, - textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f), - readOnly = true, - ) - } - - else -> { - val label = uiState.output.label?.let { stringResource(it) } ?: "" - - UnformattedTextField( - modifier = Modifier - .weight(1f) - .fillMaxWidth() - .padding(horizontal = 8.dp), - value = TextFieldValue(label), - minRatio = 1f, - onCursorChange = {}, - textColor = MaterialTheme.colorScheme.error, - readOnly = true, - ) - } - } - - } - // Handle - Box( - Modifier - .padding(8.dp) - .background( - MaterialTheme.colorScheme.onSurfaceVariant, - RoundedCornerShape(100) - ) - .sizeIn(24.dp, 4.dp) - ) - } - - // Keyboard CalculatorKeyboard( modifier = Modifier .semantics { testTag = "ready" } - .offset(y = dragDp + textBoxHeight) + .offset(y = historyListHeight + textBoxHeight) .height(keyboardHeight) .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 4.dp), diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DragState.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DragState.kt index 5b7f9a99..9af4e57a 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DragState.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/DragState.kt @@ -18,45 +18,4 @@ package com.sadellie.unitto.feature.calculator -import androidx.compose.animation.core.tween -import androidx.compose.foundation.gestures.AnchoredDraggableState -import androidx.compose.foundation.gestures.DraggableAnchors -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp - internal enum class DragState { CLOSED, SMALL, OPEN } - -@Composable -internal fun rememberDragState( - initialValue: DragState = DragState.CLOSED, - historyItem: Dp, - max: Dp, - enablePartialView: Boolean, -): AnchoredDraggableState { - val historyItemHeight = with(LocalDensity.current) { historyItem.toPx() } - val maxHeight = with(LocalDensity.current) { max.toPx() } - val anchors: DraggableAnchors = if (enablePartialView) { - DraggableAnchors { - DragState.CLOSED at 0f - DragState.SMALL at historyItemHeight - DragState.OPEN at maxHeight - } - } else { - DraggableAnchors { - DragState.CLOSED at 0f - DragState.OPEN at maxHeight - } - } - - return remember(historyItem, enablePartialView) { - AnchoredDraggableState( - initialValue = initialValue, - anchors = anchors, - positionalThreshold = { 0f }, - velocityThreshold = { 0f }, - animationSpec = tween() - ) - } -} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt index b74e42fd..fc344ecb 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -47,9 +48,7 @@ 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.LocalDensity import androidx.compose.ui.platform.LocalTextInputService import androidx.compose.ui.platform.LocalTextToolbar import androidx.compose.ui.platform.LocalView @@ -58,7 +57,6 @@ 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 androidx.compose.ui.unit.dp import com.sadellie.unitto.core.base.R import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer @@ -75,14 +73,12 @@ import java.util.Locale internal fun HistoryList( modifier: Modifier, historyItems: List, - heightCallback: (Dp) -> Unit, formatterSymbols: FormatterSymbols, addTokens: (String) -> Unit, ) { if (historyItems.isEmpty()) { HistoryListPlaceholder( modifier = modifier, - heightCallback = heightCallback ) } else { HistoryListContent( @@ -90,7 +86,6 @@ internal fun HistoryList( historyItems = historyItems, addTokens = addTokens, formatterSymbols = formatterSymbols, - heightCallback = heightCallback ) } } @@ -98,19 +93,14 @@ internal fun HistoryList( @Composable private fun HistoryListPlaceholder( modifier: Modifier, - heightCallback: (Dp) -> Unit, ) { - val density = LocalDensity.current - Column( modifier = modifier.wrapContentHeight(unbounded = true), - verticalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { Column( - modifier = Modifier - .onPlaced { heightCallback(with(density) { it.size.height.toDp() }) } - .fillMaxWidth() - .padding(vertical = 32.dp), + modifier = Modifier.height(HistoryItemHeight), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -126,12 +116,8 @@ private fun HistoryListContent( historyItems: List, addTokens: (String) -> Unit, formatterSymbols: FormatterSymbols, - heightCallback: (Dp) -> Unit, ) { - val density = LocalDensity.current val state = rememberLazyListState() - val firstItem by remember(historyItems) { mutableStateOf(historyItems.first()) } - val restOfTheItems by remember(firstItem) { mutableStateOf(historyItems.drop(1)) } LaunchedEffect(historyItems) { state.scrollToItem(0) } @@ -141,19 +127,8 @@ private fun HistoryListContent( reverseLayout = true, verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Bottom) ) { - // We do this so that callback for items height is called only once - item(firstItem.id) { + items(historyItems, { it.id }) { historyItem -> HistoryListItem( - modifier = Modifier.onPlaced { heightCallback(with(density) { it.size.height.toDp() }) }, - historyItem = historyItems.first(), - formatterSymbols = formatterSymbols, - addTokens = addTokens, - ) - } - - items(restOfTheItems, { it.id }) { historyItem -> - HistoryListItem( - modifier = Modifier, historyItem = historyItem, formatterSymbols = formatterSymbols, addTokens = addTokens, @@ -193,7 +168,10 @@ private fun HistoryListItem( } } - Column(modifier = modifier) { + Column( + modifier = modifier.height(HistoryItemHeight), + verticalArrangement = Arrangement.Center + ) { CompositionLocalProvider( LocalTextInputService provides null, LocalTextToolbar provides UnittoTextToolbar( @@ -246,6 +224,8 @@ private fun HistoryListItem( } } +internal val HistoryItemHeight = 92.dp + @Preview @Composable private fun PreviewHistoryList() { @@ -275,7 +255,6 @@ private fun PreviewHistoryList() { .fillMaxSize(), historyItems = historyItems, formatterSymbols = FormatterSymbols.Spaces, - heightCallback = {}, addTokens = {} ) } diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt new file mode 100644 index 00000000..7a377491 --- /dev/null +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/TextBox.kt @@ -0,0 +1,136 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.feature.calculator.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +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.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField +import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols +import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField +import com.sadellie.unitto.feature.calculator.CalculationResult + +@Composable +fun TextBox( + modifier: Modifier, + formatterSymbols: FormatterSymbols, + input: TextFieldValue, + deleteSymbol: () -> Unit, + addSymbol: (String) -> Unit, + onCursorChange: (TextRange) -> Unit, + output: CalculationResult, +) { + Column( + modifier = modifier + .semantics { testTag = "inputBox" } + .background( + MaterialTheme.colorScheme.surfaceVariant, + RoundedCornerShape( + topStartPercent = 0, topEndPercent = 0, + bottomStartPercent = 20, bottomEndPercent = 20 + ) + ) + .padding(top = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + ExpressionTextField( + modifier = Modifier + .weight(2f) + .fillMaxWidth() + .padding(horizontal = 8.dp), + value = input, + minRatio = 0.5f, + cutCallback = deleteSymbol, + pasteCallback = addSymbol, + onCursorChange = onCursorChange, + formatterSymbols = formatterSymbols + ) + if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { + when (output) { + is CalculationResult.Default -> { + var outputTF by remember(output) { + mutableStateOf(TextFieldValue(output.text)) + } + + ExpressionTextField( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 8.dp), + value = outputTF, + minRatio = 1f, + onCursorChange = { outputTF = outputTF.copy(selection = it) }, + formatterSymbols = formatterSymbols, + textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f), + readOnly = true, + ) + } + + else -> { + val label = output.label?.let { stringResource(it) } ?: "" + + UnformattedTextField( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 8.dp), + value = TextFieldValue(label), + minRatio = 1f, + onCursorChange = {}, + textColor = MaterialTheme.colorScheme.error, + readOnly = true, + ) + } + } + + } + // Handle + Box( + Modifier + .padding(8.dp) + .background( + MaterialTheme.colorScheme.onSurfaceVariant, + RoundedCornerShape(100) + ) + .sizeIn(24.dp, 4.dp) + ) + } +} \ No newline at end of file