redesign segment controls

This commit is contained in:
Owen LeJeune
2023-07-11 13:52:18 -04:00
parent 2bdff383b7
commit 0c2689c329
8 changed files with 251 additions and 216 deletions

View File

@@ -0,0 +1,152 @@
package com.owenlejeune.tvtime.ui.components
import android.annotation.SuppressLint
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.owenlejeune.tvtime.extensions.toDp
@SuppressLint("AutoboxingStateValueProperty")
@Composable
fun <T> PillSegmentedControl(
items: List<T>,
itemLabel: (index: Int, item: T) -> String,
onItemSelected: (index: Int, item: T) -> Unit,
modifier: Modifier = Modifier,
defaultSelectedItemIndex: Int = 0,
cornerRadius: Int = 50,
colors: SegmentControlColors = SegmentControlDefaults.pillSegmentControlColors()
) {
val selectedIndex = remember { mutableIntStateOf(defaultSelectedItemIndex) }
val parentBoxSize = remember { mutableStateOf(IntSize.Zero) }
val textViewSize = remember { mutableStateOf(IntSize.Zero) }
val maxTextViewSize = remember { mutableStateOf(IntSize.Zero) }
val offsetAnimation by animateDpAsState(targetValue = (selectedIndex.value * textViewSize.value.width).toDp())
BoxWithConstraints(
modifier = modifier.then(
Modifier
.border(
width = 1.dp,
color = colors.borderColor(),
shape = RoundedCornerShape(cornerRadius)
)
.onGloballyPositioned {
parentBoxSize.value = it.size
}
)
) {
Box(
modifier = Modifier
.offset(
x = offsetAnimation,
y = 0.dp
)
.clip(RoundedCornerShape(cornerRadius))
.width((parentBoxSize.value.width / items.size).toDp())
.height(parentBoxSize.value.height.toDp())
.background(color = colors.pillColor())
)
Row(
modifier = Modifier
.fillMaxWidth(),
) {
items.forEachIndexed { index, item ->
Text(
text = itemLabel(index, item),
color = if (selectedIndex.value == index) colors.selectedTextColor() else colors.textColor(),
textAlign = TextAlign.Center,
modifier = Modifier
.padding(vertical = 8.dp)
.onGloballyPositioned {
textViewSize.value = it.size
if (it.size.width > maxTextViewSize.value.width) {
maxTextViewSize.value = it.size
}
}
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) {
selectedIndex.value = index
onItemSelected(index, item)
}
.weight(1f)
)
}
}
}
}
interface SegmentControlColors {
@Composable fun borderColor(): Color
@Composable fun pillColor(): Color
@Composable fun textColor(): Color
@Composable fun selectedTextColor(): Color
}
object SegmentControlDefaults {
@Composable
fun pillSegmentControlColors(
borderColor: Color = MaterialTheme.colorScheme.primary,
pillColor: Color = MaterialTheme.colorScheme.primary,
textColor: Color = MaterialTheme.colorScheme.primary,
selectedTextColor: Color = MaterialTheme.colorScheme.background
): SegmentControlColors {
return object : SegmentControlColors {
@Composable override fun borderColor() = borderColor
@Composable override fun pillColor() = pillColor
@Composable override fun textColor() = textColor
@Composable override fun selectedTextColor() = selectedTextColor
}
}
}
@Preview
@Composable
fun PillSegmentControlPreview() {
val items = listOf("One", "Two", "Three", "Four")
val colors = SegmentControlDefaults.pillSegmentControlColors(borderColor = Color.Red, pillColor = Color.Red, textColor = Color.Red, selectedTextColor = Color.White)
Column(
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.background(Color.White)
) {
PillSegmentedControl(items = items.subList(0, 2), itemLabel = { _, t -> t }, onItemSelected = { _, _ -> }, colors = colors)
PillSegmentedControl(items = items.subList(0, 3), itemLabel = { _, t -> t }, onItemSelected = { _, _ -> }, colors = colors)
PillSegmentedControl(items = items, itemLabel = { _, t -> t }, onItemSelected = { _, _ -> }, colors = colors)
}
}

View File

