mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 08:45:27 +02:00
Refactor InputTextField
This commit is contained in:
parent
337a68b623
commit
aa2f66f891
@ -44,23 +44,19 @@ 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.Constraints
|
import androidx.compose.ui.unit.Density
|
||||||
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
|
||||||
@ -74,7 +70,7 @@ fun ExpressionTextField(
|
|||||||
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
formatterSymbols: FormatterSymbols,
|
formatterSymbols: FormatterSymbols,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
placeholder: String? = null,
|
placeholder: String = "",
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
fun copyCallback() {
|
fun copyCallback() {
|
||||||
@ -113,7 +109,8 @@ fun ExpressionTextField(
|
|||||||
hideToolbar = textToolbar::hide,
|
hideToolbar = textToolbar::hide,
|
||||||
visualTransformation = ExpressionTransformer(formatterSymbols),
|
visualTransformation = ExpressionTransformer(formatterSymbols),
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
textToolbar = textToolbar
|
textToolbar = textToolbar,
|
||||||
|
stepGranularityTextSize = 1.sp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +124,7 @@ fun UnformattedTextField(
|
|||||||
onCursorChange: (TextRange) -> Unit,
|
onCursorChange: (TextRange) -> Unit,
|
||||||
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
placeholder: String? = null,
|
placeholder: String = "",
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
val clipboardManager = LocalClipboardManager.current
|
||||||
fun copyCallback() {
|
fun copyCallback() {
|
||||||
@ -168,77 +165,49 @@ fun UnformattedTextField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300
|
||||||
@Composable
|
@Composable
|
||||||
private fun AutoSizableTextField(
|
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? = null,
|
placeholder: String = "",
|
||||||
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
|
|
||||||
|
|
||||||
val textValue = value.copy(value.text.take(2000))
|
var offset by remember { mutableStateOf(Offset.Zero) }
|
||||||
var nFontSize: TextUnit by remember { mutableStateOf(0.sp) }
|
|
||||||
var minFontSize: TextUnit
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
|
||||||
modifier = modifier,
|
|
||||||
contentAlignment = Alignment.BottomStart
|
|
||||||
) {
|
|
||||||
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
|
|
||||||
|
|
||||||
|
val displayValue = value.copy(text = value.text.take(2000))
|
||||||
|
// Change font scale to 1
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
LocalDensity provides Density(density = density, fontScale = 1F),
|
||||||
LocalTextInputService provides null,
|
LocalTextInputService provides null,
|
||||||
LocalTextToolbar provides textToolbar
|
LocalTextToolbar provides textToolbar
|
||||||
) {
|
) {
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier,
|
||||||
|
contentAlignment = Alignment.BottomStart,
|
||||||
|
) {
|
||||||
|
val resizeableTextStyle = resizeableTextStyle(
|
||||||
|
text = formattedValue.ifEmpty { placeholder },
|
||||||
|
textStyle = textStyle,
|
||||||
|
minRatio = minRatio
|
||||||
|
)
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = textValue,
|
value = displayValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
showToolbar(Rect(offset, 0f))
|
showToolbar(Rect(offset, 0f))
|
||||||
hideToolbar()
|
hideToolbar()
|
||||||
@ -256,43 +225,38 @@ private fun AutoSizableTextField(
|
|||||||
showToolbar(Rect(offset, 0f))
|
showToolbar(Rect(offset, 0f))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.widthIn(max = with(density) { intrinsics.width.toDp() })
|
.widthIn(max = with(localDensity) { resizeableTextStyle.layoutResult.multiParagraph.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 = (intrinsics.width - intrinsics.maxIntrinsicWidth)
|
x = (resizeableTextStyle.layoutResult.multiParagraph.width - resizeableTextStyle.layoutResult.multiParagraph.maxIntrinsicWidth)
|
||||||
.coerceAtLeast(0f)
|
.coerceAtLeast(0f)
|
||||||
.roundToInt(),
|
.roundToInt(),
|
||||||
y = (placeable.height - intrinsics.height).roundToInt()
|
y = (placeable.height - resizeableTextStyle.layoutResult.multiParagraph.height).roundToInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.onGloballyPositioned { layoutCoords ->
|
.onGloballyPositioned { layoutCoords ->
|
||||||
offset = layoutCoords.positionInWindow()
|
offset = layoutCoords.positionInWindow()
|
||||||
},
|
},
|
||||||
textStyle = nTextStyle,
|
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
|
|
||||||
singleLine = true,
|
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
|
textStyle = resizeableTextStyle.textStyle,
|
||||||
|
singleLine = true,
|
||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
|
onTextLayout = {},
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
if (textValue.text.isEmpty() and !placeholder.isNullOrEmpty()) {
|
if (displayValue.text.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = placeholder!!, // It's not null, i swear
|
text = placeholder,
|
||||||
style = nTextStyle,
|
style = resizeableTextStyle.textStyle,
|
||||||
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()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -320,7 +284,7 @@ fun ClipboardManager.copyWithoutGrouping(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ClipboardManager.copy(value: TextFieldValue) = this.setText(
|
private fun ClipboardManager.copy(value: TextFieldValue) = this.setText(
|
||||||
AnnotatedString(
|
AnnotatedString(
|
||||||
value.annotatedString
|
value.annotatedString
|
||||||
.subSequence(value.selection)
|
.subSequence(value.selection)
|
||||||
|
@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
@ -111,7 +111,7 @@ fun TextBox(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
value = TextFieldValue(label),
|
value = TextFieldValue(label),
|
||||||
minRatio = 1f,
|
minRatio = 0.8f,
|
||||||
onCursorChange = {},
|
onCursorChange = {},
|
||||||
textColor = MaterialTheme.colorScheme.error,
|
textColor = MaterialTheme.colorScheme.error,
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
|
@ -288,7 +288,7 @@ private fun Default(
|
|||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
content1 = { contentModifier ->
|
content1 = { contentModifier ->
|
||||||
ColumnWithConstraints(modifier = contentModifier) {
|
ColumnWithConstraints(modifier = contentModifier) {
|
||||||
val textFieldModifier = Modifier.weight(2f)
|
val textFieldModifier = Modifier.fillMaxWidth().weight(2f)
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = lastUpdate != null,
|
visible = lastUpdate != null,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user