mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 08:45:27 +02:00
Reinventing the wheel, but better this time. Better approach for draggable history list in calculator:
- Using anchored draggable - Remembering button sizes - Additional buttons are not resizeable anymore closes #75
This commit is contained in:
parent
19f1f436c3
commit
9cfb35b03e
@ -20,18 +20,16 @@ package com.sadellie.unitto.feature.calculator
|
|||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.rememberSplineBasedDecay
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.gestures.draggable
|
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
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
|
||||||
@ -45,18 +43,17 @@ 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.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
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.layout.onPlaced
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.TextRange
|
||||||
@ -73,13 +70,9 @@ import com.sadellie.unitto.core.ui.common.textfield.ExpressionTextField
|
|||||||
import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField
|
import com.sadellie.unitto.core.ui.common.textfield.UnformattedTextField
|
||||||
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.HistoryList
|
import com.sadellie.unitto.feature.calculator.components.HistoryList
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CalculatorRoute(
|
internal fun CalculatorRoute(
|
||||||
@ -117,24 +110,15 @@ private fun CalculatorScreen(
|
|||||||
clearHistory: () -> Unit
|
clearHistory: () -> Unit
|
||||||
) {
|
) {
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val dragAmount = remember { Animatable(0f) }
|
|
||||||
val dragCoroutineScope = rememberCoroutineScope()
|
|
||||||
val dragAnimSpec = rememberSplineBasedDecay<Float>()
|
|
||||||
|
|
||||||
var textThingyHeight by remember { mutableIntStateOf(0) }
|
|
||||||
var historyItemHeight by remember { mutableIntStateOf(0) }
|
|
||||||
|
|
||||||
var showClearHistoryDialog by rememberSaveable { mutableStateOf(false) }
|
var showClearHistoryDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
val showClearHistoryButton by remember(dragAmount.value, historyItemHeight) {
|
var showClearHistoryButton by rememberSaveable { mutableStateOf(false) }
|
||||||
derivedStateOf { dragAmount.value > historyItemHeight }
|
|
||||||
}
|
|
||||||
|
|
||||||
UnittoScreenWithTopBar(
|
UnittoScreenWithTopBar(
|
||||||
title = { Text(stringResource(R.string.calculator)) },
|
title = { Text(stringResource(R.string.calculator)) },
|
||||||
navigationIcon = { MenuButton { navigateToMenu() } },
|
navigationIcon = { MenuButton { navigateToMenu() } },
|
||||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceVariant),
|
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
actions = {
|
actions = {
|
||||||
Crossfade(showClearHistoryButton) {
|
Crossfade(showClearHistoryButton, label = "Clear button reveal") {
|
||||||
if (it) {
|
if (it) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { showClearHistoryDialog = true },
|
onClick = { showClearHistoryDialog = true },
|
||||||
@ -151,140 +135,152 @@ private fun CalculatorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
DragDownView(
|
BoxWithConstraints(
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
drag = dragAmount.value.roundToInt().coerceAtLeast(0),
|
) {
|
||||||
historyItemHeight = historyItemHeight,
|
val density = LocalDensity.current
|
||||||
historyList = {
|
var historyItemHeight by remember { mutableStateOf(0.dp) }
|
||||||
HistoryList(
|
val textBoxHeight = maxHeight * 0.25f
|
||||||
modifier = Modifier
|
var dragStateCurrentValue by rememberSaveable { mutableStateOf(DragState.CLOSED) }
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
|
||||||
.fillMaxSize(),
|
|
||||||
historyItems = uiState.history,
|
|
||||||
historyItemHeightCallback = { historyItemHeight = it },
|
|
||||||
formatterSymbols = uiState.formatterSymbols,
|
|
||||||
addTokens = addSymbol,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
textFields = { maxDragAmount ->
|
|
||||||
Column(
|
|
||||||
Modifier
|
|
||||||
.fillMaxHeight(0.25f)
|
|
||||||
.onPlaced { textThingyHeight = it.size.height }
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
RoundedCornerShape(
|
|
||||||
topStartPercent = 0, topEndPercent = 0,
|
|
||||||
bottomStartPercent = 20, bottomEndPercent = 20
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.draggable(
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
state = rememberDraggableState { delta ->
|
|
||||||
dragCoroutineScope.launch {
|
|
||||||
val draggedAmount = (dragAmount.value + delta).coerceAtLeast(0f)
|
|
||||||
dragAmount.snapTo(draggedAmount)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragStarted = {
|
|
||||||
// Moving composables with focus causes performance drop
|
|
||||||
focusManager.clearFocus(true)
|
|
||||||
},
|
|
||||||
onDragStopped = { velocity ->
|
|
||||||
dragCoroutineScope.launch {
|
|
||||||
dragAmount.animateDecay(velocity, dragAnimSpec)
|
|
||||||
|
|
||||||
// Snap to closest anchor (0, one history item, all history items)
|
val dragState = rememberDragState(
|
||||||
val draggedAmount = listOf(0, historyItemHeight, maxDragAmount)
|
historyItem = historyItemHeight,
|
||||||
.minBy { abs(dragAmount.value.roundToInt() - it) }
|
max = maxHeight - textBoxHeight,
|
||||||
.toFloat()
|
initialValue = dragStateCurrentValue
|
||||||
dragAmount.animateTo(draggedAmount)
|
)
|
||||||
}
|
val dragDp by remember(dragState.requireOffset()) {
|
||||||
}
|
derivedStateOf {
|
||||||
|
focusManager.clearFocus(true)
|
||||||
|
with(density) { dragState.requireOffset().toDp() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val keyboardHeight by remember(dragState.requireOffset()) {
|
||||||
|
derivedStateOf {
|
||||||
|
if (dragDp > historyItemHeight) {
|
||||||
|
maxHeight - textBoxHeight - historyItemHeight
|
||||||
|
} else {
|
||||||
|
maxHeight - textBoxHeight - dragDp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(dragState.currentValue) {
|
||||||
|
dragStateCurrentValue = dragState.currentValue
|
||||||
|
showClearHistoryButton = dragState.currentValue == DragState.OPEN
|
||||||
|
}
|
||||||
|
|
||||||
|
// History
|
||||||
|
HistoryList(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(dragDp),
|
||||||
|
historyItems = uiState.history,
|
||||||
|
heightCallback = { historyItemHeight = it },
|
||||||
|
formatterSymbols = uiState.formatterSymbols,
|
||||||
|
addTokens = addSymbol,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Input
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.offset(y = dragDp)
|
||||||
|
.height(textBoxHeight)
|
||||||
|
.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 = uiState.input,
|
|
||||||
minRatio = 0.5f,
|
|
||||||
cutCallback = deleteSymbol,
|
|
||||||
pasteCallback = addSymbol,
|
|
||||||
onCursorChange = onCursorChange,
|
|
||||||
formatterSymbols = uiState.formatterSymbols
|
|
||||||
)
|
)
|
||||||
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
.anchoredDraggable(
|
||||||
when (uiState.output) {
|
state = dragState,
|
||||||
is CalculationResult.Default -> {
|
orientation = Orientation.Vertical
|
||||||
var output by remember(uiState.output) {
|
)
|
||||||
mutableStateOf(TextFieldValue(uiState.output.text))
|
.padding(top = 12.dp),
|
||||||
}
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
ExpressionTextField(
|
) {
|
||||||
modifier = Modifier
|
ExpressionTextField(
|
||||||
.weight(1f)
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.weight(2f)
|
||||||
.padding(horizontal = 8.dp),
|
.fillMaxWidth()
|
||||||
value = output,
|
.padding(horizontal = 8.dp),
|
||||||
minRatio = 1f,
|
value = uiState.input,
|
||||||
onCursorChange = { output = output.copy(selection = it) },
|
minRatio = 0.5f,
|
||||||
formatterSymbols = uiState.formatterSymbols,
|
cutCallback = deleteSymbol,
|
||||||
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f),
|
pasteCallback = addSymbol,
|
||||||
readOnly = true,
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
ExpressionTextField(
|
||||||
val label = uiState.output.label?.let { stringResource(it) } ?: ""
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
UnformattedTextField(
|
.fillMaxWidth()
|
||||||
modifier = Modifier
|
.padding(horizontal = 8.dp),
|
||||||
.weight(1f)
|
value = output,
|
||||||
.fillMaxWidth()
|
minRatio = 1f,
|
||||||
.padding(horizontal = 8.dp),
|
onCursorChange = { output = output.copy(selection = it) },
|
||||||
value = TextFieldValue(label),
|
formatterSymbols = uiState.formatterSymbols,
|
||||||
minRatio = 1f,
|
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f),
|
||||||
onCursorChange = {},
|
readOnly = true,
|
||||||
textColor = MaterialTheme.colorScheme.error,
|
)
|
||||||
readOnly = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
else -> {
|
||||||
// Handle
|
val label = uiState.output.label?.let { stringResource(it) } ?: ""
|
||||||
Box(
|
|
||||||
Modifier
|
UnformattedTextField(
|
||||||
.padding(8.dp)
|
modifier = Modifier
|
||||||
.background(
|
.weight(1f)
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
.fillMaxWidth()
|
||||||
RoundedCornerShape(100)
|
.padding(horizontal = 8.dp),
|
||||||
|
value = TextFieldValue(label),
|
||||||
|
minRatio = 1f,
|
||||||
|
onCursorChange = {},
|
||||||
|
textColor = MaterialTheme.colorScheme.error,
|
||||||
|
readOnly = true,
|
||||||
)
|
)
|
||||||
.sizeIn(24.dp, 4.dp)
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
// Handle
|
||||||
numPad = {
|
Box(
|
||||||
CalculatorKeyboard(
|
Modifier
|
||||||
modifier = Modifier
|
.padding(8.dp)
|
||||||
.fillMaxSize()
|
.background(
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
radianMode = uiState.radianMode,
|
RoundedCornerShape(100)
|
||||||
fractional = uiState.formatterSymbols.fractional,
|
)
|
||||||
allowVibration = uiState.allowVibration,
|
.sizeIn(24.dp, 4.dp)
|
||||||
addSymbol = addSymbol,
|
|
||||||
clearSymbols = clearSymbols,
|
|
||||||
deleteSymbol = deleteSymbol,
|
|
||||||
toggleAngleMode = toggleAngleMode,
|
|
||||||
evaluate = evaluate
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
// Keyboard
|
||||||
|
CalculatorKeyboard(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(y = dragDp + textBoxHeight)
|
||||||
|
.height(keyboardHeight)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
radianMode = uiState.radianMode,
|
||||||
|
fractional = uiState.formatterSymbols.fractional,
|
||||||
|
allowVibration = uiState.allowVibration,
|
||||||
|
addSymbol = addSymbol,
|
||||||
|
clearSymbols = clearSymbols,
|
||||||
|
deleteSymbol = deleteSymbol,
|
||||||
|
toggleAngleMode = toggleAngleMode,
|
||||||
|
evaluate = evaluate
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showClearHistoryDialog) {
|
if (showClearHistoryDialog) {
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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, HALF, OPEN }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun rememberDragState(
|
||||||
|
initialValue: DragState = DragState.CLOSED,
|
||||||
|
historyItem: Dp,
|
||||||
|
max: Dp,
|
||||||
|
): AnchoredDraggableState<DragState> {
|
||||||
|
val historyItemHeight = with(LocalDensity.current) { historyItem.toPx() }
|
||||||
|
val maxHeight = with(LocalDensity.current) { max.toPx() }
|
||||||
|
|
||||||
|
return remember(key1 = historyItem) {
|
||||||
|
AnchoredDraggableState(
|
||||||
|
initialValue = initialValue,
|
||||||
|
anchors = DraggableAnchors {
|
||||||
|
DragState.CLOSED at 0f
|
||||||
|
DragState.HALF at historyItemHeight
|
||||||
|
DragState.OPEN at maxHeight
|
||||||
|
},
|
||||||
|
positionalThreshold = { 0f },
|
||||||
|
velocityThreshold = { 0f },
|
||||||
|
animationSpec = tween()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,7 @@ 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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
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
|
||||||
@ -50,7 +51,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import com.sadellie.unitto.core.base.Token
|
import com.sadellie.unitto.core.base.Token
|
||||||
import com.sadellie.unitto.core.ui.common.ColumnWithConstraints
|
import com.sadellie.unitto.core.ui.common.ColumnWithConstraints
|
||||||
import com.sadellie.unitto.core.ui.common.KeyboardButtonAdditional
|
import com.sadellie.unitto.core.ui.common.KeyboardButtonAdditional
|
||||||
@ -160,23 +160,36 @@ private fun PortraitKeyboard(
|
|||||||
ColumnWithConstraints(
|
ColumnWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) { constraints ->
|
) { constraints ->
|
||||||
fun verticalFraction(fraction: Float): Dp = constraints.maxHeight * fraction
|
val mainButtonHorizontalPadding by remember {
|
||||||
fun horizontalFraction(fraction: Float): Dp = constraints.maxWidth * fraction
|
derivedStateOf { (constraints.maxWidth * 0.01f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val additionalButtonHeight by remember {
|
||||||
|
mutableStateOf(constraints.maxHeight * 0.09f)
|
||||||
|
}
|
||||||
|
|
||||||
|
val spacerHeight by remember {
|
||||||
|
mutableStateOf(constraints.maxHeight * 0.025f)
|
||||||
|
}
|
||||||
|
|
||||||
|
val additionalRowSpacedBy by remember {
|
||||||
|
mutableStateOf(constraints.maxWidth * 0.03f)
|
||||||
|
}
|
||||||
|
|
||||||
val weightModifier = Modifier.weight(1f)
|
val weightModifier = Modifier.weight(1f)
|
||||||
val mainButtonModifier = Modifier
|
val mainButtonModifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontalFraction(0.015f), verticalFraction(0.009f))
|
.padding(mainButtonHorizontalPadding)
|
||||||
val additionalButtonModifier = Modifier
|
val additionalButtonModifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.height(verticalFraction(0.09f))
|
.height(additionalButtonHeight)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(verticalFraction(0.025f)))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(horizontalFraction(0.03f))
|
horizontalArrangement = Arrangement.spacedBy(additionalRowSpacedBy)
|
||||||
) {
|
) {
|
||||||
// Additional buttons
|
// Additional buttons
|
||||||
Crossfade(invMode, weightModifier) {
|
Crossfade(invMode, weightModifier) {
|
||||||
@ -204,7 +217,7 @@ private fun PortraitKeyboard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.size(verticalFraction(0.09f)),
|
modifier = Modifier.size(additionalButtonHeight),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// Expand/Collapse
|
// Expand/Collapse
|
||||||
@ -217,7 +230,7 @@ private fun PortraitKeyboard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(verticalFraction(0.025f)))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
|
|
||||||
Row(weightModifier) {
|
Row(weightModifier) {
|
||||||
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.Operator.leftBracket) }
|
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.Operator.leftBracket) }
|
||||||
@ -250,7 +263,7 @@ private fun PortraitKeyboard(
|
|||||||
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Equal, allowVibration) { evaluate() }
|
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Equal, allowVibration) { evaluate() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(verticalFraction(0.015f)))
|
Spacer(modifier = Modifier.height(spacerHeight))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,92 +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.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.layout.SubcomposeLayout
|
|
||||||
import androidx.compose.ui.unit.offset
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Screen layout where [historyList] can be seen only when you drag [textFields] down.
|
|
||||||
*
|
|
||||||
* @param modifier [Modifier] that will be applied to [SubcomposeLayout].
|
|
||||||
* @param drag Drag amount. Update this when dragging [textFields].
|
|
||||||
* @param historyItemHeight Height of one item in [historyList].
|
|
||||||
* @param textFields This part of the UI should be used as a handle. Offsets when dragging.
|
|
||||||
* @param numPad Composable with buttons. Offsets when drag amount is higher than [historyItemHeight]
|
|
||||||
* (otherwise will just shrink).
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
internal fun DragDownView(
|
|
||||||
modifier: Modifier,
|
|
||||||
drag: Int,
|
|
||||||
historyItemHeight: Int,
|
|
||||||
historyList: @Composable () -> Unit,
|
|
||||||
textFields: @Composable (maxDragAmount: Int) -> Unit,
|
|
||||||
numPad: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
SubcomposeLayout(modifier = modifier) { constraints ->
|
|
||||||
val showHistory = drag < historyItemHeight
|
|
||||||
val offset = if (showHistory) (drag - historyItemHeight).coerceAtLeast(0) else 0
|
|
||||||
|
|
||||||
val textFieldPlaceables = subcompose(DragDownContent.TextFields) {
|
|
||||||
textFields(constraints.maxHeight)
|
|
||||||
}.map { it.measure(constraints.offset(offset)) }
|
|
||||||
val textFieldsHeight = textFieldPlaceables.maxByOrNull { it.height }?.height ?: 0
|
|
||||||
|
|
||||||
val historyListPlaceables = subcompose(DragDownContent.HistoryList) {
|
|
||||||
historyList()
|
|
||||||
}.map {
|
|
||||||
it.measure(
|
|
||||||
constraints.copy(
|
|
||||||
maxHeight = drag.coerceAtMost(constraints.maxHeight - textFieldsHeight)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val padding = if (showHistory) drag.coerceAtLeast(0) else historyItemHeight
|
|
||||||
val numPadConstraints = constraints
|
|
||||||
.offset(offset)
|
|
||||||
.copy(maxHeight = constraints.maxHeight - textFieldsHeight - padding)
|
|
||||||
val numPadPlaceables = subcompose(DragDownContent.NumPad, numPad).map {
|
|
||||||
it.measure(numPadConstraints)
|
|
||||||
}
|
|
||||||
|
|
||||||
layout(constraints.maxWidth, constraints.maxHeight) {
|
|
||||||
var yPos = 0
|
|
||||||
historyListPlaceables.forEach {
|
|
||||||
it.place(0, yPos)
|
|
||||||
yPos += it.height
|
|
||||||
}
|
|
||||||
|
|
||||||
textFieldPlaceables.forEach {
|
|
||||||
it.place(0, yPos)
|
|
||||||
yPos += it.height
|
|
||||||
}
|
|
||||||
|
|
||||||
numPadPlaceables.forEach {
|
|
||||||
it.place(0, yPos)
|
|
||||||
yPos += it.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class DragDownContent { HistoryList, TextFields, NumPad }
|
|
@ -49,6 +49,7 @@ 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.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalTextInputService
|
import androidx.compose.ui.platform.LocalTextInputService
|
||||||
import androidx.compose.ui.platform.LocalTextToolbar
|
import androidx.compose.ui.platform.LocalTextToolbar
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
@ -57,6 +58,7 @@ import androidx.compose.ui.text.TextRange
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.sadellie.unitto.core.base.R
|
import com.sadellie.unitto.core.base.R
|
||||||
import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer
|
import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer
|
||||||
@ -73,14 +75,14 @@ import java.util.Locale
|
|||||||
internal fun HistoryList(
|
internal fun HistoryList(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
historyItems: List<HistoryItem>,
|
historyItems: List<HistoryItem>,
|
||||||
historyItemHeightCallback: (Int) -> Unit,
|
heightCallback: (Dp) -> Unit,
|
||||||
formatterSymbols: FormatterSymbols,
|
formatterSymbols: FormatterSymbols,
|
||||||
addTokens: (String) -> Unit,
|
addTokens: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (historyItems.isEmpty()) {
|
if (historyItems.isEmpty()) {
|
||||||
HistoryListPlaceholder(
|
HistoryListPlaceholder(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
historyItemHeightCallback = historyItemHeightCallback
|
heightCallback = heightCallback
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
HistoryListContent(
|
HistoryListContent(
|
||||||
@ -88,7 +90,7 @@ internal fun HistoryList(
|
|||||||
historyItems = historyItems,
|
historyItems = historyItems,
|
||||||
addTokens = addTokens,
|
addTokens = addTokens,
|
||||||
formatterSymbols = formatterSymbols,
|
formatterSymbols = formatterSymbols,
|
||||||
historyItemHeightCallback = historyItemHeightCallback
|
heightCallback = heightCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,15 +98,17 @@ internal fun HistoryList(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun HistoryListPlaceholder(
|
private fun HistoryListPlaceholder(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
historyItemHeightCallback: (Int) -> Unit
|
heightCallback: (Dp) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.wrapContentHeight(unbounded = true),
|
modifier = modifier.wrapContentHeight(unbounded = true),
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.onPlaced { historyItemHeightCallback(it.size.height) }
|
.onPlaced { heightCallback(with(density) { it.size.height.toDp() }) }
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 32.dp),
|
.padding(vertical = 32.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
@ -122,8 +126,9 @@ private fun HistoryListContent(
|
|||||||
historyItems: List<HistoryItem>,
|
historyItems: List<HistoryItem>,
|
||||||
addTokens: (String) -> Unit,
|
addTokens: (String) -> Unit,
|
||||||
formatterSymbols: FormatterSymbols,
|
formatterSymbols: FormatterSymbols,
|
||||||
historyItemHeightCallback: (Int) -> Unit
|
heightCallback: (Dp) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
val firstItem by remember(historyItems) { mutableStateOf(historyItems.first()) }
|
val firstItem by remember(historyItems) { mutableStateOf(historyItems.first()) }
|
||||||
val restOfTheItems by remember(firstItem) { mutableStateOf(historyItems.drop(1)) }
|
val restOfTheItems by remember(firstItem) { mutableStateOf(historyItems.drop(1)) }
|
||||||
@ -139,7 +144,7 @@ private fun HistoryListContent(
|
|||||||
// We do this so that callback for items height is called only once
|
// We do this so that callback for items height is called only once
|
||||||
item(firstItem.id) {
|
item(firstItem.id) {
|
||||||
HistoryListItem(
|
HistoryListItem(
|
||||||
modifier = Modifier.onPlaced { historyItemHeightCallback(it.size.height) },
|
modifier = Modifier.onPlaced { heightCallback(with(density) { it.size.height.toDp() }) },
|
||||||
historyItem = historyItems.first(),
|
historyItem = historyItems.first(),
|
||||||
formatterSymbols = formatterSymbols,
|
formatterSymbols = formatterSymbols,
|
||||||
addTokens = addTokens,
|
addTokens = addTokens,
|
||||||
@ -270,7 +275,7 @@ private fun PreviewHistoryList() {
|
|||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
historyItems = historyItems,
|
historyItems = historyItems,
|
||||||
formatterSymbols = FormatterSymbols.Spaces,
|
formatterSymbols = FormatterSymbols.Spaces,
|
||||||
historyItemHeightCallback = {},
|
heightCallback = {},
|
||||||
addTokens = {}
|
addTokens = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user