@@ -1116,72 +1116,4 @@ fun SearchBar(
@Composable
fun MyDivider(modifier: Modifier = Modifier) {
Divider(thickness = 0.5.dp, modifier = modifier, color = MaterialTheme.colorScheme.secondaryContainer)
}
@Composable
fun SelectableTextItem(
selected: Boolean,
onSelected: () -> Unit,
text: String,
selectedColor: Color = MaterialTheme.colorScheme.secondary,
unselectedColor: Color = MaterialTheme.colorScheme.onSurfaceVariant
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.clickable(onClick = onSelected)
) {
Column(
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(8.dp)
) {
val size = remember { mutableStateOf(IntSize.Zero) }
val color = if (selected) selectedColor else unselectedColor
Text(
text = text,
fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,
color = color,
modifier = Modifier
.padding(horizontal = 4.dp)
.onGloballyPositioned { size.value = it.size }
)
Box(
modifier = Modifier
.height(height = if (selected) 3.dp else 1.dp)
.width(width = size.value.width.toDp().plus(8.dp))
.clip(RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp))
.background(color = color)
)
}
}
}
@Composable
fun SelectableTextChip(
selected: Boolean,
onSelected: () -> Unit,
text: String,
modifier: Modifier = Modifier,
selectedColor: Color = MaterialTheme.colorScheme.secondary,
unselectedColor: Color = MaterialTheme.colorScheme.surfaceVariant
) {
Box(
modifier = modifier.then(
Modifier
.clip(RoundedCornerShape(percent = 25))
.border(width = 1.dp, color = selectedColor, shape = RoundedCornerShape(percent = 25))
.background(color = if(selected) selectedColor else unselectedColor)
.clickable(onClick = onSelected)
)
) {
Text(
text = text,
color = if (selected) unselectedColor else selectedColor,
fontSize = 16.sp,
modifier = Modifier
.align(Alignment.Center)
.padding(12.dp)
)
}
}

View File

