MangaChapterListItem: Replace swipe action method (#9682)

Using swipe (the library) and added haptic feedback
This commit is contained in:
Ivan Iskandar 2023-07-08 21:02:20 +07:00 committed by GitHub
parent d32409bd6e
commit 8287c9d193
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 182 additions and 235 deletions

View File

@ -22,7 +22,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "eu.kanade.tachiyomi" applicationId = "eu.kanade.tachiyomi"
versionCode = 103 versionCode = 103
versionName = "0.14.6" versionName = "0.14.6"
@ -239,6 +239,7 @@ dependencies {
implementation(libs.bundles.voyager) implementation(libs.bundles.voyager)
implementation(libs.compose.materialmotion) implementation(libs.compose.materialmotion)
implementation(libs.compose.simpleicons) implementation(libs.compose.simpleicons)
implementation(libs.swipe)
// Logging // Logging
implementation(libs.logcat) implementation(libs.logcat)

View File

@ -1,20 +1,12 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.SwipeToDismiss
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.Bookmark
import androidx.compose.material.icons.filled.Circle import androidx.compose.material.icons.filled.Circle
@ -25,26 +17,28 @@ import androidx.compose.material.icons.outlined.Done
import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.FileDownloadOff import androidx.compose.material.icons.outlined.FileDownloadOff
import androidx.compose.material.icons.outlined.RemoveDone import androidx.compose.material.icons.outlined.RemoveDone
import androidx.compose.material.rememberDismissState
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -53,10 +47,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import me.saket.swipe.SwipeableActionsBox
import me.saket.swipe.rememberSwipeableActionsState
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.ReadItemAlpha
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
import kotlin.math.absoluteValue
@Composable @Composable
fun MangaChapterListItem( fun MangaChapterListItem(
@ -78,6 +75,12 @@ fun MangaChapterListItem(
onDownloadClick: ((ChapterDownloadAction) -> Unit)?, onDownloadClick: ((ChapterDownloadAction) -> Unit)?,
onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit,
) { ) {
val haptic = LocalHapticFeedback.current
val density = LocalDensity.current
val textAlpha = if (read) ReadItemAlpha else 1f
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha
// Increase touch slop of swipe action to reduce accidental trigger // Increase touch slop of swipe action to reduce accidental trigger
val configuration = LocalViewConfiguration.current val configuration = LocalViewConfiguration.current
CompositionLocalProvider( CompositionLocalProvider(
@ -85,247 +88,188 @@ fun MangaChapterListItem(
override val touchSlop: Float = configuration.touchSlop * 3f override val touchSlop: Float = configuration.touchSlop * 3f
}, },
) { ) {
val textAlpha = if (read) ReadItemAlpha else 1f val start = getSwipeAction(
val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha action = chapterSwipeStartAction,
read = read,
val chapterSwipeStartEnabled = chapterSwipeStartAction != LibraryPreferences.ChapterSwipeAction.Disabled bookmark = bookmark,
val chapterSwipeEndEnabled = chapterSwipeEndAction != LibraryPreferences.ChapterSwipeAction.Disabled downloadState = downloadStateProvider(),
background = MaterialTheme.colorScheme.primaryContainer,
val dismissState = rememberDismissState() onSwipe = { onChapterSwipe(chapterSwipeStartAction) },
val dismissDirections = remember { mutableSetOf<DismissDirection>() }
var lastDismissDirection: DismissDirection? by remember { mutableStateOf(null) }
if (lastDismissDirection == null) {
if (chapterSwipeStartEnabled) {
dismissDirections.add(DismissDirection.EndToStart)
}
if (chapterSwipeEndEnabled) {
dismissDirections.add(DismissDirection.StartToEnd)
}
}
val animateDismissContentAlpha by animateFloatAsState(
label = "animateDismissContentAlpha",
targetValue = if (lastDismissDirection != null) 1f else 0f,
animationSpec = tween(durationMillis = if (lastDismissDirection != null) 500 else 0),
finishedListener = {
lastDismissDirection = null
},
) )
val dismissContentAlpha = if (lastDismissDirection != null) animateDismissContentAlpha else 1f val end = getSwipeAction(
val backgroundColor = if (chapterSwipeEndEnabled && (dismissState.dismissDirection == DismissDirection.StartToEnd || lastDismissDirection == DismissDirection.StartToEnd)) { action = chapterSwipeEndAction,
MaterialTheme.colorScheme.primary read = read,
} else if (chapterSwipeStartEnabled && (dismissState.dismissDirection == DismissDirection.EndToStart || lastDismissDirection == DismissDirection.EndToStart)) { bookmark = bookmark,
MaterialTheme.colorScheme.primary downloadState = downloadStateProvider(),
} else { background = MaterialTheme.colorScheme.primaryContainer,
Color.Unspecified onSwipe = { onChapterSwipe(chapterSwipeEndAction) },
)
val swipeableActionsState = rememberSwipeableActionsState()
LaunchedEffect(Unit) {
// Haptic effect when swipe over threshold
val swipeActionThresholdPx = with(density) { swipeActionThreshold.toPx() }
snapshotFlow { swipeableActionsState.offset.value.absoluteValue > swipeActionThresholdPx }
.collect { if (it) haptic.performHapticFeedback(HapticFeedbackType.LongPress) }
} }
LaunchedEffect(dismissState.currentValue) { SwipeableActionsBox(
when (dismissState.currentValue) { modifier = Modifier.clipToBounds(),
DismissValue.DismissedToEnd -> { state = swipeableActionsState,
lastDismissDirection = DismissDirection.StartToEnd startActions = listOfNotNull(start),
val dismissDirectionsCopy = dismissDirections.toSet() endActions = listOfNotNull(end),
dismissDirections.clear() swipeThreshold = swipeActionThreshold,
onChapterSwipe(chapterSwipeEndAction) backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
dismissState.snapTo(DismissValue.Default) ) {
dismissDirections.addAll(dismissDirectionsCopy) Row(
} modifier = modifier
DismissValue.DismissedToStart -> { .selectedBackground(selected)
lastDismissDirection = DismissDirection.EndToStart .combinedClickable(
val dismissDirectionsCopy = dismissDirections.toSet() onClick = onClick,
dismissDirections.clear() onLongClick = onLongClick,
onChapterSwipe(chapterSwipeStartAction) )
dismissState.snapTo(DismissValue.Default) .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
dismissDirections.addAll(dismissDirectionsCopy) ) {
} Column(
DismissValue.Default -> { } modifier = Modifier.weight(1f),
} verticalArrangement = Arrangement.spacedBy(6.dp),
}
SwipeToDismiss(
state = dismissState,
directions = dismissDirections,
background = {
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor),
) { ) {
if (dismissState.dismissDirection in dismissDirections) { Row(
val downloadState = downloadStateProvider() horizontalArrangement = Arrangement.spacedBy(2.dp),
SwipeBackgroundIcon( verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(start = 16.dp)
.align(Alignment.CenterStart)
.alpha(
if (dismissState.dismissDirection == DismissDirection.StartToEnd) 1f else 0f,
),
tint = contentColorFor(backgroundColor),
swipeAction = chapterSwipeEndAction,
read = read,
bookmark = bookmark,
downloadState = downloadState,
)
SwipeBackgroundIcon(
modifier = Modifier
.padding(end = 16.dp)
.align(Alignment.CenterEnd)
.alpha(
if (dismissState.dismissDirection == DismissDirection.EndToStart) 1f else 0f,
),
tint = contentColorFor(backgroundColor),
swipeAction = chapterSwipeStartAction,
read = read,
bookmark = bookmark,
downloadState = downloadState,
)
}
}
},
dismissContent = {
Row(
modifier = modifier
.background(
MaterialTheme.colorScheme.background.copy(dismissContentAlpha),
)
.selectedBackground(selected)
.alpha(dismissContentAlpha)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp),
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(6.dp),
) { ) {
Row( var textHeight by remember { mutableIntStateOf(0) }
horizontalArrangement = Arrangement.spacedBy(2.dp), if (!read) {
verticalAlignment = Alignment.CenterVertically, Icon(
) { imageVector = Icons.Filled.Circle,
var textHeight by remember { mutableIntStateOf(0) } contentDescription = stringResource(R.string.unread),
if (!read) { modifier = Modifier
Icon( .height(8.dp)
imageVector = Icons.Filled.Circle, .padding(end = 4.dp),
contentDescription = stringResource(R.string.unread), tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.height(8.dp)
.padding(end = 4.dp),
tint = MaterialTheme.colorScheme.primary,
)
}
if (bookmark) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = textAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
) )
} }
if (bookmark) {
Icon(
imageVector = Icons.Filled.Bookmark,
contentDescription = stringResource(R.string.action_filter_bookmarked),
modifier = Modifier
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
tint = MaterialTheme.colorScheme.primary,
)
}
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = textAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
onTextLayout = { textHeight = it.size.height },
)
}
Row { Row {
ProvideTextStyle( ProvideTextStyle(
value = MaterialTheme.typography.bodyMedium.copy( value = MaterialTheme.typography.bodyMedium.copy(
fontSize = 12.sp, fontSize = 12.sp,
color = LocalContentColor.current.copy(alpha = textSubtitleAlpha), color = LocalContentColor.current.copy(alpha = textSubtitleAlpha),
), ),
) { ) {
if (date != null) { if (date != null) {
Text( Text(
text = date, text = date,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
if (readProgress != null || scanlator != null) DotSeparatorText() if (readProgress != null || scanlator != null) DotSeparatorText()
} }
if (readProgress != null) { if (readProgress != null) {
Text( Text(
text = readProgress, text = readProgress,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.alpha(ReadItemAlpha), modifier = Modifier.alpha(ReadItemAlpha),
) )
if (scanlator != null) DotSeparatorText() if (scanlator != null) DotSeparatorText()
} }
if (scanlator != null) { if (scanlator != null) {
Text( Text(
text = scanlator, text = scanlator,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
}
} }
} }
} }
if (onDownloadClick != null) {
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = onDownloadClick,
)
}
} }
},
) if (onDownloadClick != null) {
ChapterDownloadIndicator(
enabled = downloadIndicatorEnabled,
modifier = Modifier.padding(start = 4.dp),
downloadStateProvider = downloadStateProvider,
downloadProgressProvider = downloadProgressProvider,
onClick = onDownloadClick,
)
}
}
}
} }
} }
@Composable private fun getSwipeAction(
private fun SwipeBackgroundIcon( action: LibraryPreferences.ChapterSwipeAction,
modifier: Modifier = Modifier,
tint: Color,
swipeAction: LibraryPreferences.ChapterSwipeAction,
read: Boolean, read: Boolean,
bookmark: Boolean, bookmark: Boolean,
downloadState: Download.State, downloadState: Download.State,
) { background: Color,
val imageVector = when (swipeAction) { onSwipe: () -> Unit,
LibraryPreferences.ChapterSwipeAction.ToggleRead -> { ): me.saket.swipe.SwipeAction? {
if (!read) { return when (action) {
Icons.Outlined.Done LibraryPreferences.ChapterSwipeAction.ToggleRead -> SwipeAction(
} else { icon = if (!read) Icons.Outlined.Done else Icons.Outlined.RemoveDone,
Icons.Outlined.RemoveDone background = background,
} isUndo = read,
} onSwipe = onSwipe,
LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> { )
if (!bookmark) { LibraryPreferences.ChapterSwipeAction.ToggleBookmark -> SwipeAction(
Icons.Outlined.BookmarkAdd icon = if (!bookmark) Icons.Outlined.BookmarkAdd else Icons.Outlined.BookmarkRemove,
} else { background = background,
Icons.Outlined.BookmarkRemove isUndo = bookmark,
} onSwipe = onSwipe,
} )
LibraryPreferences.ChapterSwipeAction.Download -> { LibraryPreferences.ChapterSwipeAction.Download -> SwipeAction(
when (downloadState) { icon = when (downloadState) {
Download.State.NOT_DOWNLOADED, Download.State.NOT_DOWNLOADED, Download.State.ERROR -> Icons.Outlined.Download
Download.State.ERROR, Download.State.QUEUE, Download.State.DOWNLOADING -> Icons.Outlined.FileDownloadOff
-> { Icons.Outlined.Download } Download.State.DOWNLOADED -> Icons.Outlined.Delete
Download.State.QUEUE, },
Download.State.DOWNLOADING, background = background,
-> { Icons.Outlined.FileDownloadOff } onSwipe = onSwipe,
Download.State.DOWNLOADED -> { Icons.Outlined.Delete } )
}
}
LibraryPreferences.ChapterSwipeAction.Disabled -> null LibraryPreferences.ChapterSwipeAction.Disabled -> null
} }
imageVector?.let {
Icon(
modifier = modifier,
imageVector = imageVector,
tint = tint,
contentDescription = null,
)
}
} }
private fun SwipeAction(
onSwipe: () -> Unit,
icon: ImageVector,
background: Color,
isUndo: Boolean = false,
): me.saket.swipe.SwipeAction {
return me.saket.swipe.SwipeAction(
icon = {
Icon(
modifier = Modifier.padding(16.dp),
imageVector = icon,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
)
},
background = background,
onSwipe = onSwipe,
isUndo = isUndo,
)
}
private val swipeActionThreshold = 56.dp

View File

@ -61,6 +61,8 @@ insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.0.3" compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.0.3"
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0" compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
swipe = "me.saket.swipe:swipe:1.2.0"
logcat = "com.squareup.logcat:logcat:0.1" logcat = "com.squareup.logcat:logcat:0.1"
acra-http = "ch.acra:acra-http:5.10.1" acra-http = "ch.acra:acra-http:5.10.1"