Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
package com.team.prezel.core.designsystem.component

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.team.prezel.core.designsystem.foundation.typography.PrezelTextStyles
import com.team.prezel.core.designsystem.preview.ThemePreview
import com.team.prezel.core.designsystem.theme.PrezelTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.abs

@Immutable
data class PrezelTabItem(
val label: String,
val badgeCount: Int? = null,
val enabled: Boolean = true,
)

enum class PrezelTabSize { Small, Regular }

@Composable
fun PrezelTabs(
tabs: ImmutableList<PrezelTabItem>,
pagerState: PagerState,
modifier: Modifier = Modifier,
size: PrezelTabSize = PrezelTabSize.Regular,
userScrollEnabled: Boolean = true,
content: @Composable (pageIndex: Int) -> Unit,
) {
require(tabs.isNotEmpty()) { "tabs는 비어있을 수 없습니다." }
require(pagerState.pageCount == tabs.size) {
"pagerState.pageCount(${pagerState.pageCount})와 tabs.size(${tabs.size})가 일치해야 합니다."
}

val scope = rememberCoroutineScope()

Column(modifier = modifier.fillMaxSize()) {
PrezelTabsBar(
tabs = tabs,
pagerState = pagerState,
size = size,
onTabClick = { index, enabled ->
if (!enabled) return@PrezelTabsBar
handleTabClick(scope, pagerState, index)
},
)

PrezelTabsPager(
pagerState = pagerState,
userScrollEnabled = userScrollEnabled,
content = content,
)
}
}

@Composable
private fun PrezelTabsBar(
tabs: ImmutableList<PrezelTabItem>,
pagerState: PagerState,
size: PrezelTabSize,
onTabClick: (index: Int, enabled: Boolean) -> Unit,
) {
SecondaryTabRow(
selectedTabIndex = pagerState.currentPage,
modifier = Modifier.fillMaxWidth(),
containerColor = Color.Transparent,
indicator = {
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(2.dp)
.tabIndicatorOffset(
selectedTabIndex = pagerState.currentPage,
).background(PrezelTheme.colors.solidBlack),
)
},
) {
tabs.forEachIndexed { index, item ->
PrezelTabContent(
item = item,
selected = pagerState.currentPage == index,
size = size,
onClick = { onTabClick(index, item.enabled) },
)
}
}
}

@Composable
private fun PrezelTabContent(
item: PrezelTabItem,
selected: Boolean,
size: PrezelTabSize,
onClick: () -> Unit,
) {
Tab(
selected = selected,
onClick = onClick,
modifier = Modifier.height(if (size == PrezelTabSize.Small) 36.dp else 48.dp),
enabled = item.enabled,
text = {
PrezelTabLabel(
label = item.label,
size = size,
badgeCount = item.badgeCount,
selected = selected,
)
},
selectedContentColor = PrezelTheme.colors.solidBlack,
unselectedContentColor = PrezelTheme.colors.textDisabled,
)
}

@Composable
private fun PrezelTabLabel(
label: String,
size: PrezelTabSize,
selected: Boolean,
badgeCount: Int? = null,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = label,
style = if (size == PrezelTabSize.Small) PrezelTextStyles.Body3Medium.toTextStyle() else PrezelTextStyles.Body2Bold.toTextStyle(),
)

if (badgeCount != null) {
PrezelBadge(
active = false,
count = badgeCount,
size = PrezelBadgeSize.REGULAR,
disabled = !selected,
)
}
}
}

@Composable
private fun PrezelTabsPager(
pagerState: PagerState,
userScrollEnabled: Boolean,
content: @Composable (pageIndex: Int) -> Unit,
) {
HorizontalPager(
state = pagerState,
userScrollEnabled = userScrollEnabled,
overscrollEffect = null,
) { page ->
content(page)
}
}

private fun handleTabClick(
scope: CoroutineScope,
pagerState: PagerState,
target: Int,
) {
scope.launch {
val current = pagerState.currentPage
val distance = abs(current - target)

if (distance <= 1) {
pagerState.animateScrollToPage(target)
} else {
pagerState.scrollToPage(target)
}
}
}

@ThemePreview
@Composable
private fun PrezelMediumTabPreview() {
val tabs = persistentListOf(
PrezelTabItem(label = "Label", badgeCount = 0),
PrezelTabItem(label = "Label", badgeCount = 0),
PrezelTabItem(label = "Label", badgeCount = 0),
)
val pagerState = rememberPagerState(initialPage = 0) { tabs.size }

PrezelTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(PrezelTheme.colors.bgRegular),
) {
PrezelTabs(
tabs = tabs,
pagerState = pagerState,
size = PrezelTabSize.Regular,
modifier = Modifier,
) { page ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text("Page: $page")
}
}
}
}
}

