Revert InputTextField

This commit is contained in:
Sad Ellie 2023-10-13 10:00:42 +03:00
parent 38da48049d
commit ccc7a2da4d
2 changed files with 77 additions and 215 deletions

View File

@ -44,19 +44,23 @@ import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity 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
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Paragraph
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation 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.TextUnit
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.sadellie.unitto.core.ui.theme.LocalNumberTypography import com.sadellie.unitto.core.ui.theme.LocalNumberTypography
import kotlin.math.ceil
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Composable @Composable
@ -110,7 +114,6 @@ fun ExpressionTextField(
visualTransformation = ExpressionTransformer(formatterSymbols), visualTransformation = ExpressionTransformer(formatterSymbols),
placeholder = placeholder, placeholder = placeholder,
textToolbar = textToolbar, textToolbar = textToolbar,
stepGranularityTextSize = 1.sp
) )
} }
@ -165,49 +168,77 @@ fun UnformattedTextField(
) )
} }
// https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300
@Composable @Composable
fun AutoSizableTextField( private fun AutoSizableTextField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
value: TextFieldValue, value: TextFieldValue,
formattedValue: String = value.text, formattedValue: String = value.text,
textStyle: TextStyle = TextStyle(), textStyle: TextStyle = TextStyle(),
scaleFactor: Float = 0.95f,
minRatio: Float = 1f,
onValueChange: (TextFieldValue) -> Unit, onValueChange: (TextFieldValue) -> Unit,
readOnly: Boolean = false, readOnly: Boolean = false,
showToolbar: (rect: Rect) -> Unit = {}, showToolbar: (rect: Rect) -> Unit = {},
hideToolbar: () -> Unit = {}, hideToolbar: () -> Unit = {},
visualTransformation: VisualTransformation = VisualTransformation.None, visualTransformation: VisualTransformation = VisualTransformation.None,
placeholder: String = "", placeholder: String? = null,
textToolbar: UnittoTextToolbar, textToolbar: UnittoTextToolbar
minRatio: Float = 1f,
stepGranularityTextSize: TextUnit = 1.sp,
suggestedFontSizes: List<TextUnit> = emptyList(),
) { ) {
val localDensity = LocalDensity.current
val density = localDensity.density
val focusRequester = remember { FocusRequester() } 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)) BoxWithConstraints(
// Change font scale to 1 modifier = modifier,
CompositionLocalProvider( contentAlignment = Alignment.BottomStart
LocalDensity provides Density(density = density, fontScale = 1F),
LocalTextInputService provides null,
LocalTextToolbar provides textToolbar
) { ) {
BoxWithConstraints( with(density) {
modifier = modifier, // Cursor handle is not visible without this, 0.836f is the minimum required factor here
contentAlignment = Alignment.BottomStart, nFontSize = maxHeight.toSp() * 0.83f
) { minFontSize = nFontSize * minRatio
val resizeableTextStyle = resizeableTextStyle( }
text = formattedValue.ifEmpty { placeholder },
textStyle = textStyle,
minRatio = 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( BasicTextField(
value = displayValue, value = textValue,
onValueChange = { onValueChange = {
showToolbar(Rect(offset, 0f)) showToolbar(Rect(offset, 0f))
hideToolbar() hideToolbar()
@ -225,38 +256,43 @@ fun AutoSizableTextField(
showToolbar(Rect(offset, 0f)) showToolbar(Rect(offset, 0f))
} }
) )
.widthIn(max = with(localDensity) { resizeableTextStyle.layoutResult.multiParagraph.width.toDp() }) .widthIn(max = with(density) { intrinsics.width.toDp() })
.layout { measurable, constraints -> .layout { measurable, constraints ->
val placeable = measurable.measure(constraints) val placeable = measurable.measure(constraints)
// TextField size is changed with a delay (text jumps). Here we correct it. // TextField size is changed with a delay (text jumps). Here we correct it.
layout(placeable.width, placeable.height) { layout(placeable.width, placeable.height) {
placeable.place( placeable.place(
x = (resizeableTextStyle.layoutResult.multiParagraph.width - resizeableTextStyle.layoutResult.multiParagraph.maxIntrinsicWidth) x = (intrinsics.width - intrinsics.maxIntrinsicWidth)
.coerceAtLeast(0f) .coerceAtLeast(0f)
.roundToInt(), .roundToInt(),
y = (placeable.height - resizeableTextStyle.layoutResult.multiParagraph.height).roundToInt() y = (placeable.height - intrinsics.height).roundToInt()
) )
} }
} }
.onGloballyPositioned { layoutCoords -> .onGloballyPositioned { layoutCoords ->
offset = layoutCoords.positionInWindow() offset = layoutCoords.positionInWindow()
}, },
readOnly = readOnly, textStyle = nTextStyle,
textStyle = resizeableTextStyle.textStyle,
singleLine = true,
visualTransformation = visualTransformation,
onTextLayout = {},
interactionSource = remember { MutableInteractionSource() },
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
singleLine = true,
readOnly = readOnly,
visualTransformation = visualTransformation,
decorationBox = { innerTextField -> decorationBox = { innerTextField ->
if (displayValue.text.isEmpty()) { if (textValue.text.isEmpty() and !placeholder.isNullOrEmpty()) {
Text( Text(
text = placeholder, text = placeholder!!, // It's not null, i swear
style = resizeableTextStyle.textStyle, style = nTextStyle,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), 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() innerTextField()
} }
) )

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<TextUnit> = 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<T>(
val value: T,
)
private fun <T> T.toImmutableWrapper() = ImmutableWrapper(this)
private operator fun <T> ImmutableWrapper<T>.getValue(thisRef: Any?, property: KProperty<*>) = value