OutlinedTextFieldと同様にmaterial3 1.4.0のドロップダウンメニューボックスExposedDropdownMenuBoxも枠線とテキスト入力域の間に入っているPaddingが広めで調整することができません。
調整可能なExposedDropdownMenuBoxコンポーネントCustomExposedDropdownMenuBox.ktをcomponentsディレクトリ(パッケージ)の直下に作成します。
package jp.co.progress_llc.portal.core.ui.components
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import jp.co.progress_llc.portal.core.ui.theme.AppTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomExposedDropdownMenuBox(
modifier: Modifier = Modifier,
label: String,
options: List<Pair<String, List<String>>>,
selectedOption: String?,
onOptionSelected: (List<String>) -> Unit,
showAdditionalItem: Boolean = false,
additionalItemText: String = "新規登録",
onAdditionalItemClick: (() -> Unit)? = null
) {
var expanded by remember { mutableStateOf(false) }
val hasSelection = selectedOption?.isNotEmpty() == true
var boxWidth by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
// labelアニメーション制御
val transition = updateTransition(
targetState = expanded || hasSelection,
label = "labelTransition"
)
val labelScale by transition.animateFloat(label = "labelScale") { if (it) 0.85f else 1f }
val labelOffsetY by transition.animateDp(label = "labelOffsetY") { if (it) (-12).dp else 2.dp }
// ドロップダウン開閉アニメーション
val dropdownTransition = updateTransition(targetState = expanded, label = "dropdownTransition")
val animatedAlpha by dropdownTransition.animateFloat(label = "alpha") { if (it) 1f else 0f }
val animatedScale by dropdownTransition.animateFloat(label = "scale") { if (it) 1f else 0.95f }
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = modifier
.onGloballyPositioned { coordinates ->
boxWidth = with(density) { coordinates.size.width.toDp() - 8.dp }
}
) {
// 外枠+ラベル+選択表示
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface.copy(alpha = 1f))
.border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(8.dp))
.padding(horizontal = 12.dp, vertical = 4.dp)
.clickable { expanded = !expanded }
) {
// 選択済テキスト
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.padding(top = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = selectedOption ?: "",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
}
}
// ラベル(OutlinedTextField風アニメーション)
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = if (expanded) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.offset(x = 10.dp, y = labelOffsetY)
.background(MaterialTheme.colorScheme.surface)
.scale(labelScale)
.padding(horizontal = 2.dp)
)
// ドロップダウンメニュー(フェード+拡縮アニメーション)
Box(
modifier = Modifier
.fillMaxWidth()
.graphicsLayer {
alpha = animatedAlpha
scaleY = animatedScale
}
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
offset = DpOffset(x = 4.dp, y = 12.dp),
modifier = Modifier
.width(boxWidth)
.background(MaterialTheme.colorScheme.surface)
) {
options.forEach { (text, values) ->
DropdownMenuItem(
text = { Text(text) },
onClick = {
onOptionSelected(values)
expanded = false
}
)
}
if (showAdditionalItem) {
if (options.isNotEmpty()) { HorizontalDivider() }
DropdownMenuItem(
text = { Text(additionalItemText) },
onClick = {
onAdditionalItemClick?.invoke()
expanded = false
}
)
}
}
}
}
}
@Preview(showBackground = true)
@Composable
fun CustomExposedDropdownMenuBoxPreview() {
AppTheme {
var selected by remember { mutableStateOf<String?>(null) }
CustomExposedDropdownMenuBox(
label = "選択してください",
options = emptyList(),
selectedOption = selected,
onOptionSelected = { },
)
}
}