@@ -2,7 +2,6 @@ package com.owenlejeune.tvtime.ui.screens
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -31,14 +30,10 @@ import androidx.navigation.NavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.CrewMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.DetailCrew
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.MovieCastMember
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCast
import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCastMember
import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.ui.components.MediaResultCard
import com.owenlejeune.tvtime.ui.components.SelectableTextChip
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
import com.owenlejeune.tvtime.utils.types.MediaViewType
@@ -99,29 +94,15 @@ fun CastCrewListScreen(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Row(
modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
SelectableTextChip(
selected = castSelected,
onSelected = { castSelected = true },
text = stringResource(id = R.string.actor_label),
selectedColor = MaterialTheme.colorScheme.tertiary,
unselectedColor = MaterialTheme.colorScheme.background
)
SelectableTextChip(
selected = !castSelected,
onSelected = { castSelected = false },
text = stringResource(id = R.string.production_label),
selectedColor = MaterialTheme.colorScheme.tertiary,
unselectedColor = MaterialTheme.colorScheme.background
)
}
val labels = listOf(stringResource(id = R.string.actor_label), stringResource(id = R.string.production_label))
PillSegmentedControl(
items = labels,
itemLabel = { _, t -> t },
onItemSelected = { i, _ -> castSelected = i == 0 },
modifier = Modifier.padding(horizontal = 8.dp)
)
}
items(items!!) { item ->
val additionalDetails = emptyList<String>().toMutableList()
when (item) {

View File

@@ -36,7 +36,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.TvCast
import com.owenlejeune.tvtime.extensions.bringToFront
import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.ui.components.MediaResultCard
import com.owenlejeune.tvtime.ui.components.SelectableTextChip
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
@@ -99,19 +99,11 @@ fun KnownForScreen(
modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
SelectableTextChip(
selected = actorSelected,
onSelected = { actorSelected = true },
text = stringResource(id = R.string.actor_label),
selectedColor = MaterialTheme.colorScheme.tertiary,
unselectedColor = MaterialTheme.colorScheme.background
)
SelectableTextChip(
selected = !actorSelected,
onSelected = { actorSelected = false },
text = stringResource(id = R.string.production_label),
selectedColor = MaterialTheme.colorScheme.tertiary,
unselectedColor = MaterialTheme.colorScheme.background
val labels = listOf(stringResource(id = R.string.actor_label), stringResource(id = R.string.production_label))
PillSegmentedControl(
items = labels,
itemLabel = { _, t -> t },
onItemSelected = { i, _ -> actorSelected = i == 0}
)
}
}

View File

@@ -1,5 +1,6 @@
package com.owenlejeune.tvtime.ui.screens
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.widget.Toast
@@ -48,7 +49,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -112,10 +112,10 @@ import com.owenlejeune.tvtime.ui.components.FullScreenThumbnailVideoPlayer
import com.owenlejeune.tvtime.ui.components.HtmlText
import com.owenlejeune.tvtime.ui.components.ImageGalleryOverlay
import com.owenlejeune.tvtime.ui.components.ListContentCard
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl
import com.owenlejeune.tvtime.ui.components.PosterItem
import com.owenlejeune.tvtime.ui.components.RoundedChip
import com.owenlejeune.tvtime.ui.components.RoundedTextField
import com.owenlejeune.tvtime.ui.components.SelectableTextChip
import com.owenlejeune.tvtime.ui.components.TwoLineImageTextCard
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
@@ -986,6 +986,7 @@ private fun VideoGroup(results: List<Video>, type: Video.Type, title: String) {
}
}
@SuppressLint("AutoboxingStateValueProperty")
@Composable
private fun WatchProvidersCard(
itemId: Int,
@@ -1012,49 +1013,33 @@ private fun WatchProvidersCard(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
var selectedIndex by remember { mutableIntStateOf(
when {
providers.flaterate != null -> 0
providers.rent != null -> 1
providers.buy != null -> 2
else -> -1
}
) }
Row(
modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
providers.flaterate?.let {
SelectableTextChip(
selected = selectedIndex == 0,
onSelected = { selectedIndex = 0 },
text = stringResource(R.string.streaming_label)
)
}
providers.rent?.let {
SelectableTextChip(
selected = selectedIndex == 1,
onSelected = { selectedIndex = 1 },
text = stringResource(R.string.rent_label)
)
}
providers.buy?.let {
SelectableTextChip(
selected = selectedIndex == 2,
onSelected = { selectedIndex = 2 },
text = stringResource(R.string.buy_label)
)
}
val itemsMap = mutableMapOf<Int, List<WatchProviderDetails>>().apply {
providers.flaterate?.let { put(0, it) }
providers.rent?.let { put(1, it) }
providers.buy?.let { put(2, it) }
}
val selected = remember { mutableStateOf(if (itemsMap.isEmpty()) null else itemsMap.values.first()) }
val context = LocalContext.current
PillSegmentedControl(
items = itemsMap.values.toList(),
itemLabel = { i, _ ->
when (i) {
0 -> context.getString(R.string.streaming_label)
1 -> context.getString(R.string.rent_label)
2 -> context.getString(R.string.buy_label)
else -> ""
}
},
onItemSelected = { i, _ -> selected.value = itemsMap.values.toList()[i] },
modifier = Modifier.padding(all = 8.dp)
)
Crossfade(
modifier = modifier.padding(top = 4.dp, bottom = 12.dp),
targetState = selectedIndex
) { index ->
when (index) {
0 -> WatchProviderContainer(watchProviders = providers.flaterate!!, link = providers.link)
1 -> WatchProviderContainer(watchProviders = providers.rent!!, link = providers.link)
2 -> WatchProviderContainer(watchProviders = providers.buy!!, link = providers.link)
}
targetState = selected.value
) { value ->
WatchProviderContainer(watchProviders = value!!, link = providers.link)
}
}
}

View File

@@ -32,7 +32,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v3.model.*
import com.owenlejeune.tvtime.extensions.getCalendarYear
import com.owenlejeune.tvtime.extensions.lazyPagingItems
import com.owenlejeune.tvtime.ui.components.MediaResultCard
import com.owenlejeune.tvtime.ui.components.SelectableTextChip
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
import com.owenlejeune.tvtime.ui.viewmodel.SearchViewModel
import com.owenlejeune.tvtime.utils.TmdbUtils
@@ -106,31 +106,26 @@ fun SearchScreen(
)
Divider(thickness = 2.dp, color = MaterialTheme.colorScheme.surfaceVariant)
Row(
modifier = Modifier.padding(start = 12.dp, top = 12.dp, bottom = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SelectableTextChip(
selected = viewType.value == MediaViewType.MOVIE,
onSelected = { viewType.value = MediaViewType.MOVIE },
text = stringResource(id = R.string.nav_movies_title)
)
SelectableTextChip(
selected = viewType.value == MediaViewType.TV,
onSelected = { viewType.value = MediaViewType.TV },
text = stringResource(id = R.string.nav_tv_title)
)
SelectableTextChip(
selected = viewType.value == MediaViewType.PERSON,
onSelected = { viewType.value = MediaViewType.PERSON },
text = stringResource(id = R.string.nav_people_title)
)
SelectableTextChip(
selected = viewType.value == MediaViewType.MIXED,
onSelected = { viewType.value = MediaViewType.MIXED },
text = stringResource(id = R.string.search_multi_title)
)
}
val searchTypes = listOf(MediaViewType.MOVIE, MediaViewType.TV, MediaViewType.PERSON, MediaViewType.MIXED)
val selected = remember { mutableStateOf(searchTypes[0]) }
val context = LocalContext.current
PillSegmentedControl(
items = searchTypes,
itemLabel = { _, i ->
when (i) {
MediaViewType.MOVIE -> context.getString(R.string.nav_movies_title)
MediaViewType.TV -> context.getString(R.string.nav_tv_title)
MediaViewType.PERSON -> context.getString(R.string.nav_people_title)
MediaViewType.MIXED -> context.getString(R.string.search_multi_title)
else -> ""
}
},
onItemSelected = { _, i ->
selected.value = i
},
modifier = Modifier.padding(start = 12.dp, top = 12.dp, end = 12.dp)
)
when (viewType.value) {
MediaViewType.TV -> {

View File

@@ -1,14 +1,14 @@
package com.owenlejeune.tvtime.ui.screens.tabs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -21,9 +21,9 @@ import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl
import com.owenlejeune.tvtime.ui.components.ScrollableTabs
import com.owenlejeune.tvtime.ui.components.SearchView
import com.owenlejeune.tvtime.ui.components.SelectableTextChip
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.HomeScreenViewModel
import com.owenlejeune.tvtime.ui.viewmodel.MainViewModel
@@ -102,25 +102,23 @@ fun MediaTabTrendingContent(
PagingPosterGrid(
lazyPagingItems = mediaListItems,
header = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
val options = listOf(TimeWindow.DAY, TimeWindow.WEEK)
val context = LocalContext.current
PillSegmentedControl(
items = options,
itemLabel = { _, i ->
when (i) {
TimeWindow.DAY -> context.getString(R.string.time_window_day)
TimeWindow.WEEK -> context.getString(R.string.time_window_week)
}
},
onItemSelected = { _, i -> timeWindow.value = i },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.padding(bottom = 4.dp)
) {
SelectableTextChip(
selected = timeWindow.value == TimeWindow.DAY,
onSelected = { timeWindow.value = TimeWindow.DAY },
text = stringResource(id = R.string.time_window_day),
modifier = Modifier.weight(1f)
)
SelectableTextChip(
selected = timeWindow.value == TimeWindow.WEEK,
onSelected = { timeWindow.value = TimeWindow.WEEK },
text = stringResource(id = R.string.time_window_week),
modifier = Modifier.weight(1f)
)
}
)
},
onClick = { id ->
appNavController.navigate(

View File

@@ -1,14 +1,13 @@
package com.owenlejeune.tvtime.ui.screens.tabs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -23,8 +22,8 @@ import com.google.accompanist.pager.rememberPagerState
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.ui.components.PagingPeoplePosterGrid
import com.owenlejeune.tvtime.ui.components.PagingPosterGrid
import com.owenlejeune.tvtime.ui.components.PillSegmentedControl
import com.owenlejeune.tvtime.ui.components.SearchView
import com.owenlejeune.tvtime.ui.components.SelectableTextChip
import com.owenlejeune.tvtime.ui.components.Tabs
import com.owenlejeune.tvtime.ui.navigation.AppNavItem
import com.owenlejeune.tvtime.ui.viewmodel.HomeScreenViewModel
@@ -91,23 +90,24 @@ fun TrendingPeopleContent(
val mediaListItems = flow.value.collectAsLazyPagingItems()
Column {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(all = 12.dp)
) {
SelectableTextChip(
selected = timeWindow.value == TimeWindow.DAY,
onSelected = { timeWindow.value = TimeWindow.DAY },
text = stringResource(id = R.string.time_window_day),
modifier = Modifier.weight(1f)
)
SelectableTextChip(
selected = timeWindow.value == TimeWindow.WEEK,
onSelected = { timeWindow.value = TimeWindow.WEEK },
text = stringResource(id = R.string.time_window_week),
modifier = Modifier.weight(1f)
)
}
val timeWindows = listOf(TimeWindow.DAY, TimeWindow.WEEK)
val selected = remember { mutableStateOf(timeWindows[0]) }
val context = LocalContext.current
PillSegmentedControl(
items = timeWindows,
itemLabel = { _, i ->
when (i) {
TimeWindow.DAY -> context.getString(R.string.time_window_day)
TimeWindow.WEEK -> context.getString(R.string.time_window_week)
}
},
onItemSelected = { _, t ->
selected.value = t
},
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp)
)
PagingPosterGrid(
lazyPagingItems = mediaListItems,
onClick = { id ->