Edhar Khimich
Edhar Khimich

Reputation: 1654

Adjustable by entered text OutlinedTextField in Jetpack Compose

I have an OutlinedTextField with DropdownMenu inside it I want that after pressing on the item inside the DropdownMenu list, the value of the item started to be inside the OutlinedTextField being also adjusted by width depending of how long the text is. How I can do it ?

enter image description here

Update (14.02.2022)

By default OutlinedTextField uses OutlinedTextFieldLayout which contains BasicTextField with defaultMinSize Modifier parameter.

BasicTextField(
    value = value,
    modifier = modifier
        .then(
            if (decoratedLabel != null) {
                Modifier.padding(top = OutlinedTextFieldTopPadding)
            } else {
                Modifier
            }
        )
        .defaultMinSize(
            minWidth = MinWidth,
            minHeight = MinHeight
        )
...

 /** The default min width applied for a TextField and OutlinedTextField. Note that you can override it by applying Modifier.widthIn directly on a text field. */
 val MinWidth = 280.dp

To make the width be Intrinsic Min I should have to duplicate 3 files (TextField.kt, TextFieldImpl.kt, OutlinedTextField.kt) from Compose library and make my own OutlinedTextField with these changes in OutlinedTextFieldLayout component:

 @Composable
internal fun OutlinedFormTextFieldLayout(
    modifier: Modifier,
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit,
    enabled: Boolean,
    readOnly: Boolean,
    keyboardOptions: KeyboardOptions,
    keyboardActions: KeyboardActions,
    textStyle: TextStyle,
    singleLine: Boolean,
    maxLines: Int = Int.MAX_VALUE,
    visualTransformation: VisualTransformation,
    interactionSource: MutableInteractionSource,
    decoratedPlaceholder: @Composable ((Modifier) -> Unit)?,
    decoratedLabel: @Composable (() -> Unit)?,
    leading: @Composable (() -> Unit)?,
    trailing: @Composable (() -> Unit)?,
    leadingColor: Color,
    trailingColor: Color,
    labelProgress: Float,
    indicatorWidth: Dp,
    indicatorColor: Color,
    cursorColor: Color,
    shape: Shape
) {
    val textWidth = remember { mutableStateOf(0) }
    val labelSize = remember { mutableStateOf(Size.Zero) }

    fun Modifier.widthIntrinsicSizeMinModifier() = width(IntrinsicSize.Min)
    fun Modifier.widthTextWidthModifier() = width(textWidth.value.dp)

    if (textWidth.value == 0) {
        modifier.then(Modifier.widthIntrinsicSizeMinModifier())
    } else {
        modifier.then(Modifier.widthTextWidthModifier())
    }

    BasicTextField(
        value = value,
        modifier = modifier
            .then(
                if (decoratedLabel != null) {
                    Modifier.padding(top = OutlinedTextFieldTopPadding)
                } else {
                    Modifier
                }
            )
            .onSizeChanged {
                textWidth.value = it.width
            }, ...

With these changes we don't have a default width anymore but we still have some spacing left from the right side

enter image description here

Update(15.02.2022)

Don't duplicate files from @Compose library. Some of the API call won't work. In my case textColor and background set was not working for my custom OutlinedFormTextField where for OutlinedTextField everything was working fine:

  colors = TextFieldDefaults.outlinedTextFieldColors(
       textColor = Color.Red,
       backgroundColor = backgroundColor.value
...

I also found that instead of overwriting the files which related to OutlinedTextField somehow you can just wrap your OutlinedTextField with Row component and set in it:

Modifier.defaultMinSize(minWidth = 1.dp)

It will remove the huge minWidth that Compose suggest by default but the extra spacing after the label is still exist.

Does anyone knows how to remove it ?

Upvotes: 2

Views: 3127

Answers (2)

vovahost
vovahost

Reputation: 35997

To force the BasicTextField to warp its width instead of using the defaut minWidth = 280 dp use the following modifier:

BasicTextField(
    modifier = modifier
      .width(IntrinsicSize.Min)
)

What doesn't work: Setting the Modifier.widthIn(min = 1.dp).

Upvotes: 0

Edhar Khimich
Edhar Khimich

Reputation: 1654

Here is my temporary solution with adjustable OutlinedTextField component. It also fix the OutlinedTextField crossed label on the top left corner when the background was changed:

// Duration
private const val LabelOffsetAnimationDuration = 500
private const val LabelBackgroundColorAnimationDuration = 1

private const val LabelBackgroundColorAnimationDelay = 200

private const val TextFieldBackgroundColorAnimationDuration = 0
private const val TextFieldBackgroundColorAnimationDelay = 0

private const val BorderColorAnimationDuration = 10
private const val BorderColorAnimationDelay = 50

// Offset
private const val LabelAnimationTopLeftPositionOffsetX = 20F
private const val LabelAnimationTopLeftPositionOffsetY = -30F
private const val LabelAnimationCenterPositionOffsetX = 25F
private const val LabelAnimationCenterPositionOffsetY = 0F

private const val LabelTextSizeAnimationTopLeftPosition = 12F
private const val LabelTextSizeAnimationCenterPosition = 16F

// Z-Index
private const val LabelBubbleZIndex = 2f

// Size
private val TextFieldMinWidth = 100.dp

private fun getTargetLabelPosition(isFocused: Boolean, text: String) =
 if (!isFocused && text.isNotEmpty() || isFocused) {
     LabelPosition.TopLeft
 } else {
     LabelPosition.Center
 }

@Composable
fun MyOutlinedTextField(
    modifier: Modifier = Modifier,
    textFieldState: TextFieldState,
    onValueChange: (String) -> Unit,
    label: String,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    isError: Boolean,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardAction: ((KeyboardActionScope) -> Unit),
    trailingIcon: @Composable (() -> Unit)? = null,
) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.Center
    ) {
        var textFieldIsFocused by remember { mutableStateOf(false) }
        var labelPosition by remember {
            mutableStateOf(
                getTargetLabelPosition(
                    isFocused = textFieldIsFocused,
                    text = textFieldState.text
                )
            )
        }

        val labelPositionTransition = updateTransition(
            targetState = labelPosition,
            label = LabelPositionTransitionLabel
        )

        val labelOffsetAnimation by labelPositionTransition.animateOffset(
            transitionSpec = {
                tween(
                    durationMillis = LabelOffsetAnimationDuration
                )
            },
            label = LabelOffsetAnimationLabel
        ) { position ->
            when (position) {
                LabelPosition.TopLeft -> Offset(
                    LabelAnimationTopLeftPositionOffsetX,
                    LabelAnimationTopLeftPositionOffsetY
                )
                LabelPosition.Center -> Offset(
                    LabelAnimationCenterPositionOffsetX,
                    LabelAnimationCenterPositionOffsetY
                )
            }
        }

        val labelTextSizeAnimation by labelPositionTransition.animateFloat(
            label = LabelTextSizeAnimationLabel
        ) { position ->
            when (position) {
                LabelPosition.TopLeft -> LabelTextSizeAnimationTopLeftPosition
                LabelPosition.Center -> LabelTextSizeAnimationCenterPosition
            }
        }

        val labelBackgroundColorAnimation by labelPositionTransition.animateColor(
            transitionSpec = {
                tween(
                    durationMillis = LabelBackgroundColorAnimationDuration,
                    delayMillis = LabelBackgroundColorAnimationDelay,
                )
            },
            label = LabelBackgroundColorAnimationLabel
        ) { position ->
            when (position) {
                LabelPosition.TopLeft -> Color.White
                LabelPosition.Center -> Color.Transparent
            }
        }

        val textFieldIsFocusedTransition = updateTransition(
            targetState = textFieldIsFocused,
            label = TextFieldIsFocusedTransitionLabel
        )

        val textFieldBackgroundColorAnimation by textFieldIsFocusedTransition.animateColor(
            transitionSpec = { ->
                tween(
                    durationMillis = TextFieldBackgroundColorAnimationDuration,
                    delayMillis = TextFieldBackgroundColorAnimationDelay,
                )
            },
            label = TextFieldBackgroundColorAnimationLabel
        ) { _isFocused ->
            when {
                _isFocused -> LinkWater
                !_isFocused && textFieldState.text.isEmpty() -> Color.Transparent
                !_isFocused && textFieldState.text.isNotEmpty() -> Alabaster
                else -> Color.Transparent
            }
        }

        val borderColorAnimation by textFieldIsFocusedTransition.animateColor(
            transitionSpec = { ->
                tween(
                    durationMillis = BorderColorAnimationDuration,
                    delayMillis = BorderColorAnimationDelay,
                )
            },
            label = BorderColorAnimationLabel
        ) { _isFocused ->
            when {
                _isFocused -> Color.Transparent
                !_isFocused && textFieldState.text.isEmpty() -> Mercury
                !_isFocused && textFieldState.text.isNotEmpty() -> Color.Transparent
                else -> Mercury
            }
        }

        val textFieldBoxModifier = Modifier.textFieldBoxModifier(
            textFieldState = textFieldState,
            textFieldBackgroundColorAnimation = textFieldBackgroundColorAnimation,
            borderColorAnimation = borderColorAnimation,
            textFieldIsFocused = textFieldIsFocused
        )

        Box(
            contentAlignment = Alignment.CenterStart
        ) {
            Text(
                modifier = Modifier
                    .zIndex(LabelBubbleZIndex)
                    .defaultMinSize(1.dp)
                    .offset(labelOffsetAnimation.x.dp, labelOffsetAnimation.y.dp)
                    .clip(
                        shape = CircleShape
                    )
                    .background(
                        color = labelBackgroundColorAnimation
                    )
                    // Padding inside the Email bubble
                    .padding(
                        start = if (labelPosition == LabelPosition.TopLeft) 8.dp else 0.dp,
                        end = if (labelPosition == LabelPosition.TopLeft) 8.dp else 0.dp,
                        top = 2.dp,
                        bottom = 2.dp
                    ),
                text = label,
                fontSize = dpToSp(
                    labelTextSizeAnimation,
                    LocalContext.current
                ).sp,
                style = LocalTextStyle.current.copy(
                    color = OsloGray
                ),
            )

            Box(
                modifier = textFieldBoxModifier
            ) {
                TextField(
                    modifier = Modifier
                        .padding(
                            start = 8.dp,
                            end = 8.dp
                        )
                        .onFocusChanged {
                            textFieldIsFocused = it.isFocused
                            labelPosition =
                                getTargetLabelPosition(textFieldIsFocused, textFieldState.text)
                            textFieldState.enableDisplayErrors(textFieldIsFocused)
                        }
                        .defaultMinSize(
                            minWidth = 1.dp
                        ),
                    value = textFieldState.text,
                    onValueChange = onValueChange,
                    enabled = enabled,
                    keyboardOptions = keyboardOptions,
                    readOnly = readOnly,
                    isError = isError,
                    keyboardActions = KeyboardActions(
                        keyboardAction
                    ),
                    trailingIcon = trailingIcon,
                    singleLine = true,
                    textStyle = TextFieldStyle,
                    colors = TextFieldDefaults.textFieldColors(
                        textColor = Fiord,
                        cursorColor = Fiord,
                        disabledTextColor = Color.Transparent,
                        backgroundColor = Color.Transparent,
                        focusedIndicatorColor = Color.Transparent,
                        unfocusedIndicatorColor = Color.Transparent,
                        disabledIndicatorColor = Color.Transparent,
                        errorIndicatorColor = Color.Transparent
                    )
                )
            }
        }
        textFieldState.getError()?.let { error ->
            TextFieldError(
                textError = error
            )
        }
    }
}

private fun Modifier.textFieldBoxModifier(
    textFieldState: TextFieldState,
    textFieldBackgroundColorAnimation: Color,
    borderColorAnimation: Color,
    textFieldIsFocused: Boolean
): Modifier {
    var basicTextFieldBoxModifier = this
        .padding(
            start = 4.dp,
            end = 4.dp,
            top = 8.dp,
            bottom = 8.dp
        )
        .height(60.dp)
        .background(
            color = textFieldBackgroundColorAnimation,
            shape = CircleShape
        )
        .border(1.dp, borderColorAnimation, CircleShape)
        .zIndex(1f)
        .wrapContentSize(
            align = Alignment.CenterStart
        )

    when {
        textFieldIsFocused -> {
            basicTextFieldBoxModifier = basicTextFieldBoxModifier.then(Modifier.fillMaxWidth())
        }
        !textFieldIsFocused && textFieldState.text.isEmpty() -> {
            basicTextFieldBoxModifier =
                basicTextFieldBoxModifier.then(Modifier.width(TextFieldMinWidth))
        }
    }

    return basicTextFieldBoxModifier
}

Upvotes: 1

Related Questions