mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-24 04:30:54 -05:00
swipe on list item cards
This commit is contained in:
@@ -133,6 +133,8 @@ dependencies {
|
||||
def markdown = "0.2.1"
|
||||
implementation "org.jetbrains:markdown:$markdown"
|
||||
|
||||
implementation 'de.charlex.compose:revealswipe:1.0.0'
|
||||
|
||||
// testing
|
||||
def junit = "4.13.2"
|
||||
def androidx_junit = "1.1.3"
|
||||
|
||||
@@ -1,25 +1,43 @@
|
||||
package com.owenlejeune.tvtime.ui.components
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.magnifier
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.FractionalThreshold
|
||||
import androidx.compose.material.ThresholdConfig
|
||||
import androidx.compose.material.rememberSwipeableState
|
||||
import androidx.compose.material.swipeable
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.owenlejeune.tvtime.R
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ContentCard(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -31,10 +49,10 @@ fun ContentCard(
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
.wrapContentHeight()
|
||||
.background(color = backgroundColor),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
backgroundColor = backgroundColor,
|
||||
elevation = 8.dp
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
title?.let {
|
||||
@@ -50,6 +68,7 @@ fun ContentCard(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ExpandableContentCard(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -66,6 +85,7 @@ fun ExpandableContentCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.background(color = backgroundColor)
|
||||
.animateContentSize(
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
@@ -73,8 +93,7 @@ fun ExpandableContentCard(
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
backgroundColor = backgroundColor,
|
||||
elevation = 8.dp
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
title()
|
||||
@@ -95,6 +114,7 @@ fun ExpandableContentCard(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LazyListContentCard(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -104,10 +124,10 @@ fun LazyListContentCard(
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.background(color = backgroundColor),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
backgroundColor = backgroundColor,
|
||||
elevation = 8.dp
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -129,6 +149,7 @@ fun LazyListContentCard(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListContentCard(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -138,10 +159,10 @@ fun ListContentCard(
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.background(color = backgroundColor),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
backgroundColor = backgroundColor,
|
||||
elevation = 8.dp
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -203,3 +224,257 @@ fun TwoLineImageTextCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun SwipeableActionCard(
|
||||
modifier: Modifier = Modifier,
|
||||
leftSwipeCard: (@Composable () -> Unit)? = null,
|
||||
rightSwipeCard: (@Composable () -> Unit)? = null,
|
||||
leftSwiped: () -> Unit = {},
|
||||
rightSwiped: () -> Unit = {},
|
||||
animationSpec: AnimationSpec<Float> = tween(250),
|
||||
velocityThreshold: Dp = 125.dp,
|
||||
// shape: Shape = RectangleShape,
|
||||
mainContent: @Composable () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val thresholds = { _: SwipeCardState, _: SwipeCardState ->
|
||||
FractionalThreshold(0.6f)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier//.clip(shape)
|
||||
) {
|
||||
val swipeableState = rememberSwipeableState(
|
||||
initialValue = SwipeCardState.DEFAULT,
|
||||
animationSpec = animationSpec
|
||||
)
|
||||
|
||||
val swipeLeftCardVisible = remember { mutableStateOf(false) }
|
||||
val swipeEnabled = remember { mutableStateOf(true) }
|
||||
val maxWidthInPx = with(LocalDensity.current) {
|
||||
LocalConfiguration.current.screenWidthDp.dp.toPx()
|
||||
}
|
||||
|
||||
val anchors = hashMapOf(0f to SwipeCardState.DEFAULT)
|
||||
leftSwipeCard?.let { anchors[-maxWidthInPx] = SwipeCardState.LEFT }
|
||||
rightSwipeCard?.let { anchors[maxWidthInPx] = SwipeCardState.RIGHT }
|
||||
|
||||
Surface(
|
||||
color = Color.Transparent,
|
||||
content = if (swipeLeftCardVisible.value) {
|
||||
leftSwipeCard
|
||||
} else {
|
||||
rightSwipeCard
|
||||
} ?: {}
|
||||
)
|
||||
|
||||
Surface(
|
||||
color = Color.Transparent,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.offset {
|
||||
var offset = swipeableState.offset.value.roundToInt()
|
||||
if (offset < 0 && leftSwipeCard == null) offset = 0
|
||||
if (offset > 0 && rightSwipeCard == null) offset = 0
|
||||
IntOffset(offset, 0)
|
||||
}
|
||||
.swipeable(
|
||||
state = swipeableState,
|
||||
anchors = anchors,
|
||||
orientation = Orientation.Horizontal,
|
||||
enabled = swipeEnabled.value,
|
||||
thresholds = thresholds,
|
||||
velocityThreshold = velocityThreshold
|
||||
)
|
||||
) {
|
||||
val resetStateToDefault = {
|
||||
coroutineScope.launch {
|
||||
swipeEnabled.value = false
|
||||
swipeableState.animateTo(SwipeCardState.DEFAULT)
|
||||
swipeEnabled.value = true
|
||||
}
|
||||
}
|
||||
when {
|
||||
swipeableState.currentValue == SwipeCardState.LEFT && !swipeableState.isAnimationRunning -> {
|
||||
leftSwiped()
|
||||
LaunchedEffect(Unit) {
|
||||
resetStateToDefault()
|
||||
}
|
||||
}
|
||||
swipeableState.currentValue == SwipeCardState.RIGHT && !swipeableState.isAnimationRunning -> {
|
||||
rightSwiped()
|
||||
LaunchedEffect(Unit) {
|
||||
resetStateToDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
swipeLeftCardVisible.value = swipeableState.offset.value <= 0
|
||||
|
||||
mainContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DraggableCard(
|
||||
modifier: Modifier = Modifier,
|
||||
cardOffset: Float,
|
||||
isRevealed: Boolean,
|
||||
cardElevation: CardElevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape: Shape = RectangleShape,
|
||||
onExpand: () -> Unit = {},
|
||||
onCollapse: () -> Unit = {},
|
||||
backgroundContent: @Composable () -> Unit,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val animationDuration = 500
|
||||
val velicityThreshold = 125.dp
|
||||
|
||||
val thresholds: (from: SwipeCardState, to: SwipeCardState) -> ThresholdConfig = { _, _ ->
|
||||
FractionalThreshold(0.6f)
|
||||
}
|
||||
|
||||
val swipeableState = rememberSwipeableState(
|
||||
initialValue = SwipeCardState.DEFAULT,
|
||||
animationSpec = tween(animationDuration)
|
||||
)
|
||||
|
||||
val maxWidthPx = with(LocalDensity.current) {
|
||||
LocalConfiguration.current.screenWidthDp.dp.toPx()
|
||||
}
|
||||
|
||||
val anchors = hashMapOf(0f to SwipeCardState.DEFAULT)
|
||||
anchors[-maxWidthPx] = SwipeCardState.LEFT
|
||||
|
||||
val swipeEnabled = remember { mutableStateOf(true) }
|
||||
|
||||
Box (
|
||||
modifier = Modifier.clip(shape = shape)
|
||||
) {
|
||||
backgroundContent()
|
||||
|
||||
Card(
|
||||
shape = shape,
|
||||
elevation = cardElevation,
|
||||
modifier = modifier
|
||||
.background(color = backgroundColor)
|
||||
.offset {
|
||||
var offset = swipeableState.offset.value.roundToInt()
|
||||
if (offset < 0) offset = 0
|
||||
IntOffset(offset, 0)
|
||||
}
|
||||
.swipeable(
|
||||
state = swipeableState,
|
||||
anchors = anchors,
|
||||
orientation = Orientation.Horizontal,
|
||||
enabled = swipeEnabled.value,
|
||||
thresholds = thresholds,
|
||||
velocityThreshold = velicityThreshold
|
||||
)
|
||||
) {
|
||||
if (swipeableState.currentValue == SwipeCardState.LEFT && !swipeableState.isAnimationRunning) {
|
||||
onExpand()
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
coroutineScope.launch {
|
||||
swipeEnabled.value = false
|
||||
swipeableState.animateTo(SwipeCardState.DEFAULT)
|
||||
swipeEnabled.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
// DraggableCardInternal(
|
||||
// modifier = modifier,
|
||||
// cardOffset = cardOffset,
|
||||
// isRevealed = isRevealed,
|
||||
// cardElevation = cardElevation,
|
||||
// backgroundColor = backgroundColor,
|
||||
// shape = shape,
|
||||
// onExpand = onExpand,
|
||||
// onCollapse = onCollapse,
|
||||
// content = content
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
private enum class SwipeCardState {
|
||||
DEFAULT,
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@SuppressLint("UnusedTransitionTargetStateParameter")
|
||||
@Composable
|
||||
fun DraggableCardInternal(
|
||||
modifier: Modifier = Modifier,
|
||||
cardOffset: Float,
|
||||
isRevealed: Boolean,
|
||||
cardElevation: CardElevation = CardDefaults.cardElevation(defaultElevation = 10.dp),
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape: Shape = RectangleShape,
|
||||
onExpand: () -> Unit = {},
|
||||
onCollapse: () -> Unit = {},
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val animationDuration = 500
|
||||
|
||||
val swipeableState = rememberSwipeableState(
|
||||
initialValue = SwipeCardState.DEFAULT,
|
||||
animationSpec = tween(animationDuration)
|
||||
)
|
||||
|
||||
val maxWidthPx = with(LocalDensity.current) {
|
||||
LocalConfiguration.current.screenWidthDp.dp.toPx()
|
||||
}
|
||||
|
||||
val anchors = hashMapOf(0f to SwipeCardState.DEFAULT)
|
||||
anchors[-maxWidthPx] = SwipeCardState.LEFT
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.background(color = backgroundColor),
|
||||
shape = shape,
|
||||
elevation = cardElevation
|
||||
){}
|
||||
// val animationDuration = 500
|
||||
// val minDragAmount = 5
|
||||
//
|
||||
// val transitionState = remember {
|
||||
// MutableTransitionState(isRevealed).apply {
|
||||
// targetState = !isRevealed
|
||||
// }
|
||||
// }
|
||||
// val transition = updateTransition(targetState = transitionState, "cardTransition")
|
||||
// val offsetTransition by transition.animateFloat(
|
||||
// label = "cardOffsetTransition",
|
||||
// transitionSpec = { tween(durationMillis = animationDuration) },
|
||||
// targetValueByState = { if (isRevealed) cardOffset else 0f },
|
||||
// )
|
||||
//
|
||||
// Card(
|
||||
// modifier = modifier
|
||||
// .background(color = backgroundColor)
|
||||
// .offset { IntOffset(offsetTransition.roundToInt(), 0) }
|
||||
// .pointerInput(Unit) {
|
||||
// detectHorizontalDragGestures { _, dragAmount ->
|
||||
// when {
|
||||
// dragAmount >= minDragAmount -> onExpand()
|
||||
// dragAmount < -minDragAmount -> onCollapse()
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// shape = shape,
|
||||
// content = content
|
||||
// )
|
||||
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
package com.owenlejeune.tvtime.ui.screens.main
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
@@ -8,14 +7,12 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
@@ -44,12 +41,11 @@ import com.owenlejeune.tvtime.extensions.WindowSizeClass
|
||||
import com.owenlejeune.tvtime.extensions.unlessEmpty
|
||||
import com.owenlejeune.tvtime.preferences.AppPreferences
|
||||
import com.owenlejeune.tvtime.ui.navigation.MainNavItem
|
||||
import com.owenlejeune.tvtime.ui.theme.FavoriteSelected
|
||||
import com.owenlejeune.tvtime.ui.theme.RatingSelected
|
||||
import com.owenlejeune.tvtime.ui.theme.WatchlistSelected
|
||||
import com.owenlejeune.tvtime.ui.theme.actionButtonColor
|
||||
import com.owenlejeune.tvtime.ui.theme.*
|
||||
import com.owenlejeune.tvtime.utils.SessionManager
|
||||
import com.owenlejeune.tvtime.utils.TmdbUtils
|
||||
import de.charlex.compose.RevealDirection
|
||||
import de.charlex.compose.RevealSwipe
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -252,11 +248,29 @@ private fun RowScope.OverviewStatCard(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun ListItemView(
|
||||
appNavController: NavController,
|
||||
listItem: ListItem
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
RevealSwipe (
|
||||
directions = setOf(RevealDirection.EndToStart),
|
||||
hiddenContentEnd = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(horizontal = 15.dp),
|
||||
onClick = { Toast.makeText(context, "Remove from list", Toast.LENGTH_SHORT).show() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = stringResource(id = R.string.remove_from_list_cd),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
},
|
||||
backgroundCardEndColor = SwipeRemoveBackground
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
@@ -342,9 +356,8 @@ private fun ListItemView(
|
||||
}
|
||||
|
||||
|
||||
val rating = SessionManager.currentSession?.getRatingForId(listItem.id, listItem.mediaType) ?: 0f
|
||||
RatingView(
|
||||
progress = rating / 10f,
|
||||
progress = listItem.voteAverage / 10f,
|
||||
modifier = Modifier
|
||||
.constrainAs(ratingView) {
|
||||
end.linkTo(parent.end)
|
||||
@@ -356,30 +369,6 @@ private fun ListItemView(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeleteButton(
|
||||
modifier: Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(CircleShape)
|
||||
.size(48.dp)
|
||||
.background(color = MaterialTheme.colorScheme.actionButtonColor)
|
||||
.clickable(
|
||||
onClick = {
|
||||
}
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.align(Alignment.Center),
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = stringResource(id = R.string.remove_from_list_cd),
|
||||
tint = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -55,5 +55,7 @@ val WatchlistSelected = Color(0xFFBF3F39)
|
||||
val RatingSelected = Color(0xFFE7C343)
|
||||
val FavoriteSelected = Color(0xFFDD57B2)
|
||||
|
||||
val SwipeRemoveBackground = WatchlistSelected
|
||||
|
||||
val LightActionButton = Color(0xFF48C9B0)
|
||||
val DarkActionButton = Color(0xFF17A589)
|
||||
Reference in New Issue
Block a user