Skip to content

Commit 45c1e2a

Browse files
committed
library: Add TabRowWithContour Component
1 parent 279556e commit 45c1e2a

File tree

2 files changed

+203
-34
lines changed

2 files changed

+203
-34
lines changed

composeApp/src/commonMain/kotlin/component/OtherComponent.kt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import top.yukonga.miuix.kmp.basic.Slider
4141
import top.yukonga.miuix.kmp.basic.SliderDefaults
4242
import top.yukonga.miuix.kmp.basic.SmallTitle
4343
import top.yukonga.miuix.kmp.basic.TabRow
44+
import top.yukonga.miuix.kmp.basic.TabRowWithContour
4445
import top.yukonga.miuix.kmp.basic.Text
4546
import top.yukonga.miuix.kmp.basic.TextButton
4647
import top.yukonga.miuix.kmp.basic.TextField
@@ -111,6 +112,7 @@ fun OtherComponent(padding: PaddingValues) {
111112
val progressValues = remember { listOf(0.0f, 0.25f, 0.5f, 0.75f, 1.0f, null) }
112113
val progressDisable by remember { mutableStateOf(0.5f) }
113114
val tabTexts = listOf("tab1", "tab2", "tab3", "tab4", "tab5", "tab6")
115+
var selectedTabIndex by remember { mutableStateOf(0) }
114116
var selectedTabIndex1 by remember { mutableStateOf(0) }
115117
val miuixIconsNormal = listOf(
116118
MiuixIcons.Useful.AddSecret,
@@ -310,12 +312,30 @@ fun OtherComponent(padding: PaddingValues) {
310312
SmallTitle(text = "TabRow")
311313
TabRow(
312314
tabs = tabTexts,
313-
selectedTabIndex = selectedTabIndex1,
315+
selectedTabIndex = selectedTabIndex,
314316
modifier = Modifier
315317
.padding(horizontal = 12.dp)
316-
.padding(bottom = 6.dp)
318+
.padding(bottom = 12.dp)
317319
) {
318-
selectedTabIndex1 = it
320+
selectedTabIndex = it
321+
}
322+
Card(
323+
modifier = Modifier
324+
.fillMaxWidth()
325+
.padding(horizontal = 12.dp)
326+
.padding(bottom = 6.dp),
327+
insideMargin = PaddingValues(16.dp)
328+
) {
329+
TabRowWithContour(
330+
tabs = tabTexts,
331+
selectedTabIndex = selectedTabIndex1,
332+
) {
333+
selectedTabIndex1 = it
334+
}
335+
Text(
336+
text = "Selected Tab: ${tabTexts[selectedTabIndex1]}",
337+
modifier = Modifier.padding(top = 12.dp)
338+
)
319339
}
320340

321341
SmallTitle(text = "Icon")
Lines changed: 180 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
package top.yukonga.miuix.kmp.basic
22

3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.clickable
35
import androidx.compose.foundation.layout.Arrangement
46
import androidx.compose.foundation.layout.Box
57
import androidx.compose.foundation.layout.fillMaxHeight
68
import androidx.compose.foundation.layout.fillMaxSize
79
import androidx.compose.foundation.layout.fillMaxWidth
810
import androidx.compose.foundation.layout.height
11+
import androidx.compose.foundation.layout.padding
912
import androidx.compose.foundation.layout.width
1013
import androidx.compose.foundation.lazy.LazyRow
1114
import androidx.compose.foundation.lazy.itemsIndexed
1215
import androidx.compose.foundation.lazy.rememberLazyListState
1316
import androidx.compose.runtime.Composable
17+
import androidx.compose.runtime.Immutable
18+
import androidx.compose.runtime.Stable
1419
import androidx.compose.runtime.derivedStateOf
1520
import androidx.compose.runtime.remember
1621
import androidx.compose.ui.Alignment
1722
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.draw.clip
1824
import androidx.compose.ui.graphics.Color
1925
import androidx.compose.ui.platform.LocalDensity
2026
import androidx.compose.ui.semantics.Role
2127
import androidx.compose.ui.semantics.role
2228
import androidx.compose.ui.semantics.semantics
2329
import androidx.compose.ui.text.font.FontWeight
30+
import androidx.compose.ui.text.style.TextOverflow
2431
import androidx.compose.ui.unit.Dp
2532
import androidx.compose.ui.unit.dp
2633
import androidx.compose.ui.unit.times
@@ -35,54 +42,44 @@ import top.yukonga.miuix.kmp.utils.overScrollHorizontal
3542
* @param tabs The text to be displayed in the [TabRow].
3643
* @param selectedTabIndex The selected tab index of the [TabRow]
3744
* @param modifier The modifier to be applied to the [TabRow].
38-
* @param backgroundColor The background color of the tab in [TabRow].
39-
* @param contentColor The text color of the tab in [TabRow].
40-
* @param selectedBackgroundColor The background color of the selected tab in [TabRow].
41-
* @param selectedColor The text color of the selected tab in [TabRow].
45+
* @param colors The colors of the [TabRow].
46+
* @param minWidth The minimum width of the tab in [TabRow].
47+
* @param height The height of the [TabRow].
4248
* @param cornerRadius The round corner radius of the tab in [TabRow].
43-
* @param onSelect The callback when the tab of the [TabRow] is clicked.
49+
* @param onTabSelected The callback when a tab is selected.
4450
*/
4551
@Composable
4652
fun TabRow(
4753
tabs: List<String>,
4854
selectedTabIndex: Int,
4955
modifier: Modifier = Modifier,
50-
backgroundColor: Color = MiuixTheme.colorScheme.background,
51-
contentColor: Color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
52-
selectedBackgroundColor: Color = MiuixTheme.colorScheme.surface,
53-
selectedColor: Color = MiuixTheme.colorScheme.onSurface,
54-
cornerRadius: Dp = 8.dp,
55-
onSelect: ((Int) -> Unit)? = null,
56+
colors: TabRowColors = TabRowDefaults.tabRowColors(),
57+
minWidth: Dp = TabRowDefaults.TabRowMinWidth,
58+
height: Dp = TabRowDefaults.TabRowHeight,
59+
cornerRadius: Dp = TabRowDefaults.TabRowCornerRadius,
60+
onTabSelected: ((Int) -> Unit)? = null,
5661
) {
57-
val listState = rememberLazyListState()
58-
val windowSize = getWindowSize()
59-
val density = LocalDensity.current
60-
var tabWidth: Dp
61-
with(density) {
62-
tabWidth = ((windowSize.width.toDp() - (tabs.size - 1) * 9.dp) / tabs.size).coerceAtLeast(62.dp)
63-
}
64-
65-
val shape = remember { derivedStateOf { SmoothRoundedCornerShape(cornerRadius) } }
62+
val config = rememberTabRowConfig(tabs, minWidth, cornerRadius)
6663

6764
LazyRow(
68-
state = listState,
65+
state = config.listState,
6966
modifier = modifier
70-
.fillMaxWidth().height(42.dp)
67+
.fillMaxWidth()
68+
.height(height)
69+
.clip(config.shape)
7170
.overScrollHorizontal(),
7271
verticalAlignment = Alignment.CenterVertically,
7372
horizontalArrangement = Arrangement.spacedBy(9.dp)
7473
) {
7574
itemsIndexed(tabs) { index, tabText ->
7675
Surface(
77-
shape = shape.value,
78-
onClick = {
79-
onSelect?.invoke(index)
80-
},
81-
enabled = onSelect != null,
82-
color = if (selectedTabIndex == index) selectedBackgroundColor else backgroundColor,
76+
shape = config.shape,
77+
onClick = { onTabSelected?.invoke(index) },
78+
enabled = onTabSelected != null,
79+
color = colors.backgroundColor(selectedTabIndex == index),
8380
modifier = Modifier
8481
.fillMaxHeight()
85-
.width(tabWidth)
82+
.width(config.tabWidth)
8683
.semantics { role = Role.Tab }
8784
) {
8885
Box(
@@ -91,12 +88,164 @@ fun TabRow(
9188
) {
9289
Text(
9390
text = tabText,
94-
color = if (selectedTabIndex == index) selectedColor else contentColor,
91+
color = colors.contentColor(selectedTabIndex == index),
9592
fontWeight = if (selectedTabIndex == index) FontWeight.Bold else FontWeight.Normal,
96-
maxLines = 1
93+
maxLines = 1,
94+
overflow = TextOverflow.Ellipsis
9795
)
9896
}
9997
}
10098
}
10199
}
100+
}
101+
102+
/**
103+
* A [TabRowWithContour] with Miuix style.
104+
*
105+
* @param tabs The text to be displayed in the [TabRow].
106+
* @param selectedTabIndex The selected tab index of the [TabRow]
107+
* @param modifier The modifier to be applied to the [TabRow].
108+
* @param colors The colors of the [TabRow].
109+
* @param minWidth The minimum width of the tab in [TabRow].
110+
* @param height The height of the [TabRow].
111+
* @param cornerRadius The round corner radius of the tab in [TabRow].
112+
* @param onTabSelected The callback when a tab is selected.
113+
*/
114+
@Composable
115+
fun TabRowWithContour(
116+
tabs: List<String>,
117+
selectedTabIndex: Int,
118+
modifier: Modifier = Modifier,
119+
colors: TabRowColors = TabRowDefaults.tabRowColors(),
120+
minWidth: Dp = TabRowDefaults.TabRowWithContourMinWidth,
121+
height: Dp = TabRowDefaults.TabRowHeight,
122+
cornerRadius: Dp = TabRowDefaults.TabRowWithContourCornerRadius,
123+
onTabSelected: ((Int) -> Unit)? = null,
124+
) {
125+
val config = rememberTabRowConfig(tabs, minWidth, cornerRadius)
126+
127+
LazyRow(
128+
state = config.listState,
129+
modifier = modifier
130+
.fillMaxWidth()
131+
.height(height)
132+
.clip(SmoothRoundedCornerShape(cornerRadius + 5.dp))
133+
.background(color = colors.backgroundColor(false))
134+
.padding(5.dp)
135+
.clip(SmoothRoundedCornerShape(cornerRadius))
136+
.overScrollHorizontal(),
137+
verticalAlignment = Alignment.CenterVertically
138+
) {
139+
itemsIndexed(tabs) { index, tabText ->
140+
val isSelected = index == selectedTabIndex
141+
142+
Box(
143+
modifier = Modifier
144+
.fillMaxHeight()
145+
.width(config.tabWidth)
146+
.clip(config.shape)
147+
.background(colors.backgroundColor(selectedTabIndex == index))
148+
.clickable(enabled = onTabSelected != null) {
149+
onTabSelected?.invoke(index)
150+
}
151+
.semantics { role = Role.Tab },
152+
contentAlignment = Alignment.Center
153+
) {
154+
Text(
155+
text = tabText,
156+
color = colors.contentColor(selectedTabIndex == index),
157+
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
158+
maxLines = 1,
159+
overflow = TextOverflow.Ellipsis
160+
)
161+
}
162+
}
163+
}
164+
}
165+
166+
/**
167+
* Base configuration for TabRow implementations.
168+
*/
169+
private data class TabRowConfig(
170+
val tabWidth: Dp,
171+
val shape: SmoothRoundedCornerShape,
172+
val listState: androidx.compose.foundation.lazy.LazyListState
173+
)
174+
175+
/**
176+
* Prepare common TabRow configuration.
177+
*/
178+
@Composable
179+
private fun rememberTabRowConfig(tabs: List<String>, minWidth: Dp, cornerRadius: Dp): TabRowConfig {
180+
val listState = rememberLazyListState()
181+
val windowSize = getWindowSize()
182+
val density = LocalDensity.current
183+
val tabWidth = with(density) {
184+
((windowSize.width.toDp() - (tabs.size - 1) * 9.dp) / tabs.size).coerceAtLeast(minWidth)
185+
}
186+
val shape = remember { derivedStateOf { SmoothRoundedCornerShape(cornerRadius) } }
187+
188+
return TabRowConfig(tabWidth, shape.value, listState)
189+
}
190+
191+
object TabRowDefaults {
192+
193+
/**
194+
* The default height of the [TabRow].
195+
*/
196+
val TabRowHeight = 42.dp
197+
198+
/**
199+
* The default corner radius of the [TabRow].
200+
*/
201+
val TabRowCornerRadius = 8.dp
202+
203+
/**
204+
* The default corner radius of the [TabRow] with contour style.
205+
*/
206+
val TabRowWithContourCornerRadius = 10.dp
207+
208+
/**
209+
* The default minimum width of the [TabRow].
210+
*/
211+
val TabRowMinWidth = 76.dp
212+
213+
/**
214+
* The default minimum width of the [TabRow] with contour style.
215+
*/
216+
val TabRowWithContourMinWidth = 62.dp
217+
218+
/**
219+
* The default colors for the [TabRow].
220+
*/
221+
@Composable
222+
fun tabRowColors(
223+
backgroundColor: Color = MiuixTheme.colorScheme.background,
224+
contentColor: Color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
225+
selectedBackgroundColor: Color = MiuixTheme.colorScheme.surface,
226+
selectedContentColor: Color = MiuixTheme.colorScheme.onSurface
227+
): TabRowColors {
228+
return TabRowColors(
229+
backgroundColor = backgroundColor,
230+
contentColor = contentColor,
231+
selectedBackgroundColor = selectedBackgroundColor,
232+
selectedContentColor = selectedContentColor
233+
)
234+
}
235+
}
236+
237+
@Immutable
238+
class TabRowColors(
239+
private val backgroundColor: Color,
240+
private val contentColor: Color,
241+
private val selectedBackgroundColor: Color,
242+
private val selectedContentColor: Color
243+
) {
244+
@Stable
245+
internal fun backgroundColor(selected: Boolean): Color =
246+
if (selected) selectedBackgroundColor else backgroundColor
247+
248+
@Stable
249+
internal fun contentColor(selected: Boolean): Color =
250+
if (selected) selectedContentColor else contentColor
102251
}

0 commit comments

Comments
 (0)