@ThemePreview
@Composable
private fun PrezelSmallTabPreview() {
val tabs = persistentListOf(
PrezelTabItem(label = "Label", badgeCount = 2),
PrezelTabItem(label = "Label", badgeCount = 3),
)
val pagerState = rememberPagerState(initialPage = 0) { tabs.size }

PrezelTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(PrezelTheme.colors.bgRegular),
) {
PrezelTabs(
tabs = tabs,
pagerState = pagerState,
size = PrezelTabSize.Small,
) { page ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text("Page: $page")
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3.898,20.994C3.427,20.946 3.053,20.572 3.005,20.101L3,20V17.413C3,17.148 3.106,16.894 3.293,16.706L16.413,3.586C17.194,2.805 18.462,2.805 19.243,3.586L20.414,4.758C21.195,5.539 21.195,6.806 20.414,7.587L7.294,20.707L7.221,20.773C7.043,20.919 6.819,21 6.587,21H4L3.898,20.994ZM18.182,4.647C17.987,4.452 17.67,4.452 17.475,4.647L4.5,17.62V19.5H6.38L19.353,6.526C19.549,6.331 19.549,6.014 19.353,5.819L18.182,4.647Z"
android:fillColor="#6E737D"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,3C14.761,3 17,5.066 17,7.615V9H19C20.105,9 21,9.895 21,11V19L20.989,19.204C20.894,20.146 20.146,20.894 19.204,20.989L19,21H5L4.796,20.989C3.854,20.894 3.106,20.146 3.011,19.204L3,19V11C3,9.895 3.895,9 5,9H7V7.615C7,5.066 9.239,3 12,3ZM5,10.5C4.724,10.5 4.5,10.724 4.5,11V19C4.5,19.276 4.724,19.5 5,19.5H19C19.276,19.5 19.5,19.276 19.5,19V11C19.5,10.724 19.276,10.5 19,10.5H5ZM12,13.25C12.414,13.25 12.75,13.586 12.75,14V16C12.75,16.414 12.414,16.75 12,16.75C11.586,16.75 11.25,16.414 11.25,16V14C11.25,13.586 11.586,13.25 12,13.25ZM12,4.5C9.952,4.5 8.5,6.005 8.5,7.615V9H15.5V7.615C15.5,6.005 14.048,4.5 12,4.5Z"
android:fillColor="#6E737D"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.802,3.585C10.501,1.471 13.499,1.471 14.198,3.585L14.522,4.567C14.868,5.612 15.899,6.259 16.976,6.139L17.191,6.104L18.206,5.893C20.39,5.441 21.888,8.03 20.403,9.691L19.714,10.463L19.576,10.632C18.934,11.502 18.98,12.716 19.714,13.537L20.403,14.309C21.842,15.918 20.481,18.398 18.408,18.14L18.206,18.106L17.191,17.896C16.04,17.657 14.891,18.318 14.522,19.433L14.198,20.415L14.126,20.606C13.34,22.465 10.66,22.465 9.874,20.606L9.802,20.415L9.478,19.433C9.109,18.318 7.96,17.657 6.809,17.896L5.794,18.106L5.592,18.14C3.586,18.39 2.246,16.075 3.466,14.467L3.597,14.309L4.286,13.537C5.069,12.661 5.069,11.339 4.286,10.463L3.597,9.691C2.112,8.03 3.61,5.441 5.794,5.893L6.809,6.104C7.888,6.328 8.965,5.761 9.399,4.77L9.478,4.567L9.802,3.585ZM12.773,4.056C12.528,3.315 11.472,3.315 11.226,4.056L10.901,5.038C10.293,6.878 8.4,7.966 6.505,7.573L5.489,7.362C4.71,7.201 4.202,8.118 4.715,8.691L5.404,9.463C6.696,10.908 6.696,13.092 5.404,14.537L4.715,15.309C4.202,15.882 4.71,16.799 5.489,16.638L6.505,16.427C8.4,16.034 10.293,17.122 10.901,18.962L11.226,19.943C11.472,20.685 12.528,20.685 12.773,19.943L13.099,18.962C13.707,17.122 15.6,16.034 17.495,16.427L18.511,16.638C19.29,16.799 19.798,15.882 19.285,15.309L18.596,14.537C17.304,13.092 17.303,10.908 18.596,9.463L19.285,8.691C19.798,8.118 19.29,7.201 18.511,7.362L17.495,7.573C15.6,7.966 13.707,6.878 13.099,5.038L12.773,4.056ZM12,8C14.209,8 16,9.791 16,12C16,14.209 14.209,16 12,16C9.791,16 8,14.209 8,12C8,9.791 9.791,8 12,8ZM12,9.5C10.619,9.5 9.5,10.619 9.5,12C9.5,13.381 10.619,14.5 12,14.5C13.381,14.5 14.5,13.381 14.5,12C14.5,10.619 13.381,9.5 12,9.5Z"
android:fillColor="#6E737D"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,8C20.105,8 21,8.895 21,10V18.637C21,19.942 20.104,21 19,21H5C3.896,21 3,19.942 3,18.637V10C3,8.895 3.895,8 5,8H19ZM11,12.25C10.586,12.25 10.25,12.586 10.25,13C10.25,13.414 10.586,13.75 11,13.75H13C13.414,13.75 13.75,13.414 13.75,13C13.75,12.586 13.414,12.25 13,12.25H11ZM19,5.496C19.414,5.496 19.75,5.832 19.75,6.246C19.75,6.66 19.414,6.996 19,6.996H5C4.586,6.996 4.25,6.66 4.25,6.246C4.25,5.832 4.586,5.496 5,5.496H19ZM18,3.001C18.414,3.001 18.75,3.337 18.75,3.751C18.75,4.165 18.414,4.501 18,4.501H6C5.586,4.501 5.25,4.165 5.25,3.751C5.25,3.337 5.586,3.001 6,3.001H18Z"
android:fillColor="#6E737D"/>
</vector>