Now in REALITY Tech #89 ScrollableTabRowの横幅の最小値が設定できなくて、ちょっと困った話
こんにちは!REALITYのAndroidエンジニアのhosicoです!
去年のちょうど10月にREALITYにJOINしまして、ちょうど1年ぐらいが経ちました。月日が過ぎるのは早いですね。楽しく、日々過ごせております。
最近、インフルエンザが流行っており、ビクビクしている今日この頃です。
今回は、Jetpack Composeでタブベースのナビゲーションを実装するためのUIコンポーネントの一つである「ScrollableTabRow」を使う際に、少し困ったことがあったので、共有させていただこうかなと思います。
ScrollableTabRowについて
「ScrollableTabRow」は、タブの数が画面内に収まらないようなケースにて、横にスクロール可能なタブのリストを表示するためのUIコンポーネントとして、使用されます。REALITYのAndroidアプリの中でも、実際にいくつかの画面にて、利用されております。
ScrollableTabRowを使って、困ったこと
配信中の配信者の方に対して、視聴者側から送る応援ギフトを選択する画面があるのですが、該当の画面がxmlベースで書かれており、Composeへの移行を進めていました。
そして、移行を進めていく中で、上の画像の赤枠で囲ったタブリストのUIを、「ScrollableTabRow」で置き換えたところ…
上の画像のように、タブ同士のPaddingが不均一に見えてしまうようになりました。
androidx.compose.materialのTabRow.ktにて、ScrollableTabRowのComposable関数が定義されているので、読んでみると、
@Composable
@UiComposable
fun ScrollableTabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
indicator: @Composable @UiComposable
(tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
divider: @Composable @UiComposable () -> Unit =
@Composable {
TabRowDefaults.Divider()
},
tabs: @Composable @UiComposable () -> Unit
) {
Surface(
modifier = modifier,
color = backgroundColor,
contentColor = contentColor
) {
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
val scrollableTabData = remember(scrollState, coroutineScope) {
ScrollableTabData(
scrollState = scrollState,
coroutineScope = coroutineScope
)
}
SubcomposeLayout(
Modifier.fillMaxWidth()
.wrapContentSize(align = Alignment.CenterStart)
.horizontalScroll(scrollState)
.selectableGroup()
.clipToBounds()
) { constraints ->
val minTabWidth = ScrollableTabRowMinimumTabWidth.roundToPx()
val padding = edgePadding.roundToPx()
val tabConstraints = constraints.copy(minWidth = minTabWidth)
val tabPlaceables = subcompose(TabSlots.Tabs, tabs)
.map { it.measure(tabConstraints) }
var layoutWidth = padding * 2
var layoutHeight = 0
tabPlaceables.forEach {
layoutWidth += it.width
layoutHeight = maxOf(layoutHeight, it.height)
}
// Position the children.
layout(layoutWidth, layoutHeight) {
// Place the tabs
val tabPositions = mutableListOf<TabPosition>()
var left = padding
tabPlaceables.forEach {
it.placeRelative(left, 0)
tabPositions.add(TabPosition(left = left.toDp(), width = it.width.toDp()))
left += it.width
}
// The divider is measured with its own height, and width equal to the total width
// of the tab row, and then placed on top of the tabs.
subcompose(TabSlots.Divider, divider).forEach {
val placeable = it.measure(
constraints.copy(
minHeight = 0,
minWidth = layoutWidth,
maxWidth = layoutWidth
)
)
placeable.placeRelative(0, layoutHeight - placeable.height)
}
// The indicator container is measured to fill the entire space occupied by the tab
// row, and then placed on top of the divider.
subcompose(TabSlots.Indicator) {
indicator(tabPositions)
}.forEach {
it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0)
}
scrollableTabData.onLaidOut(
density = this@SubcomposeLayout,
edgeOffset = padding,
tabPositions = tabPositions,
selectedTab = selectedTabIndex
)
}
}
}
}
// ...省略
private val ScrollableTabRowMinimumTabWidth = 90.dp
どうやら、ScrollableTabRowMinimumTabWidthという変数の中に、各タブのwidthの最小値が90.dpと設定され、各タブが配置されているようでした。
そのため、タブの中のテキスト幅が90.dpに満たなければ、足りない分の余白が生まれてしまい、タブ同士のpaddingが、見た目上、均一ではないように見えてしまっていました。
これを解決するには、各タブのwidthの最小値を0.dpにしてあげれば良いのですが、どうやら、ScrollableTabRowのインターフェイスには、widthの最小値を設定する術が用意されておらず、インターネット上でも、同じような問題で困っているような例もいくつか見つかりました。
結局どうしたか?
賢い解決方法が思いつかず、androidx.compose.materialのTabRow.ktをREALITYのコードの中にfolkしてきました。取り込んだTabRow.ktの中で、横幅の最小値を定義している「ScrollableTabRowMinimumTabWidth」の値を0.dpに置き換え、取り込んだ「ScrollableTabRow」のComposableをリネームして、それを最小幅が0.dpの「ScrollableTabRow」として、REALITY内で使うようにして、一旦回避してます。
タブの横幅の最小値を0.dpにしたことで、タブ同士のpaddingが均一になり、一画面中に多くのタブが収まるようになりました。
まとめ
今回、自分がcompose移行を進めていく中で、「ScrollableTabRow」に置き換える際に、困ったことを共有させていただきました。もっと良いスマートな解決方法があれば、教えてください。似たような内容の記事は他にもあるのですが、もし同じようなことで困っている方がいらっしゃいましたら、参考になれば幸いです。