androidx.compose:compose-bom:2025.10.01に入っているandroidx.compose.material3:material3はバージョン1.4.0になります。
material3 1.4.0のテキストボックスOutlinedTextFieldは、枠線とテキスト入力域の間に入っているPaddingが広めで調整することができません。
調整可能なOutlinedTextFieldコンポーネントCustomOutlinedTextField.ktをcomponentsディレクトリ(パッケージ)の直下に作成します。
package jp.co.progress_llc.portal.core.ui.components
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
@Composable
fun CustomOutlinedTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
label: (@Composable (() -> Unit))? = null,
placeholder: String? = null,
textStyle: TextStyle = MaterialTheme.typography.bodySmall,
singleLine: Boolean = true,
shape: Shape = MaterialTheme.shapes.small,
contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
height: Dp = 48.dp,
readOnly: Boolean = false,
onClick: (() -> Unit)? = null
) {
var isFocused by remember { mutableStateOf(false) }
// MaterialThemeを@Composable内で安全に参照
val colorScheme = MaterialTheme.colorScheme
// focus状態に応じて枠線色を変更
val borderColor = if (isFocused) colorScheme.primary else colorScheme.outline
// labelの移動・縮小アニメーション
val labelScale by animateFloatAsState(targetValue = if (value.isNotEmpty() || isFocused) 0.75f else 1f)
val labelOffsetY by animateDpAsState(targetValue = if (value.isNotEmpty() || isFocused) (-8).dp else 2.dp)
// プレースホルダーの表示条件
val isPlaceholderVisible = value.isEmpty() && !isFocused && placeholder != null
Spacer(modifier = Modifier.height(4.dp))
Box(modifier = modifier) {
Box(
modifier = Modifier
.height(height)
.border(1.dp, borderColor, shape)
.padding(contentPadding),
contentAlignment = Alignment.CenterStart
) {
// プレースホルダー
if (isPlaceholderVisible) {
androidx.compose.material3.Text(
text = placeholder,
style = textStyle.copy(
color = colorScheme.onSurface.copy(alpha = 0.6f),
fontSize = 14.sp
),
modifier = Modifier.fillMaxWidth()
)
}
// 入力フィールド
BasicTextField(
value = value,
onValueChange = onValueChange,
readOnly = readOnly,
singleLine = singleLine,
textStyle = textStyle.copy(color = colorScheme.onSurface, fontSize = 14.sp),
cursorBrush = SolidColor(colorScheme.primary),
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { focusState ->
val newFocus = focusState.isFocused
if (isFocused != newFocus) isFocused = newFocus
}
)
// readOnlyかつonClickが指定されている場合、透明なBoxを重ねてクリック可能にする
if (readOnly && onClick != null) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable { onClick() }
)
}
}
// ラベルが指定されていて、プレースホルダーが表示されていなければラベルがを描画
if (label != null && !isPlaceholderVisible) {
Box(
modifier = Modifier
.offset(x = 10.dp, y = labelOffsetY)
.background(color = MaterialTheme.colorScheme.background)
.scale(labelScale)
) {
label()
}
}
}
}
@Preview(showBackground = true)
@Composable
fun CustomOutlinedTextFieldPreview() {
var text by remember { mutableStateOf("") }
CustomOutlinedTextField(
value = text,
onValueChange = { },
placeholder = "プレースホルダー"
)
}