From dc9b7f57912880d2bac6bc4fde21c0c329b11a28 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 1 Dec 2023 08:12:41 +0300 Subject: [PATCH 1/2] add match cards --- .../kotlin/main_screen/MainScreen.kt | 4 +- .../com/stslex/core/ui/theme/AppDimension.kt | 6 + .../feature/match_feed/ui/MatchFeedScreen.kt | 12 +- ...enContent.kt => MatchFeedScreenContent.kt} | 23 ++- ...ScreenError.kt => MatchFeedScreenError.kt} | 2 +- ...FilmItem.kt => MatchFeedScreenFilmItem.kt} | 145 +++++++++++++----- ...enLoading.kt => MatchFeedScreenLoading.kt} | 2 +- .../feature/match_feed/ui/components/UiExt.kt | 28 ++++ 8 files changed, 170 insertions(+), 52 deletions(-) rename feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/{FeedScreenContent.kt => MatchFeedScreenContent.kt} (76%) rename feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/{FeedScreenError.kt => MatchFeedScreenError.kt} (93%) rename feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/{FeedScreenFilmItem.kt => MatchFeedScreenFilmItem.kt} (52%) rename feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/{FeedScreenLoading.kt => MatchFeedScreenLoading.kt} (93%) create mode 100644 feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/UiExt.kt diff --git a/composeApp/src/commonMain/kotlin/main_screen/MainScreen.kt b/composeApp/src/commonMain/kotlin/main_screen/MainScreen.kt index a9a52d6f..3fcfea45 100644 --- a/composeApp/src/commonMain/kotlin/main_screen/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/main_screen/MainScreen.kt @@ -11,7 +11,7 @@ import cafe.adriel.voyager.navigator.tab.CurrentTab import cafe.adriel.voyager.navigator.tab.TabNavigator import com.stslex.core.ui.mvi.setupNavigator import main_screen.bottom_nav_bar.BottomNavigationBar -import main_screen.bottom_nav_bar.FeedTab +import main_screen.bottom_nav_bar.BottomNavigationTabs object MainScreen : Screen { @@ -20,7 +20,7 @@ object MainScreen : Screen { setupNavigator() TabNavigator( - tab = FeedTab, + tab = BottomNavigationTabs.MATCH_FEED.tab, ) { tabNavigator -> Scaffold( content = { paddingValues -> diff --git a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/theme/AppDimension.kt b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/theme/AppDimension.kt index 14433a20..e8629a45 100644 --- a/core/ui/src/commonMain/kotlin/com/stslex/core/ui/theme/AppDimension.kt +++ b/core/ui/src/commonMain/kotlin/com/stslex/core/ui/theme/AppDimension.kt @@ -17,4 +17,10 @@ object AppDimension { val medium = 10.dp val large = 15.dp } + + object Elevation { + val smallest = 2.dp + val small = 4.dp + val medium = 8.dp + } } \ No newline at end of file diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt index bc0bf789..21d52fb2 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt @@ -10,9 +10,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.getScreenModel -import com.stslex.feature.match_feed.ui.components.FeedScreenContent -import com.stslex.feature.match_feed.ui.components.FeedScreenError -import com.stslex.feature.match_feed.ui.components.FeedScreenLoading +import com.stslex.feature.match_feed.ui.components.MatchFeedScreenContent +import com.stslex.feature.match_feed.ui.components.MatchFeedScreenError +import com.stslex.feature.match_feed.ui.components.MatchFeedScreenLoading import com.stslex.feature.match_feed.ui.store.MatchFeedStore import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Action import com.stslex.feature.match_feed.ui.store.MatchFeedStoreComponent.Event.ErrorSnackBar @@ -55,15 +55,15 @@ private fun MatchFeedScreen( modifier = modifier.fillMaxSize() ) { when (val screenState = state.screen) { - is ScreenState.Content -> FeedScreenContent( + is ScreenState.Content -> MatchFeedScreenContent( loadMore = remember { { sendAction(Action.LoadFilms) } }, films = state.films, screenState = screenState, onFilmClick = remember { { sendAction(Action.FilmClick(it)) } }, ) - is ScreenState.Error -> FeedScreenError(screenState.message) - ScreenState.Loading -> FeedScreenLoading() + is ScreenState.Error -> MatchFeedScreenError(screenState.message) + ScreenState.Loading -> MatchFeedScreenLoading() } } } \ No newline at end of file diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenContent.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenContent.kt similarity index 76% rename from feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenContent.kt rename to feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenContent.kt index fac5a0d8..febe7f59 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenContent.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenContent.kt @@ -1,16 +1,21 @@ package com.stslex.feature.match_feed.ui.components +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.stslex.core.core.Logger import com.stslex.feature.match_feed.ui.model.FilmUi @@ -20,8 +25,9 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @Composable -internal fun FeedScreenContent( +internal fun MatchFeedScreenContent( loadMore: () -> Unit, films: ImmutableList, screenState: ScreenState.Content, @@ -45,12 +51,16 @@ internal fun FeedScreenContent( } } } + BoxWithConstraints { - val itemHeight = remember(maxHeight) { maxHeight / 3 } + val screenWidth = remember(maxWidth) { maxWidth } LazyColumn( modifier = modifier .fillMaxSize(), - state = listState + state = listState, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + flingBehavior = rememberSnapFlingBehavior(listState) ) { items( count = films.size, @@ -61,10 +71,11 @@ internal fun FeedScreenContent( ) { index -> val film = films.getOrNull(index) if (film != null) { - FeedScreenFilmItem( + MatchFeedScreenFilmItem( film = film, - itemHeight = itemHeight, + screenWidth = screenWidth, onFilmClick = onFilmClick, + listState = listState, ) } } @@ -77,7 +88,7 @@ internal fun FeedScreenContent( ) { Box( modifier = Modifier.fillMaxWidth(), - contentAlignment = androidx.compose.ui.Alignment.Center + contentAlignment = Alignment.Center ) { CircularProgressIndicator() } diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenError.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenError.kt similarity index 93% rename from feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenError.kt rename to feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenError.kt index e7a83e79..e37d934a 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenError.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenError.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @Composable -internal fun FeedScreenError( +internal fun MatchFeedScreenError( message: String, modifier: Modifier = Modifier ) { diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenFilmItem.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenFilmItem.kt similarity index 52% rename from feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenFilmItem.kt rename to feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenFilmItem.kt index c752bc06..2364c66b 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenFilmItem.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenFilmItem.kt @@ -1,28 +1,35 @@ package com.stslex.feature.match_feed.ui.components import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation 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.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.ElevatedCard +import androidx.compose.material.rememberSwipeableState +import androidx.compose.material.swipeable +import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import com.stslex.core.ui.base.image.NetworkImage @@ -30,33 +37,97 @@ import com.stslex.core.ui.base.onClickDelay import com.stslex.core.ui.theme.AppDimension import com.stslex.feature.match_feed.ui.model.FilmUi import kotlinx.collections.immutable.ImmutableList +import kotlin.math.abs -@OptIn(ExperimentalMaterial3Api::class) +enum class SwipeDirection { + LEFT, + RIGHT, + NONE +} + +@OptIn(ExperimentalMaterialApi::class) @Composable -internal fun FeedScreenFilmItem( +internal fun MatchFeedScreenFilmItem( modifier: Modifier = Modifier, - itemHeight: Dp, + screenWidth: Dp, film: FilmUi, + listState: LazyListState, onFilmClick: (String) -> Unit, ) { - val posterWidth = remember(itemHeight) { - (itemHeight - AppDimension.Padding.medium * 2) / 4 * 3 + val swipeableState = rememberSwipeableState(SwipeDirection.NONE) + + val posterWidth by remember(screenWidth) { + derivedStateOf { screenWidth - AppDimension.Padding.large * 2 } + } + val posterHeight by remember(posterWidth) { + derivedStateOf { posterWidth * 1.5f } } + val progress by remember { + derivedStateOf { + swipeableState.offset.value / screenWidth.value + } + } + - ElevatedCard( + Box( modifier = modifier - .fillMaxWidth() - .height(itemHeight) - .padding(AppDimension.Padding.medium), - shape = RoundedCornerShape(AppDimension.Radius.medium), - onClick = onClickDelay { - onFilmClick(film.uuid) - } + .graphicsLayer { + rotationZ = 15f * progress + translationX = swipeableState.offset.value + alpha = 1f - abs(progress * 0.5f) + } + .width(posterWidth) + .height(posterHeight) + .swipeable( + state = swipeableState, + anchors = mapOf( + -screenWidth.value to SwipeDirection.LEFT, + screenWidth.value to SwipeDirection.RIGHT, + 0f to SwipeDirection.NONE, + ), + orientation = Orientation.Horizontal, + ) + .animateItemTop( + listState = listState, + key = film.uuid, + ) + ) { + MatchFeedScreenFilmItemContent( + modifier = Modifier.fillMaxSize(), + film = film, + onFilmClick = onFilmClick, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun MatchFeedScreenFilmItemContent( + modifier: Modifier = Modifier, + film: FilmUi, + onFilmClick: (String) -> Unit, +) { + Column( + modifier = modifier + .fillMaxSize(), ) { - Row(modifier = Modifier.fillMaxSize()) { - Box { + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = film.title, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) + Card( + modifier = Modifier, + shape = RoundedCornerShape(AppDimension.Radius.medium), + onClick = onClickDelay { + onFilmClick(film.uuid) + }, + ) { + Box(modifier = Modifier.fillMaxSize()) { FeedItemFilmPreview( - modifier = Modifier.width(posterWidth), + modifier = Modifier.fillMaxSize(), url = film.poster, description = film.title ) @@ -72,23 +143,25 @@ internal fun FeedScreenFilmItem( text = film.rate, style = MaterialTheme.typography.titleSmall, ) - } - - Spacer(modifier = Modifier.width(AppDimension.Padding.big)) - Column { - Text( - text = film.title, - style = MaterialTheme.typography.titleLarge - ) - Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) - FilmItemGenres(genres = film.genres) - Spacer(modifier = Modifier.height(AppDimension.Padding.big)) - Text( - modifier = Modifier.fillMaxSize(), - text = film.description, - style = MaterialTheme.typography.bodySmall, - overflow = TextOverflow.Ellipsis - ) + Spacer(modifier = Modifier.width(AppDimension.Padding.big)) + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .background( + color = MaterialTheme.colorScheme.background.copy(alpha = 0.7f), + ) + .padding(AppDimension.Padding.medium) + ) { + Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) + FilmItemGenres(genres = film.genres) + Spacer(modifier = Modifier.height(AppDimension.Padding.big)) + Text( + text = film.description, + style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis, + maxLines = 4, + ) + } } } } diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenLoading.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenLoading.kt similarity index 93% rename from feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenLoading.kt rename to feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenLoading.kt index a663507f..28d3ef06 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/FeedScreenLoading.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenLoading.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @Composable -internal fun FeedScreenLoading( +internal fun MatchFeedScreenLoading( modifier: Modifier = Modifier ) { // TODO add loading screen diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/UiExt.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/UiExt.kt new file mode 100644 index 00000000..8626b005 --- /dev/null +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/UiExt.kt @@ -0,0 +1,28 @@ +package com.stslex.feature.match_feed.ui.components + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import kotlin.math.absoluteValue + +fun Modifier.animateItemTop( + listState: LazyListState, + key: Any?, + valueKf: Float = 0.2f +): Modifier = graphicsLayer { + val position = listState.normalizedPositionTop(key) + val value = 1 - (position.absoluteValue * valueKf) + alpha = value + scaleX = value + scaleY = value +} + +fun LazyListState.normalizedPositionTop( + key: Any? +): Float = with(layoutInfo) { + visibleItemsInfo.firstOrNull { + it.key == key + }?.let { + 1 - (it.size - it.offset.toFloat()) / it.size + } ?: 0F +} \ No newline at end of file From 80a77260a846e45b935b83b0d5ebe2775b56d762 Mon Sep 17 00:00:00 2001 From: stslex Date: Fri, 1 Dec 2023 08:45:28 +0300 Subject: [PATCH 2/2] refactor match to pager --- .../feature/match_feed/ui/MatchFeedScreen.kt | 10 ++ .../ui/components/MatchFeedScreenContent.kt | 91 +++++++------------ .../ui/components/MatchFeedScreenFilmItem.kt | 84 +++++++++++------ .../feature/match_feed/ui/components/UiExt.kt | 19 +--- .../match_feed/ui/store/MatchFeedStore.kt | 5 + .../ui/store/MatchFeedStoreComponent.kt | 8 ++ 6 files changed, 118 insertions(+), 99 deletions(-) diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt index 21d52fb2..ca6393b4 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/MatchFeedScreen.kt @@ -60,6 +60,16 @@ private fun MatchFeedScreen( films = state.films, screenState = screenState, onFilmClick = remember { { sendAction(Action.FilmClick(it)) } }, + onItemSwiped = remember { + { direction, id -> + sendAction( + Action.FilmSwiped( + direction = direction, + uuid = id + ) + ) + } + }, ) is ScreenState.Error -> MatchFeedScreenError(screenState.message) diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenContent.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenContent.kt index febe7f59..63d5143e 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenContent.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenContent.kt @@ -1,19 +1,14 @@ package com.stslex.feature.match_feed.ui.components import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,23 +18,27 @@ import com.stslex.feature.match_feed.ui.store.ScreenState import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun MatchFeedScreenContent( loadMore: () -> Unit, films: ImmutableList, screenState: ScreenState.Content, onFilmClick: (String) -> Unit, + onItemSwiped: (SwipeDirection, String) -> Unit, modifier: Modifier = Modifier, ) { - val listState = rememberLazyListState() - LaunchedEffect(listState, films.size) { + val pagerState = rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f + ) { films.size } + + LaunchedEffect(pagerState, films.size) { snapshotFlow { - listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index + pagerState.currentPage } - .filterNotNull() .distinctUntilChanged() .collectLatest { index -> if ( @@ -52,57 +51,35 @@ internal fun MatchFeedScreenContent( } } + val coroutineScope = rememberCoroutineScope() + BoxWithConstraints { val screenWidth = remember(maxWidth) { maxWidth } - LazyColumn( + + VerticalPager( + state = pagerState, modifier = modifier .fillMaxSize(), - state = listState, horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - flingBehavior = rememberSnapFlingBehavior(listState) - ) { - items( - count = films.size, - key = films.key, - contentType = { - "film" - } - ) { index -> - val film = films.getOrNull(index) - if (film != null) { - MatchFeedScreenFilmItem( - film = film, - screenWidth = screenWidth, - onFilmClick = onFilmClick, - listState = listState, - ) - } + key = { page -> + films.getOrNull(page)?.uuid ?: page } - - if (screenState is ScreenState.Content.AppendLoading) { - item( - contentType = { - "loading" + ) { page -> + val film = films.getOrNull(page) + if (film != null) { + MatchFeedScreenFilmItem( + film = film, + screenWidth = screenWidth, + onFilmClick = onFilmClick, + pagerState = pagerState, + onItemSwiped = { direction, id -> + coroutineScope.launch { + pagerState.animateScrollToPage(page + 1) + onItemSwiped(direction, id) + } } - ) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } + ) } } } } - -private val ImmutableList.key: ((Int) -> Any)? - get() = if (isEmpty()) { - null - } else { - { index -> - get(index).uuid - } - } \ No newline at end of file diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenFilmItem.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenFilmItem.kt index 2364c66b..e94deb92 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenFilmItem.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/MatchFeedScreenFilmItem.kt @@ -1,5 +1,6 @@ package com.stslex.feature.match_feed.ui.components +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box @@ -9,7 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons @@ -22,9 +23,11 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -37,6 +40,10 @@ import com.stslex.core.ui.base.onClickDelay import com.stslex.core.ui.theme.AppDimension import com.stslex.feature.match_feed.ui.model.FilmUi import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlin.math.abs enum class SwipeDirection { @@ -45,14 +52,15 @@ enum class SwipeDirection { NONE } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable internal fun MatchFeedScreenFilmItem( modifier: Modifier = Modifier, screenWidth: Dp, film: FilmUi, - listState: LazyListState, + pagerState: PagerState, onFilmClick: (String) -> Unit, + onItemSwiped: (SwipeDirection, String) -> Unit, ) { val swipeableState = rememberSwipeableState(SwipeDirection.NONE) @@ -68,35 +76,55 @@ internal fun MatchFeedScreenFilmItem( } } + LaunchedEffect(swipeableState, film.uuid) { + snapshotFlow { + swipeableState.progress.to.takeIf { + swipeableState.isAnimationRunning.not() && + swipeableState.progress.fraction == 1f + } + } + .filter { + it != SwipeDirection.NONE + } + .filterNotNull() + .distinctUntilChanged() + .collect { value -> + delay(300) + onItemSwiped(value, film.uuid) + } + } Box( modifier = modifier - .graphicsLayer { - rotationZ = 15f * progress - translationX = swipeableState.offset.value - alpha = 1f - abs(progress * 0.5f) - } - .width(posterWidth) - .height(posterHeight) - .swipeable( - state = swipeableState, - anchors = mapOf( - -screenWidth.value to SwipeDirection.LEFT, - screenWidth.value to SwipeDirection.RIGHT, - 0f to SwipeDirection.NONE, - ), - orientation = Orientation.Horizontal, - ) - .animateItemTop( - listState = listState, - key = film.uuid, - ) + .fillMaxSize(), + contentAlignment = Alignment.Center, ) { - MatchFeedScreenFilmItemContent( - modifier = Modifier.fillMaxSize(), - film = film, - onFilmClick = onFilmClick, - ) + Box( + modifier = modifier + .graphicsLayer { + rotationZ = 15f * progress + translationX = swipeableState.offset.value + alpha = 1f - abs(progress * 0.5f) + } + .width(posterWidth) + .height(posterHeight) + .swipeable( + state = swipeableState, + anchors = mapOf( + -screenWidth.value to SwipeDirection.LEFT, + screenWidth.value to SwipeDirection.RIGHT, + 0f to SwipeDirection.NONE, + ), + orientation = Orientation.Horizontal, + ) + .animateItemTop(pagerState) + ) { + MatchFeedScreenFilmItemContent( + modifier = Modifier.fillMaxSize(), + film = film, + onFilmClick = onFilmClick, + ) + } } } diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/UiExt.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/UiExt.kt index 8626b005..b926482c 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/UiExt.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/components/UiExt.kt @@ -1,28 +1,19 @@ package com.stslex.feature.match_feed.ui.components -import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.pager.PagerState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import kotlin.math.absoluteValue +@OptIn(ExperimentalFoundationApi::class) fun Modifier.animateItemTop( - listState: LazyListState, - key: Any?, + pagerState: PagerState, valueKf: Float = 0.2f ): Modifier = graphicsLayer { - val position = listState.normalizedPositionTop(key) + val position = pagerState.currentPageOffsetFraction val value = 1 - (position.absoluteValue * valueKf) alpha = value scaleX = value scaleY = value -} - -fun LazyListState.normalizedPositionTop( - key: Any? -): Float = with(layoutInfo) { - visibleItemsInfo.firstOrNull { - it.key == key - }?.let { - 1 - (it.size - it.offset.toFloat()) / it.size - } ?: 0F } \ No newline at end of file diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStore.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStore.kt index 1c42d197..0403bf3c 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStore.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStore.kt @@ -29,9 +29,14 @@ class MatchFeedStore( Action.Init -> actionInit() Action.LoadFilms -> actionLoadFilms() is Action.FilmClick -> actionFilmClick(action) + is Action.FilmSwiped -> actionFilmSwiped(action) } } + private fun actionFilmSwiped(action: Action.FilmSwiped) { + // TODO send action to backend + } + private fun actionFilmClick(action: Action.FilmClick) { navigate(Navigation.Film(action.uuid)) } diff --git a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreComponent.kt b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreComponent.kt index 341410c8..d6e5e06e 100644 --- a/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreComponent.kt +++ b/feature/match_feed/src/commonMain/kotlin/com/stslex/feature/match_feed/ui/store/MatchFeedStoreComponent.kt @@ -2,6 +2,7 @@ package com.stslex.feature.match_feed.ui.store import androidx.compose.runtime.Stable import com.stslex.core.ui.mvi.Store +import com.stslex.feature.match_feed.ui.components.SwipeDirection import com.stslex.feature.match_feed.ui.model.FilmUi import com.stslex.feature.match_feed.ui.model.MatchUi import kotlinx.collections.immutable.ImmutableList @@ -45,9 +46,16 @@ interface MatchFeedStoreComponent : Store { data object LoadFilms : Action + @Stable data class FilmClick( val uuid: String ) : Action + + @Stable + data class FilmSwiped( + val direction: SwipeDirection, + val uuid: String + ) : Action } }