侧边栏壁纸
  • 累计撰写 100 篇文章
  • 累计创建 46 个标签
  • 累计收到 5 条评论

目 录CONTENT

文章目录

Compose Multiplatform 自定义 TextField 解决 contentPadding 过大和 leadingIcon、trailingIcon 边距过大

勤为径苦作舟
2025-05-04 / 0 评论 / 0 点赞 / 1 阅读 / 0 字

简介

学习 Compose Multiplatform UI Framework | JetBrains 过程中使用到 TextField,觉得默认内边距太大了,又没法修改,所以自定义MyTextField

原效果:

自定义效果:

步骤

尝试调用androidx.compose.material.TextField

@Composable
@Preview
fun App() {
  MaterialTheme {
    Column(
      modifier = Modifier.padding(8.dp, 5.dp)
    ) {
      TextField(
        value = searchText.value,
        onValueChange = { searchText.value = it },
        leadingIcon = {
          Icon(
            Icons.Filled.Search,
            contentDescription = null
          )
        },
        singleLine = true
      )
    }
  }
}

新建MyTextField,复制androidx.compose.material.TextField源码。

  • .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))这行代码中都是internal的,所以替换为.semantics { if (isError) error("Invalid input") },暂时不考虑国际化。
  • 增加contentPadding用于修改左右内边距,默认为 0。
  • minHeight = TextFieldDefaults.MinHeight替换为minHeight = 0.dp,修改上下内边距为 0。
@Composable
fun MyTextField(
  value: String,
  onValueChange: (String) -> Unit,
  modifier: Modifier = Modifier,
  enabled: Boolean = true,
  readOnly: Boolean = false,
  textStyle: TextStyle = LocalTextStyle.current,
  label: @Composable (() -> Unit)? = null,
  placeholder: @Composable (() -> Unit)? = null,
  leadingIcon: @Composable (() -> Unit)? = null,
  trailingIcon: @Composable (() -> Unit)? = null,
  isError: Boolean = false,
  visualTransformation: VisualTransformation = VisualTransformation.None,
  keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
  keyboardActions: KeyboardActions = KeyboardActions(),
  singleLine: Boolean = false,
  maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
  minLines: Int = 1,
  interactionSource: MutableInteractionSource? = null,
  shape: Shape = TextFieldDefaults.TextFieldShape,
  colors: TextFieldColors = TextFieldDefaults.textFieldColors(),
  // 修改左右内边距
  contentPadding: PaddingValues = PaddingValues(0.dp)
) {
  @Suppress("NAME_SHADOWING")
  val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
  // If color is not provided via the text style, use content color as a default
  val textColor = textStyle.color.takeOrElse {
    colors.textColor(enabled).value
  }
  val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))

  @OptIn(ExperimentalMaterialApi::class)
  BasicTextField(
    value = value,
    modifier = modifier
      .indicatorLine(enabled, isError, interactionSource, colors)
      // .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
      .semantics { if (isError) error("Invalid input") }
      .defaultMinSize(
        minWidth = TextFieldDefaults.MinWidth,
        // minHeight = TextFieldDefaults.MinHeight
        // 修改上下内边距
        minHeight = 0.dp
      ),
    onValueChange = onValueChange,
    enabled = enabled,
    readOnly = readOnly,
    textStyle = mergedTextStyle,
    cursorBrush = SolidColor(colors.cursorColor(isError).value),
    visualTransformation = visualTransformation,
    keyboardOptions = keyboardOptions,
    keyboardActions = keyboardActions,
    interactionSource = interactionSource,
    singleLine = singleLine,
    maxLines = maxLines,
    minLines = minLines,
    decorationBox = @Composable { innerTextField ->
      // places leading icon, text field with label and placeholder, trailing icon
      TextFieldDefaults.TextFieldDecorationBox(
        value = value,
        visualTransformation = visualTransformation,
        innerTextField = innerTextField,
        placeholder = placeholder,
        label = label,
        leadingIcon = leadingIcon,
        trailingIcon = trailingIcon,
        singleLine = singleLine,
        enabled = enabled,
        isError = isError,
        interactionSource = interactionSource,
        shape = shape,
        colors = colors,
        // 修改左右内边距
        contentPadding = contentPadding,
      )
    }
  )
}

修改 TextField 调用为 MyTextField:

MyTextField(
  value = searchText.value,
  onValueChange = { searchText.value = it },
  // leadingIcon = {
  //   Icon(
  //     Icons.Filled.Search,
  //     contentDescription = null
  //   )
  // },
  singleLine = true
)

但把leadingIcon取消注释之后,发现图标的内边距会把输入框也撑大,所以也要自定义。

  • 有 leadingIcon 时,文本左内边距+=图标大小+图标边距,trailingIcon 同理。
  • 把 BasicTextField 和自定义的图标 Box 放在另一个 Box 里。
  • BasicTextField 撑满父级高度。
  • BasicTextField 父级 Box 默认高度 24.dp。

