From ccc7a2da4d7052acad4dac6f9e88529618dd7951 Mon Sep 17 00:00:00 2001 From: Sad Ellie Date: Fri, 13 Oct 2023 10:00:42 +0300 Subject: [PATCH] Revert InputTextField --- .../ui/common/textfield/InputTextField.kt | 118 +++++++----- .../core/ui/common/textfield/Resizeable.kt | 174 ------------------ 2 files changed, 77 insertions(+), 215 deletions(-) delete mode 100644 core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/Resizeable.kt diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt index 3ba987cd..04bbc08c 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt @@ -44,19 +44,23 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalTextInputService import androidx.compose.ui.platform.LocalTextToolbar import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Paragraph import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import com.sadellie.unitto.core.ui.theme.LocalNumberTypography +import kotlin.math.ceil import kotlin.math.roundToInt @Composable @@ -110,7 +114,6 @@ fun ExpressionTextField( visualTransformation = ExpressionTransformer(formatterSymbols), placeholder = placeholder, textToolbar = textToolbar, - stepGranularityTextSize = 1.sp ) } @@ -165,49 +168,77 @@ fun UnformattedTextField( ) } -// https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300 @Composable -fun AutoSizableTextField( +private fun AutoSizableTextField( modifier: Modifier = Modifier, value: TextFieldValue, formattedValue: String = value.text, textStyle: TextStyle = TextStyle(), + scaleFactor: Float = 0.95f, + minRatio: Float = 1f, onValueChange: (TextFieldValue) -> Unit, readOnly: Boolean = false, showToolbar: (rect: Rect) -> Unit = {}, hideToolbar: () -> Unit = {}, visualTransformation: VisualTransformation = VisualTransformation.None, - placeholder: String = "", - textToolbar: UnittoTextToolbar, - minRatio: Float = 1f, - stepGranularityTextSize: TextUnit = 1.sp, - suggestedFontSizes: List = emptyList(), + placeholder: String? = null, + textToolbar: UnittoTextToolbar ) { - val localDensity = LocalDensity.current - val density = localDensity.density val focusRequester = remember { FocusRequester() } + val density = LocalDensity.current - var offset by remember { mutableStateOf(Offset.Zero) } + val textValue = value.copy(value.text.take(2000)) + var nFontSize: TextUnit by remember { mutableStateOf(0.sp) } + var minFontSize: TextUnit - val displayValue = value.copy(text = value.text.take(2000)) - // Change font scale to 1 - CompositionLocalProvider( - LocalDensity provides Density(density = density, fontScale = 1F), - LocalTextInputService provides null, - LocalTextToolbar provides textToolbar + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.BottomStart ) { - BoxWithConstraints( - modifier = modifier, - contentAlignment = Alignment.BottomStart, - ) { - val resizeableTextStyle = resizeableTextStyle( - text = formattedValue.ifEmpty { placeholder }, - textStyle = textStyle, - minRatio = minRatio - ) + with(density) { + // Cursor handle is not visible without this, 0.836f is the minimum required factor here + nFontSize = maxHeight.toSp() * 0.83f + minFontSize = nFontSize * minRatio + } + // Modified: https://blog.canopas.com/autosizing-textfield-in-jetpack-compose-7a80f0270853 + val calculateParagraph = @Composable { + Paragraph( + text = formattedValue, + style = textStyle.copy(fontSize = nFontSize), + constraints = Constraints( + maxWidth = ceil(with(density) { maxWidth.toPx() }).toInt() + ), + density = density, + fontFamilyResolver = createFontFamilyResolver(LocalContext.current), + spanStyles = listOf(), + placeholders = listOf(), + maxLines = 1, + ellipsis = false + ) + } + + var intrinsics = calculateParagraph() + with(density) { + while ((intrinsics.maxIntrinsicWidth > maxWidth.toPx()) && nFontSize >= minFontSize) { + nFontSize *= scaleFactor + intrinsics = calculateParagraph() + } + } + + val nTextStyle = textStyle.copy( + // https://issuetracker.google.com/issues/266470454 + // textAlign = TextAlign.End, + fontSize = nFontSize + ) + var offset = Offset.Zero + + CompositionLocalProvider( + LocalTextInputService provides null, + LocalTextToolbar provides textToolbar + ) { BasicTextField( - value = displayValue, + value = textValue, onValueChange = { showToolbar(Rect(offset, 0f)) hideToolbar() @@ -225,38 +256,43 @@ fun AutoSizableTextField( showToolbar(Rect(offset, 0f)) } ) - .widthIn(max = with(localDensity) { resizeableTextStyle.layoutResult.multiParagraph.width.toDp() }) + .widthIn(max = with(density) { intrinsics.width.toDp() }) .layout { measurable, constraints -> val placeable = measurable.measure(constraints) // TextField size is changed with a delay (text jumps). Here we correct it. layout(placeable.width, placeable.height) { placeable.place( - x = (resizeableTextStyle.layoutResult.multiParagraph.width - resizeableTextStyle.layoutResult.multiParagraph.maxIntrinsicWidth) + x = (intrinsics.width - intrinsics.maxIntrinsicWidth) .coerceAtLeast(0f) .roundToInt(), - y = (placeable.height - resizeableTextStyle.layoutResult.multiParagraph.height).roundToInt() + y = (placeable.height - intrinsics.height).roundToInt() ) } } - .onGloballyPositioned { layoutCoords -> offset = layoutCoords.positionInWindow() }, - readOnly = readOnly, - textStyle = resizeableTextStyle.textStyle, - singleLine = true, - visualTransformation = visualTransformation, - onTextLayout = {}, - interactionSource = remember { MutableInteractionSource() }, + textStyle = nTextStyle, cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), + singleLine = true, + readOnly = readOnly, + visualTransformation = visualTransformation, decorationBox = { innerTextField -> - if (displayValue.text.isEmpty()) { + if (textValue.text.isEmpty() and !placeholder.isNullOrEmpty()) { Text( - text = placeholder, - style = resizeableTextStyle.textStyle, + text = placeholder!!, // It's not null, i swear + style = nTextStyle, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + modifier = Modifier.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + + layout(placeable.width, placeable.height) { + placeable.place(x = -placeable.width, y = 0) + } + } ) } + innerTextField() } ) diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/Resizeable.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/Resizeable.kt deleted file mode 100644 index 96f8a17e..00000000 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/Resizeable.kt +++ /dev/null @@ -1,174 +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 . - */ - -package com.sadellie.unitto.core.ui.common.textfield - -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.text.InternalFoundationTextApi -import androidx.compose.foundation.text.TextDelegate -import androidx.compose.material3.LocalTextStyle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFontFamilyResolver -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.isUnspecified -import androidx.compose.ui.unit.min -import androidx.compose.ui.unit.sp -import kotlin.math.ceil -import kotlin.math.floor -import kotlin.reflect.KProperty - -@Composable -internal fun BoxWithConstraintsScope.resizeableTextStyle( - text: String, - textStyle: TextStyle, - minRatio: Float, - stepGranularityTextSize: TextUnit = 1.sp, - suggestedFontSizes: List = emptyList(), -): ResizeableTextStyle { - val fontSizes = suggestedFontSizes.toImmutableWrapper() - val localDensity = LocalDensity.current - val maxTextSize by remember { - with(localDensity) { - mutableStateOf(maxHeight.toSp()) - } - } - - val minTextSize by remember { - mutableStateOf(maxTextSize * minRatio) - } - - // (1 / density).sp represents 1px when font scale equals 1 - val step = remember(stepGranularityTextSize) { - (1 / localDensity.density).let { - if (stepGranularityTextSize.isUnspecified) - it.sp - else - stepGranularityTextSize.value.coerceAtLeast(it).sp - } - } - - val max = remember(maxWidth, maxHeight, maxTextSize) { - min(maxWidth, maxHeight).value.let { - if (maxTextSize.isUnspecified) - it.sp - else - maxTextSize.value.coerceAtMost(it).sp - } - } - - val min = remember(minTextSize, step, max) { - if (minTextSize.isUnspecified) - step - else - minTextSize.value.coerceIn( - minimumValue = step.value, - maximumValue = max.value - ).sp - } - - val possibleFontSizes = remember(fontSizes, min, max, step) { - if (fontSizes.value.isEmpty()) { - val firstIndex = ceil(min.value / step.value).toInt() - val lastIndex = floor(max.value / step.value).toInt() - MutableList(size = (lastIndex - firstIndex) + 1) { index -> - step * (lastIndex - index) - } - } else - fontSizes.value.filter { - it.isSp && it.value in min.value..max.value - }.sortedByDescending { - it.value - } - } - - var combinedTextStyle = LocalTextStyle.current + textStyle - var layoutResult: TextLayoutResult = layoutText( - text = text, - textStyle = combinedTextStyle, - maxLines = 1, - softWrap = false, - ) - - if (possibleFontSizes.isNotEmpty()) { - // Dichotomous binary search - var low = 0 - var high = possibleFontSizes.lastIndex - while (low <= high) { - val mid = low + (high - low) / 2 - layoutResult = layoutText( - text = text, - textStyle = combinedTextStyle.copy(fontSize = possibleFontSizes[mid]), - maxLines = 1, - softWrap = false, - ) - - if (layoutResult.hasVisualOverflow) low = mid + 1 - else high = mid - 1 - } - combinedTextStyle = combinedTextStyle.copy( - fontSize = possibleFontSizes[low.coerceIn(possibleFontSizes.indices)], - ) - } - - return ResizeableTextStyle( - combinedTextStyle, layoutResult - ) -} - -@OptIn(InternalFoundationTextApi::class) -@Composable -internal fun BoxWithConstraintsScope.layoutText( - text: String, - textStyle: TextStyle, - maxLines: Int, - softWrap: Boolean, -): TextLayoutResult = TextDelegate( - text = AnnotatedString(text), - style = textStyle, - maxLines = maxLines, - softWrap = softWrap, - overflow = TextOverflow.Clip, - density = LocalDensity.current, - fontFamilyResolver = LocalFontFamilyResolver.current, -) - .layout(constraints, LocalLayoutDirection.current) - -@Immutable -internal data class ResizeableTextStyle( - val textStyle: TextStyle, - val layoutResult: TextLayoutResult, -) - -@Immutable -private data class ImmutableWrapper( - val value: T, -) - -private fun T.toImmutableWrapper() = ImmutableWrapper(this) - -private operator fun ImmutableWrapper.getValue(thisRef: Any?, property: KProperty<*>) = value