mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 00:35:26 +02:00
Revert InputTextField
This commit is contained in:
parent
38da48049d
commit
ccc7a2da4d
@ -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<TextUnit> = 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()
|
||||
}
|
||||
)
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user