最终代码:

import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.TextFieldColors
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

@Composable
fun MyTextField(
  value: String,
  onValueChange: (String) -> Unit,
  modifier: Modifier = Modifier,
  enabled: Boolean = true,
  readOnly: Boolean = false,
  textStyle: TextStyle = LocalTextStyle.current,
  label: @Composable (() -> Unit)? = null,
  placeholder: @Composable (() -> Unit)? = null,
  leadingIcon: @Composable (() -> Unit)? = null,
  trailingIcon: @Composable (() -> Unit)? = null,
  isError: Boolean = false,
  visualTransformation: VisualTransformation = VisualTransformation.None,
  keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
  keyboardActions: KeyboardActions = KeyboardActions(),
  singleLine: Boolean = false,
  maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
  minLines: Int = 1,
  interactionSource: MutableInteractionSource? = null,
  shape: Shape = TextFieldDefaults.TextFieldShape,
  colors: TextFieldColors = TextFieldDefaults.textFieldColors(),
  // 修改左右内边距
  contentPadding: PaddingValues = PaddingValues(0.dp),
  // leadingIcon 边距
  leadingIconPadding: PaddingValues = PaddingValues(start = 0.dp),
  // trailingIcon 边距
  trailingIconPadding: PaddingValues = PaddingValues(end = 0.dp),
  // 图标大小
  iconSize: Dp = 24.dp
) {
  @Suppress("NAME_SHADOWING")
  val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
  // If color is not provided via the text style, use content color as a default
  val textColor = textStyle.color.takeOrElse {
    colors.textColor(enabled).value
  }
  val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))

  val hasLeadingIcon = leadingIcon != null
  val hasTrailingIcon = trailingIcon != null
  // 有左图标时,文本左内边距+=图标大小+图标边距
  val textPaddingStart = if (hasLeadingIcon) {
    contentPadding.calculateStartPadding(LocalLayoutDirection.current) + iconSize + leadingIconPadding.calculateStartPadding(LocalLayoutDirection.current)
  } else {
    contentPadding.calculateStartPadding(LocalLayoutDirection.current)
  }
  // 有右图标时,文本右内边距+=图标大小+图标边距
  val textPaddingEnd = if (hasTrailingIcon) {
    contentPadding.calculateEndPadding(LocalLayoutDirection.current) + iconSize + trailingIconPadding.calculateEndPadding(LocalLayoutDirection.current)
  } else {
    contentPadding.calculateEndPadding(LocalLayoutDirection.current)
  }

  Box(modifier = modifier.height(24.dp)) {
    @OptIn(ExperimentalMaterialApi::class)
    BasicTextField(
      value = value,
      modifier = Modifier
        .indicatorLine(enabled, isError, interactionSource, colors)
        // .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
        .semantics { if (isError) error("Invalid input") }
        .defaultMinSize(
          minWidth = TextFieldDefaults.MinWidth,
          // minHeight = TextFieldDefaults.MinHeight
        ),
      onValueChange = onValueChange,
      enabled = enabled,
      readOnly = readOnly,
      textStyle = mergedTextStyle,
      cursorBrush = SolidColor(colors.cursorColor(isError).value),
      visualTransformation = visualTransformation,
      keyboardOptions = keyboardOptions,
      keyboardActions = keyboardActions,
      interactionSource = interactionSource,
      singleLine = singleLine,
      maxLines = maxLines,
      minLines = minLines,
      decorationBox = @Composable { innerTextField ->
        // places leading icon, text field with label and placeholder, trailing icon
        TextFieldDefaults.TextFieldDecorationBox(
          value = value,
          visualTransformation = visualTransformation,
          innerTextField = innerTextField,
          placeholder = placeholder,
          label = label,
          // 不使用内置的 leadingIcon
          leadingIcon = null,
          // 不使用内置的 trailingIcon
          trailingIcon = null,
          singleLine = singleLine,
          enabled = enabled,
          isError = isError,
          interactionSource = interactionSource,
          shape = shape,
          colors = colors,
          // 修改内边距
          contentPadding = PaddingValues(
            start = textPaddingStart,
            end = textPaddingEnd,
            top = contentPadding.calculateTopPadding(),
            bottom = contentPadding.calculateBottomPadding()
          )
        )
      }
    )

    // 放置 leadingIcon
    if (hasLeadingIcon) {
      Box(
        modifier = Modifier
          .padding(leadingIconPadding)
          .size(iconSize)
          .align(Alignment.CenterStart)
      ) {
        leadingIcon!!()
      }
    }
    // 放置 trailingIcon
    if (hasTrailingIcon) {
      Box(
        modifier = Modifier
          .padding(trailingIconPadding)
          .size(iconSize)
          .align(Alignment.CenterEnd)
      ) {
        trailingIcon!!()
      }
    }
  }
}

