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 androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@ -45,18 +43,17 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onPlaced
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.data.model.HistoryItem
|
||||
import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard
|
||||
import com.sadellie.unitto.feature.calculator.components.DragDownView
|
||||
import com.sadellie.unitto.feature.calculator.components.HistoryList
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
internal fun CalculatorRoute(
|
||||
@ -117,24 +110,15 @@ private fun CalculatorScreen(
|
||||
clearHistory: () -> Unit
|
||||
) {
|
||||
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) }
|
||||
val showClearHistoryButton by remember(dragAmount.value, historyItemHeight) {
|
||||
derivedStateOf { dragAmount.value > historyItemHeight }
|
||||
}
|
||||
var showClearHistoryButton by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
UnittoScreenWithTopBar(
|
||||
title = { Text(stringResource(R.string.calculator)) },
|
||||
navigationIcon = { MenuButton { navigateToMenu() } },
|
||||
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
actions = {
|
||||
Crossfade(showClearHistoryButton) {
|
||||
Crossfade(showClearHistoryButton, label = "Clear button reveal") {
|
||||
if (it) {
|
||||
IconButton(
|
||||
onClick = { showClearHistoryDialog = true },
|
||||
@ -151,140 +135,152 @@ private fun CalculatorScreen(
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
DragDownView(
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
drag = dragAmount.value.roundToInt().coerceAtLeast(0),
|
||||
historyItemHeight = historyItemHeight,
|
||||
historyList = {
|
||||
HistoryList(
|
||||
modifier = Modifier
|
||||
.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)
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
var historyItemHeight by remember { mutableStateOf(0.dp) }
|
||||
val textBoxHeight = maxHeight * 0.25f
|
||||
var dragStateCurrentValue by rememberSaveable { mutableStateOf(DragState.CLOSED) }
|
||||
|
||||
// Snap to closest anchor (0, one history item, all history items)
|
||||
val draggedAmount = listOf(0, historyItemHeight, maxDragAmount)
|
||||
.minBy { abs(dragAmount.value.roundToInt() - it) }
|
||||
.toFloat()
|
||||
dragAmount.animateTo(draggedAmount)
|
||||
}
|
||||
}
|
||||
val dragState = rememberDragState(
|
||||
historyItem = historyItemHeight,
|
||||
max = maxHeight - textBoxHeight,
|
||||
initialValue = dragStateCurrentValue
|
||||
)
|
||||
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) {
|
||||
when (uiState.output) {
|
||||
is CalculationResult.Default -> {
|
||||
var output by remember(uiState.output) {
|
||||
mutableStateOf(TextFieldValue(uiState.output.text))
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
.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))
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
// Handle
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
RoundedCornerShape(100)
|
||||
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,
|
||||
)
|
||||
.sizeIn(24.dp, 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
numPad = {
|
||||
CalculatorKeyboard(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.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
|
||||
// Handle
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
RoundedCornerShape(100)
|
||||
)
|
||||
.sizeIn(24.dp, 4.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// 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) {
|
||||
|
@ -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.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -50,7 +51,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.sadellie.unitto.core.base.Token
|
||||
import com.sadellie.unitto.core.ui.common.ColumnWithConstraints
|
||||
import com.sadellie.unitto.core.ui.common.KeyboardButtonAdditional
|
||||
@ -160,23 +160,36 @@ private fun PortraitKeyboard(
|
||||
ColumnWithConstraints(
|
||||
modifier = modifier
|
||||
) { constraints ->
|
||||
fun verticalFraction(fraction: Float): Dp = constraints.maxHeight * fraction
|
||||
fun horizontalFraction(fraction: Float): Dp = constraints.maxWidth * fraction
|
||||
val mainButtonHorizontalPadding by remember {
|
||||
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 mainButtonModifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(1f)
|
||||
.padding(horizontalFraction(0.015f), verticalFraction(0.009f))
|
||||
.padding(mainButtonHorizontalPadding)
|
||||
val additionalButtonModifier = Modifier
|
||||
.weight(1f)
|
||||
.height(verticalFraction(0.09f))
|
||||
.height(additionalButtonHeight)
|
||||
|
||||
Spacer(modifier = Modifier.height(verticalFraction(0.025f)))
|
||||
Spacer(modifier = Modifier.height(spacerHeight))
|
||||
|
||||
Row(
|
||||
modifier = Modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(horizontalFraction(0.03f))
|
||||
horizontalArrangement = Arrangement.spacedBy(additionalRowSpacedBy)
|
||||
) {
|
||||
// Additional buttons
|
||||
Crossfade(invMode, weightModifier) {
|
||||
@ -204,7 +217,7 @@ private fun PortraitKeyboard(
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(verticalFraction(0.09f)),
|
||||
modifier = Modifier.size(additionalButtonHeight),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// Expand/Collapse
|
||||
@ -217,7 +230,7 @@ private fun PortraitKeyboard(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(verticalFraction(0.025f)))
|
||||
Spacer(modifier = Modifier.height(spacerHeight))
|
||||
|
||||
Row(weightModifier) {
|
||||
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.Operator.leftBracket) }
|
||||
@ -250,7 +263,7 @@ private fun PortraitKeyboard(
|
||||
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.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
|
||||
@ -57,6 +58,7 @@ 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
|
||||
@ -73,14 +75,14 @@ import java.util.Locale
|
||||
internal fun HistoryList(
|
||||
modifier: Modifier,
|
||||
historyItems: List<HistoryItem>,
|
||||
historyItemHeightCallback: (Int) -> Unit,
|
||||
heightCallback: (Dp) -> Unit,
|
||||
formatterSymbols: FormatterSymbols,
|
||||
addTokens: (String) -> Unit,
|
||||
) {
|
||||
if (historyItems.isEmpty()) {
|
||||
HistoryListPlaceholder(
|
||||
modifier = modifier,
|
||||
historyItemHeightCallback = historyItemHeightCallback
|
||||
heightCallback = heightCallback
|
||||
)
|
||||
} else {
|
||||
HistoryListContent(
|
||||
@ -88,7 +90,7 @@ internal fun HistoryList(
|
||||
historyItems = historyItems,
|
||||
addTokens = addTokens,
|
||||
formatterSymbols = formatterSymbols,
|
||||
historyItemHeightCallback = historyItemHeightCallback
|
||||
heightCallback = heightCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -96,15 +98,17 @@ internal fun HistoryList(
|
||||
@Composable
|
||||
private fun HistoryListPlaceholder(
|
||||
modifier: Modifier,
|
||||
historyItemHeightCallback: (Int) -> Unit
|
||||
heightCallback: (Dp) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
Column(
|
||||
modifier = modifier.wrapContentHeight(unbounded = true),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.onPlaced { historyItemHeightCallback(it.size.height) }
|
||||
.onPlaced { heightCallback(with(density) { it.size.height.toDp() }) }
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 32.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
@ -122,8 +126,9 @@ private fun HistoryListContent(
|
||||
historyItems: List<HistoryItem>,
|
||||
addTokens: (String) -> Unit,
|
||||
formatterSymbols: FormatterSymbols,
|
||||
historyItemHeightCallback: (Int) -> Unit
|
||||
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)) }
|
||||
@ -139,7 +144,7 @@ private fun HistoryListContent(
|
||||
// We do this so that callback for items height is called only once
|
||||
item(firstItem.id) {
|
||||
HistoryListItem(
|
||||
modifier = Modifier.onPlaced { historyItemHeightCallback(it.size.height) },
|
||||
modifier = Modifier.onPlaced { heightCallback(with(density) { it.size.height.toDp() }) },
|
||||
historyItem = historyItems.first(),
|
||||
formatterSymbols = formatterSymbols,
|
||||
addTokens = addTokens,
|
||||
@ -270,7 +275,7 @@ private fun PreviewHistoryList() {
|
||||
.fillMaxSize(),
|
||||
historyItems = historyItems,
|
||||
formatterSymbols = FormatterSymbols.Spaces,
|
||||
historyItemHeightCallback = {},
|
||||
heightCallback = {},
|
||||
addTokens = {}
|
||||
)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user