@Composable
fun MyTextField(
  value: TextFieldValue,
  onValueChange: (TextFieldValue) -> Unit,
  modifier: Modifier = Modifier,
  enabled: Boolean = true,
  readOnly: Boolean = false,
  textStyle: TextStyle = LocalTextStyle.current,
  label: @Composable (() -> Unit)? = null,
  placeholder: @Composable (() -> Unit)? = null,
  leadingIcon: @Composable (() -> Unit)? = null,
  trailingIcon: @Composable (() -> Unit)? = null,
  isError: Boolean = false,
  visualTransformation: VisualTransformation = VisualTransformation.None,
  keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
  keyboardActions: KeyboardActions = KeyboardActions(),
  singleLine: Boolean = false,
  maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
  minLines: Int = 1,
  interactionSource: MutableInteractionSource? = null,
  shape: Shape = TextFieldDefaults.TextFieldShape,
  colors: TextFieldColors = TextFieldDefaults.textFieldColors(),
  // 修改左右内边距
  contentPadding: PaddingValues = PaddingValues(0.dp),
  // leadingIcon 边距
  leadingIconPadding: PaddingValues = PaddingValues(start = 0.dp),
  // trailingIcon 边距
  trailingIconPadding: PaddingValues = PaddingValues(end = 0.dp),
  // 图标大小
  iconSize: Dp = 24.dp
) {
  @Suppress("NAME_SHADOWING")
  val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
  // If color is not provided via the text style, use content color as a default
  val textColor = textStyle.color.takeOrElse {
    colors.textColor(enabled).value
  }
  val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))

  val hasLeadingIcon = leadingIcon != null
  val hasTrailingIcon = trailingIcon != null
  // 有左图标时,文本左内边距+=图标大小+图标边距
  val textPaddingStart = if (hasLeadingIcon) {
    contentPadding.calculateStartPadding(LocalLayoutDirection.current) + iconSize + leadingIconPadding.calculateStartPadding(LocalLayoutDirection.current)
  } else {
    contentPadding.calculateStartPadding(LocalLayoutDirection.current)
  }
  // 有右图标时,文本右内边距+=图标大小+图标边距
  val textPaddingEnd = if (hasTrailingIcon) {
    contentPadding.calculateEndPadding(LocalLayoutDirection.current) + iconSize + trailingIconPadding.calculateEndPadding(LocalLayoutDirection.current)
  } else {
    contentPadding.calculateEndPadding(LocalLayoutDirection.current)
  }

  Box(modifier = modifier.height(24.dp)) {
    @OptIn(ExperimentalMaterialApi::class)
    BasicTextField(
      value = value,
      modifier = Modifier
        .indicatorLine(enabled, isError, interactionSource, colors)
        // .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
        .semantics { if (isError) error("Invalid input") }
        .defaultMinSize(
          minWidth = TextFieldDefaults.MinWidth,
          // minHeight = TextFieldDefaults.MinHeight
        ),
      onValueChange = onValueChange,
      enabled = enabled,
      readOnly = readOnly,
      textStyle = mergedTextStyle,
      cursorBrush = SolidColor(colors.cursorColor(isError).value),
      visualTransformation = visualTransformation,
      keyboardOptions = keyboardOptions,
      keyboardActions = keyboardActions,
      interactionSource = interactionSource,
      singleLine = singleLine,
      maxLines = maxLines,
      minLines = minLines,
      decorationBox = @Composable { innerTextField ->
        // places leading icon, text field with label and placeholder, trailing icon
        TextFieldDefaults.TextFieldDecorationBox(
          value = value.text,
          visualTransformation = visualTransformation,
          innerTextField = innerTextField,
          placeholder = placeholder,
          label = label,
          // 不使用内置的 leadingIcon
          leadingIcon = null,
          // 不使用内置的 trailingIcon
          trailingIcon = null,
          singleLine = singleLine,
          enabled = enabled,
          isError = isError,
          interactionSource = interactionSource,
          shape = shape,
          colors = colors,
          // 修改内边距
          contentPadding = PaddingValues(
            start = textPaddingStart,
            end = textPaddingEnd,
            top = contentPadding.calculateTopPadding(),
            bottom = contentPadding.calculateBottomPadding()
          )
        )
      }
    )

    // 放置 leadingIcon
    if (hasLeadingIcon) {
      Box(
        modifier = Modifier
          .padding(leadingIconPadding)
          .size(iconSize)
          .align(Alignment.CenterStart)
      ) {
        leadingIcon!!()
      }
    }
    // 放置 trailingIcon
    if (hasTrailingIcon) {
      Box(
        modifier = Modifier
          .padding(trailingIconPadding)
          .size(iconSize)
          .align(Alignment.CenterEnd)
      ) {
        trailingIcon!!()
      }
    }
  }
}

参考

0

评论区