Add full predictive back support (#2085)
Co-authored-by: p (cherry picked from commit c12bdbae8e7bc14da8966e45a3c450913e32129f) # Conflicts: # app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt # app/src/main/java/eu/kanade/tachiyomi/ui/home/HomeScreen.kt
This commit is contained in:
parent
5c7b3c6c3b
commit
ef3d9626c1
@ -1,10 +1,9 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.compose.animation.SizeTransform
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@ -28,20 +27,14 @@ fun NavigatorAdaptiveSheet(
|
|||||||
screen = screen,
|
screen = screen,
|
||||||
content = { sheetNavigator ->
|
content = { sheetNavigator ->
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
|
||||||
) {
|
) {
|
||||||
ScreenTransition(
|
ScreenTransition(
|
||||||
navigator = sheetNavigator,
|
navigator = sheetNavigator,
|
||||||
transition = {
|
enterTransition = { fadeIn(animationSpec = tween(220, delayMillis = 90)) },
|
||||||
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
|
exitTransition = { fadeOut(animationSpec = tween(90)) },
|
||||||
fadeOut(animationSpec = tween(90))
|
sizeTransform = { SizeTransform() },
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
BackHandler(
|
|
||||||
enabled = sheetNavigator.size > 1,
|
|
||||||
onBack = sheetNavigator::pop,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,10 +72,10 @@ fun AdaptiveSheet(
|
|||||||
properties = dialogProperties,
|
properties = dialogProperties,
|
||||||
) {
|
) {
|
||||||
AdaptiveSheetImpl(
|
AdaptiveSheetImpl(
|
||||||
modifier = modifier,
|
|
||||||
isTabletUi = isTabletUi,
|
isTabletUi = isTabletUi,
|
||||||
enableSwipeDismiss = enableSwipeDismiss,
|
enableSwipeDismiss = enableSwipeDismiss,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
@ -349,13 +349,9 @@ private fun MangaScreenSmallImpl(
|
|||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
BackHandler(onBack = {
|
BackHandler(enabled = isAnySelected) {
|
||||||
if (isAnySelected) {
|
onAllChapterSelected(false)
|
||||||
onAllChapterSelected(false)
|
}
|
||||||
} else {
|
|
||||||
navigateUp()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -680,13 +676,9 @@ fun MangaScreenLargeImpl(
|
|||||||
|
|
||||||
val chapterListState = rememberLazyListState()
|
val chapterListState = rememberLazyListState()
|
||||||
|
|
||||||
BackHandler(onBack = {
|
BackHandler(enabled = isAnySelected) {
|
||||||
if (isAnySelected) {
|
onAllChapterSelected(false)
|
||||||
onAllChapterSelected(false)
|
}
|
||||||
} else {
|
|
||||||
navigateUp()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
|
@ -3,6 +3,9 @@ package eu.kanade.presentation.manga.components
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -25,18 +28,22 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import coil3.asDrawable
|
import coil3.asDrawable
|
||||||
import coil3.imageLoader
|
import coil3.imageLoader
|
||||||
@ -49,11 +56,14 @@ import eu.kanade.presentation.components.DropdownMenu
|
|||||||
import eu.kanade.presentation.manga.EditCoverAction
|
import eu.kanade.presentation.manga.EditCoverAction
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import soup.compose.material.motion.MotionConstants
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.util.PredictiveBack
|
||||||
import tachiyomi.presentation.core.util.clickableNoIndication
|
import tachiyomi.presentation.core.util.clickableNoIndication
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaCoverDialog(
|
fun MangaCoverDialog(
|
||||||
@ -152,10 +162,32 @@ fun MangaCoverDialog(
|
|||||||
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
val statusBarPaddingPx = with(LocalDensity.current) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||||
|
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
PredictiveBackHandler { progress ->
|
||||||
|
try {
|
||||||
|
progress.collect { backEvent ->
|
||||||
|
scale = lerp(1f, 0.8f, PredictiveBack.transform(backEvent.progress))
|
||||||
|
}
|
||||||
|
onDismissRequest()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clickableNoIndication(onClick = onDismissRequest),
|
.clickableNoIndication(onClick = onDismissRequest)
|
||||||
|
.graphicsLayer {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = {
|
factory = {
|
||||||
@ -172,20 +204,20 @@ fun MangaCoverDialog(
|
|||||||
.memoryCachePolicy(CachePolicy.DISABLED)
|
.memoryCachePolicy(CachePolicy.DISABLED)
|
||||||
.target { image ->
|
.target { image ->
|
||||||
val drawable = image.asDrawable(view.context.resources)
|
val drawable = image.asDrawable(view.context.resources)
|
||||||
|
|
||||||
// Copy bitmap in case it came from memory cache
|
// Copy bitmap in case it came from memory cache
|
||||||
// Because SSIV needs to thoroughly read the image
|
// Because SSIV needs to thoroughly read the image
|
||||||
val copy = (drawable as? BitmapDrawable)?.let {
|
val copy = (drawable as? BitmapDrawable)
|
||||||
val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
?.bitmap
|
||||||
Bitmap.Config.HARDWARE
|
?.copy(
|
||||||
} else {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
Bitmap.Config.ARGB_8888
|
Bitmap.Config.HARDWARE
|
||||||
}
|
} else {
|
||||||
BitmapDrawable(
|
Bitmap.Config.ARGB_8888
|
||||||
view.context.resources,
|
},
|
||||||
it.bitmap.copy(config, false),
|
false,
|
||||||
)
|
)
|
||||||
} ?: drawable
|
?.toDrawable(view.context.resources)
|
||||||
|
?: drawable
|
||||||
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
|
view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500))
|
||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
|
@ -42,7 +42,9 @@ fun OnboardingScreen(
|
|||||||
}
|
}
|
||||||
val isLastStep = currentStep == steps.lastIndex
|
val isLastStep = currentStep == steps.lastIndex
|
||||||
|
|
||||||
BackHandler(enabled = currentStep != 0, onBack = { currentStep-- })
|
BackHandler(enabled = currentStep != 0) {
|
||||||
|
currentStep--
|
||||||
|
}
|
||||||
|
|
||||||
InfoScreen(
|
InfoScreen(
|
||||||
icon = Icons.Outlined.RocketLaunch,
|
icon = Icons.Outlined.RocketLaunch,
|
||||||
|
@ -60,7 +60,9 @@ fun UpdateScreen(
|
|||||||
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
onUpdateSelected: (UpdatesItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onOpenChapter: (UpdatesItem) -> Unit,
|
onOpenChapter: (UpdatesItem) -> Unit,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
BackHandler(enabled = state.selectionMode) {
|
||||||
|
onSelectAll(false)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
|
@ -1,12 +1,46 @@
|
|||||||
package eu.kanade.presentation.util
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.activity.BackEventCompat
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.animation.core.SeekableTransitionState
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.rememberTransition
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.ScreenModelStore
|
import cafe.adriel.voyager.core.model.ScreenModelStore
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
@ -15,18 +49,28 @@ import cafe.adriel.voyager.core.screen.uniqueScreenKey
|
|||||||
import cafe.adriel.voyager.core.stack.StackEvent
|
import cafe.adriel.voyager.core.stack.StackEvent
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
import cafe.adriel.voyager.transitions.ScreenTransitionContent
|
||||||
|
import eu.kanade.tachiyomi.util.view.getWindowRadius
|
||||||
import kotlinx.coroutines.CoroutineName
|
import kotlinx.coroutines.CoroutineName
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.dropWhile
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import soup.compose.material.motion.animation.materialSharedAxisX
|
import soup.compose.material.motion.animation.materialSharedAxisXIn
|
||||||
|
import soup.compose.material.motion.animation.materialSharedAxisXOut
|
||||||
import soup.compose.material.motion.animation.rememberSlideDistance
|
import soup.compose.material.motion.animation.rememberSlideDistance
|
||||||
|
import tachiyomi.presentation.core.util.PredictiveBack
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For invoking back press to the parent activity
|
* For invoking back press to the parent activity
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("ComposeCompositionLocalUsage")
|
||||||
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
|
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
|
||||||
|
|
||||||
interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
|
interface Tab : cafe.adriel.voyager.navigator.tab.Tab {
|
||||||
@ -59,39 +103,278 @@ interface AssistContentScreen {
|
|||||||
fun onProvideAssistUrl(): String?
|
fun onProvideAssistUrl(): String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(InternalVoyagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultNavigatorScreenTransition(
|
fun DefaultNavigatorScreenTransition(
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val slideDistance = rememberSlideDistance()
|
val screenCandidatesToDispose = rememberSaveable(saver = screenCandidatesToDisposeSaver()) {
|
||||||
|
mutableStateOf(emptySet())
|
||||||
|
}
|
||||||
|
val currentScreens = navigator.items
|
||||||
|
DisposableEffect(currentScreens) {
|
||||||
|
onDispose {
|
||||||
|
val newScreenKeys = navigator.items.map { it.key }
|
||||||
|
screenCandidatesToDispose.value += currentScreens.filter { it.key !in newScreenKeys }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val slideDistance = rememberSlideDistance(slideDistance = 30.dp)
|
||||||
ScreenTransition(
|
ScreenTransition(
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
transition = {
|
|
||||||
materialSharedAxisX(
|
|
||||||
forward = navigator.lastEvent != StackEvent.Pop,
|
|
||||||
slideDistance = slideDistance,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
enterTransition = {
|
||||||
|
if (it == SwipeEdge.Right) {
|
||||||
|
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
|
||||||
|
} else {
|
||||||
|
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
if (it == SwipeEdge.Right) {
|
||||||
|
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
|
||||||
|
} else {
|
||||||
|
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
if (it == SwipeEdge.Right) {
|
||||||
|
materialSharedAxisXIn(forward = true, slideDistance = slideDistance)
|
||||||
|
} else {
|
||||||
|
materialSharedAxisXIn(forward = false, slideDistance = slideDistance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
if (it == SwipeEdge.Right) {
|
||||||
|
materialSharedAxisXOut(forward = true, slideDistance = slideDistance)
|
||||||
|
} else {
|
||||||
|
materialSharedAxisXOut(forward = false, slideDistance = slideDistance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = { screen ->
|
||||||
|
if (this.transition.targetState == this.transition.currentState) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
val newScreens = navigator.items.map { it.key }
|
||||||
|
val screensToDispose = screenCandidatesToDispose.value.filterNot { it.key in newScreens }
|
||||||
|
if (screensToDispose.isNotEmpty()) {
|
||||||
|
screensToDispose.forEach { navigator.dispose(it) }
|
||||||
|
navigator.clearEvent()
|
||||||
|
}
|
||||||
|
screenCandidatesToDispose.value = emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.Content()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SwipeEdge {
|
||||||
|
Unknown,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class AnimationType {
|
||||||
|
Pop,
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScreenTransition(
|
fun ScreenTransition(
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
transition: AnimatedContentTransitionScope<Screen>.() -> ContentTransform,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
enterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = { fadeIn() },
|
||||||
|
exitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = { fadeOut() },
|
||||||
|
popEnterTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> EnterTransition = enterTransition,
|
||||||
|
popExitTransition: AnimatedContentTransitionScope<Screen>.(SwipeEdge) -> ExitTransition = exitTransition,
|
||||||
|
sizeTransform: (AnimatedContentTransitionScope<Screen>.() -> SizeTransform?)? = null,
|
||||||
|
flingAnimationSpec: () -> AnimationSpec<Float> = { spring(stiffness = Spring.StiffnessLow) },
|
||||||
content: ScreenTransitionContent = { it.Content() },
|
content: ScreenTransitionContent = { it.Content() },
|
||||||
) {
|
) {
|
||||||
AnimatedContent(
|
val view = LocalView.current
|
||||||
targetState = navigator.lastItem,
|
val viewConfig = LocalViewConfiguration.current
|
||||||
transitionSpec = transition,
|
val scope = rememberCoroutineScope()
|
||||||
|
val state = remember {
|
||||||
|
ScreenTransitionState(
|
||||||
|
navigator = navigator,
|
||||||
|
scope = scope,
|
||||||
|
flingAnimationSpec = flingAnimationSpec(),
|
||||||
|
windowCornerRadius = view.getWindowRadius().toFloat(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val transitionState = remember { SeekableTransitionState(navigator.lastItem) }
|
||||||
|
val transition = rememberTransition(transitionState = transitionState)
|
||||||
|
|
||||||
|
if (state.isPredictiveBack || state.isAnimating) {
|
||||||
|
LaunchedEffect(state.progress) {
|
||||||
|
if (!state.isPredictiveBack) return@LaunchedEffect
|
||||||
|
val previousEntry = navigator.items.getOrNull(navigator.size - 2)
|
||||||
|
if (previousEntry != null) {
|
||||||
|
transitionState.seekTo(fraction = state.progress, targetState = previousEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(navigator) {
|
||||||
|
snapshotFlow { navigator.lastItem }
|
||||||
|
.collect {
|
||||||
|
state.cancelCancelAnimation()
|
||||||
|
if (it != transitionState.currentState) {
|
||||||
|
transitionState.animateTo(it)
|
||||||
|
} else {
|
||||||
|
transitionState.snapTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PredictiveBackHandler(enabled = navigator.canPop) { backEvent ->
|
||||||
|
state.cancelCancelAnimation()
|
||||||
|
var startOffset: Offset? = null
|
||||||
|
backEvent
|
||||||
|
.dropWhile {
|
||||||
|
if (startOffset == null) startOffset = Offset(it.touchX, it.touchY)
|
||||||
|
if (state.isAnimating) return@dropWhile true
|
||||||
|
// Touch slop check
|
||||||
|
val diff = Offset(it.touchX, it.touchY) - startOffset!!
|
||||||
|
diff.x.absoluteValue < viewConfig.touchSlop && diff.y.absoluteValue < viewConfig.touchSlop
|
||||||
|
}
|
||||||
|
.onCompletion {
|
||||||
|
if (it == null) {
|
||||||
|
state.finish()
|
||||||
|
} else {
|
||||||
|
state.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.collect {
|
||||||
|
state.setPredictiveBackProgress(
|
||||||
|
progress = it.progress,
|
||||||
|
swipeEdge = when (it.swipeEdge) {
|
||||||
|
BackEventCompat.EDGE_LEFT -> SwipeEdge.Left
|
||||||
|
BackEventCompat.EDGE_RIGHT -> SwipeEdge.Right
|
||||||
|
else -> SwipeEdge.Unknown
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.AnimatedContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
label = "transition",
|
transitionSpec = {
|
||||||
) { screen ->
|
val pop = navigator.lastEvent == StackEvent.Pop || state.isPredictiveBack
|
||||||
navigator.saveableState("transition", screen) {
|
ContentTransform(
|
||||||
content(screen)
|
targetContentEnter = if (pop) {
|
||||||
|
popEnterTransition(state.swipeEdge)
|
||||||
|
} else {
|
||||||
|
enterTransition(state.swipeEdge)
|
||||||
|
},
|
||||||
|
initialContentExit = if (pop) {
|
||||||
|
popExitTransition(state.swipeEdge)
|
||||||
|
} else {
|
||||||
|
exitTransition(state.swipeEdge)
|
||||||
|
},
|
||||||
|
targetContentZIndex = if (pop) 0f else 1f,
|
||||||
|
sizeTransform = sizeTransform?.invoke(this),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentKey = { it.key },
|
||||||
|
) {
|
||||||
|
navigator.saveableState("transition", it) {
|
||||||
|
content(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
private class ScreenTransitionState(
|
||||||
|
private val navigator: Navigator,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val flingAnimationSpec: AnimationSpec<Float>,
|
||||||
|
windowCornerRadius: Float,
|
||||||
|
) {
|
||||||
|
var isPredictiveBack: Boolean by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var progress: Float by mutableFloatStateOf(0f)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var swipeEdge: SwipeEdge by mutableStateOf(SwipeEdge.Unknown)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var animationJob: Pair<Job, AnimationType>? by mutableStateOf(null)
|
||||||
|
|
||||||
|
val isAnimating: Boolean
|
||||||
|
get() = animationJob?.first?.isActive == true
|
||||||
|
|
||||||
|
val windowCornerShape = RoundedCornerShape(windowCornerRadius)
|
||||||
|
|
||||||
|
private fun reset() {
|
||||||
|
this.isPredictiveBack = false
|
||||||
|
this.swipeEdge = SwipeEdge.Unknown
|
||||||
|
this.animationJob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPredictiveBackProgress(progress: Float, swipeEdge: SwipeEdge) {
|
||||||
|
this.progress = lerp(0f, 0.65f, PredictiveBack.transform(progress))
|
||||||
|
this.swipeEdge = swipeEdge
|
||||||
|
this.isPredictiveBack = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finish() {
|
||||||
|
if (!isPredictiveBack) {
|
||||||
|
navigator.pop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
animationJob = scope.launch {
|
||||||
|
try {
|
||||||
|
animate(
|
||||||
|
initialValue = progress,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = flingAnimationSpec,
|
||||||
|
block = { i, _ -> progress = i },
|
||||||
|
)
|
||||||
|
navigator.pop()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// Cancelled
|
||||||
|
progress = 0f
|
||||||
|
} finally {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
} to AnimationType.Pop
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
if (!isPredictiveBack) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
animationJob = scope.launch {
|
||||||
|
try {
|
||||||
|
animate(
|
||||||
|
initialValue = progress,
|
||||||
|
targetValue = 0f,
|
||||||
|
animationSpec = flingAnimationSpec,
|
||||||
|
block = { i, _ -> progress = i },
|
||||||
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// Cancelled
|
||||||
|
progress = 1f
|
||||||
|
} finally {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
} to AnimationType.Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelCancelAnimation() {
|
||||||
|
if (animationJob?.second == AnimationType.Cancel) {
|
||||||
|
animationJob?.first?.cancel()
|
||||||
|
animationJob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun screenCandidatesToDisposeSaver(): Saver<MutableState<Set<Screen>>, List<Screen>> {
|
||||||
|
return Saver(
|
||||||
|
save = { it.value.toList() },
|
||||||
|
restore = { mutableStateOf(it.toSet()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.ui.home
|
package eu.kanade.tachiyomi.ui.home
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.material3.BadgedBox
|
import androidx.compose.material3.BadgedBox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.NavigationRailItem
|
import androidx.compose.material3.NavigationRailItem
|
||||||
@ -23,15 +24,24 @@ 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.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastFilter
|
import androidx.compose.ui.util.fastFilter
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
@ -53,6 +63,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import soup.compose.material.motion.MotionConstants
|
||||||
import soup.compose.material.motion.animation.materialFadeThroughIn
|
import soup.compose.material.motion.animation.materialFadeThroughIn
|
||||||
import soup.compose.material.motion.animation.materialFadeThroughOut
|
import soup.compose.material.motion.animation.materialFadeThroughOut
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
@ -61,8 +72,10 @@ import tachiyomi.presentation.core.components.material.NavigationBar
|
|||||||
import tachiyomi.presentation.core.components.material.NavigationRail
|
import tachiyomi.presentation.core.components.material.NavigationRail
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
|
import tachiyomi.presentation.core.util.PredictiveBack
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
object HomeScreen : Screen() {
|
object HomeScreen : Screen() {
|
||||||
|
|
||||||
@ -70,8 +83,14 @@ object HomeScreen : Screen() {
|
|||||||
private val openTabEvent = Channel<Tab>()
|
private val openTabEvent = Channel<Tab>()
|
||||||
private val showBottomNavEvent = Channel<Boolean>()
|
private val showBottomNavEvent = Channel<Boolean>()
|
||||||
|
|
||||||
private const val TAB_FADE_DURATION = 200
|
@Suppress("ConstPropertyName")
|
||||||
private const val TAB_NAVIGATOR_KEY = "HomeTabs"
|
private const val TabFadeDuration = 200
|
||||||
|
|
||||||
|
@Suppress("ConstPropertyName")
|
||||||
|
private const val TabNavigatorKey = "HomeTabs"
|
||||||
|
|
||||||
|
@SuppressLint("ComposeCompositionLocalUsage")
|
||||||
|
val LocalHomeScreenInsetsProvider = staticCompositionLocalOf { WindowInsets(0.dp) }
|
||||||
|
|
||||||
private val TABS = listOf(
|
private val TABS = listOf(
|
||||||
LibraryTab,
|
LibraryTab,
|
||||||
@ -84,6 +103,7 @@ object HomeScreen : Screen() {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@ -94,7 +114,7 @@ object HomeScreen : Screen() {
|
|||||||
|
|
||||||
TabNavigator(
|
TabNavigator(
|
||||||
tab = LibraryTab,
|
tab = LibraryTab,
|
||||||
key = TAB_NAVIGATOR_KEY,
|
key = TabNavigatorKey,
|
||||||
) { tabNavigator ->
|
) { tabNavigator ->
|
||||||
// Provide usable navigator to content screen
|
// Provide usable navigator to content screen
|
||||||
CompositionLocalProvider(LocalNavigator provides navigator) {
|
CompositionLocalProvider(LocalNavigator provides navigator) {
|
||||||
@ -134,26 +154,62 @@ object HomeScreen : Screen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentWindowInsets = WindowInsets(0),
|
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.graphicsLayer {
|
||||||
.consumeWindowInsets(contentPadding),
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
}
|
||||||
|
.windowInsetsPadding(
|
||||||
|
remember {
|
||||||
|
object : WindowInsets {
|
||||||
|
override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int {
|
||||||
|
return with(density) {
|
||||||
|
contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRight(density: Density, layoutDirection: LayoutDirection): Int {
|
||||||
|
return with(density) {
|
||||||
|
contentPadding.calculateRightPadding(layoutDirection).roundToPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBottom(density: Density): Int = 0
|
||||||
|
|
||||||
|
override fun getTop(density: Density): Int = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
|
val insets = remember {
|
||||||
|
object : WindowInsets {
|
||||||
|
override fun getBottom(density: Density): Int {
|
||||||
|
return with(density) { contentPadding.calculateBottomPadding().roundToPx() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTop(density: Density): Int {
|
||||||
|
return with(density) { contentPadding.calculateTopPadding().roundToPx() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0
|
||||||
|
|
||||||
|
override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = tabNavigator.current,
|
targetState = tabNavigator.current,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
materialFadeThroughIn(
|
materialFadeThroughIn(initialScale = 1f, durationMillis = TabFadeDuration) togetherWith
|
||||||
initialScale = 1f,
|
materialFadeThroughOut(durationMillis = TabFadeDuration)
|
||||||
durationMillis = TAB_FADE_DURATION,
|
|
||||||
) togetherWith
|
|
||||||
materialFadeThroughOut(durationMillis = TAB_FADE_DURATION)
|
|
||||||
},
|
},
|
||||||
label = "tabContent",
|
label = "tabContent",
|
||||||
) {
|
) {
|
||||||
tabNavigator.saveableState(key = "currentTab", it) {
|
CompositionLocalProvider(LocalHomeScreenInsetsProvider provides insets) {
|
||||||
it.Content()
|
tabNavigator.saveableState(key = "currentTab", it) {
|
||||||
|
it.Content()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,10 +217,32 @@ object HomeScreen : Screen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val goToLibraryTab = { tabNavigator.current = LibraryTab }
|
val goToLibraryTab = { tabNavigator.current = LibraryTab }
|
||||||
BackHandler(
|
|
||||||
enabled = tabNavigator.current != LibraryTab,
|
var handlingBack by remember { mutableStateOf(false) }
|
||||||
onBack = goToLibraryTab,
|
PredictiveBackHandler(
|
||||||
)
|
enabled = handlingBack || tabNavigator.current::class != LibraryTab::class,
|
||||||
|
) { progress ->
|
||||||
|
handlingBack = true
|
||||||
|
val currentTab = tabNavigator.current
|
||||||
|
try {
|
||||||
|
progress.collect { backEvent ->
|
||||||
|
scale = lerp(1f, 0.92f, PredictiveBack.transform(backEvent.progress))
|
||||||
|
tabNavigator.current = if (backEvent.progress > 0.25f) TABS[0] else currentTab
|
||||||
|
}
|
||||||
|
goToLibraryTab()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
tabNavigator.current = currentTab
|
||||||
|
} finally {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = tween(durationMillis = MotionConstants.DefaultMotionDuration),
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
handlingBack = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
launch {
|
launch {
|
||||||
@ -312,8 +390,6 @@ object HomeScreen : Screen() {
|
|||||||
Icon(
|
Icon(
|
||||||
painter = tab.options.icon!!,
|
painter = tab.options.icon!!,
|
||||||
contentDescription = tab.options.title,
|
contentDescription = tab.options.title,
|
||||||
// TODO: https://issuetracker.google.com/u/0/issues/316327367
|
|
||||||
tint = LocalContentColor.current,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,12 +33,9 @@ class OnboardingScreen : Screen() {
|
|||||||
|
|
||||||
val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString)
|
val restoreSettingKey = stringResource(SettingsDataScreen.restorePreferenceKeyString)
|
||||||
|
|
||||||
BackHandler(
|
BackHandler(enabled = !shownOnboardingFlow) {
|
||||||
enabled = !shownOnboardingFlow,
|
// Prevent exiting if onboarding hasn't been completed
|
||||||
onBack = {
|
}
|
||||||
// Prevent exiting if onboarding hasn't been completed
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
OnboardingScreen(
|
OnboardingScreen(
|
||||||
onComplete = finishOnboarding,
|
onComplete = finishOnboarding,
|
||||||
|
@ -40,19 +40,19 @@ class SettingsScreen(
|
|||||||
Destination.Tracking.id -> SettingsTrackingScreen
|
Destination.Tracking.id -> SettingsTrackingScreen
|
||||||
else -> SettingsMainScreen
|
else -> SettingsMainScreen
|
||||||
},
|
},
|
||||||
content = {
|
onBackPressed = null,
|
||||||
val pop: () -> Unit = {
|
) {
|
||||||
if (it.canPop) {
|
val pop: () -> Unit = {
|
||||||
it.pop()
|
if (it.canPop) {
|
||||||
} else {
|
it.pop()
|
||||||
parentNavigator.pop()
|
} else {
|
||||||
}
|
parentNavigator.pop()
|
||||||
}
|
}
|
||||||
CompositionLocalProvider(LocalBackPress provides pop) {
|
}
|
||||||
DefaultNavigatorScreenTransition(navigator = it)
|
CompositionLocalProvider(LocalBackPress provides pop) {
|
||||||
}
|
DefaultNavigatorScreenTransition(navigator = it)
|
||||||
},
|
}
|
||||||
)
|
}
|
||||||
} else {
|
} else {
|
||||||
Navigator(
|
Navigator(
|
||||||
screen = when (destination) {
|
screen = when (destination) {
|
||||||
@ -61,6 +61,7 @@ class SettingsScreen(
|
|||||||
Destination.Tracking.id -> SettingsTrackingScreen
|
Destination.Tracking.id -> SettingsTrackingScreen
|
||||||
else -> SettingsAppearanceScreen
|
else -> SettingsAppearanceScreen
|
||||||
},
|
},
|
||||||
|
onBackPressed = null,
|
||||||
) {
|
) {
|
||||||
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
|
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)
|
||||||
TwoPanelBox(
|
TwoPanelBox(
|
||||||
|
@ -4,9 +4,11 @@ package eu.kanade.tachiyomi.util.view
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.RoundedCorner
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@ -95,3 +97,22 @@ fun View?.isVisibleOnScreen(): Boolean {
|
|||||||
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
|
Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels)
|
||||||
return actualPosition.intersect(screen)
|
return actualPosition.intersect(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns window radius (in pixel) applied to this view
|
||||||
|
*/
|
||||||
|
fun View.getWindowRadius(): Int {
|
||||||
|
val rad = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val windowInsets = rootWindowInsets
|
||||||
|
listOfNotNull(
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT),
|
||||||
|
windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT),
|
||||||
|
)
|
||||||
|
.minOfOrNull { it.radius }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return rad ?: 0
|
||||||
|
}
|
||||||
|
@ -6,8 +6,6 @@ import androidx.compose.animation.Crossfade
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.forEachGesture
|
|
||||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -201,13 +199,7 @@ class SettingsDebugScreen : Screen() {
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(color = Color.White.copy(alpha = 0.3F))
|
.background(color = Color.White.copy(alpha = 0.3F))
|
||||||
.pointerInput(running && result == null) {
|
.pointerInput(running && result == null) {},
|
||||||
forEachGesture {
|
|
||||||
awaitPointerEventScope {
|
|
||||||
waitForUpOrCancellation()?.consume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
|
@ -84,7 +84,7 @@ class MangaDexFollowsScreen(private val sourceId: Long) : Screen() {
|
|||||||
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
|
val duplicateManga = screenModel.getDuplicateLibraryManga(manga)
|
||||||
when {
|
when {
|
||||||
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
|
manga.favorite -> screenModel.setDialog(BrowseSourceScreenModel.Dialog.RemoveManga(manga))
|
||||||
duplicateManga != null -> screenModel.setDialog(
|
duplicateManga.isNotEmpty() -> screenModel.setDialog(
|
||||||
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
|
BrowseSourceScreenModel.Dialog.AddDuplicateManga(
|
||||||
manga,
|
manga,
|
||||||
duplicateManga,
|
duplicateManga,
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package tachiyomi.presentation.core.components
|
package tachiyomi.presentation.core.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.PredictiveBackHandler
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.rememberSplineBasedDecay
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
|
||||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
@ -23,16 +26,21 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
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.composed
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
@ -41,14 +49,15 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import tachiyomi.presentation.core.util.PredictiveBack
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AdaptiveSheet(
|
fun AdaptiveSheet(
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
@ -75,6 +84,7 @@ fun AdaptiveSheet(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
|
enabled = true,
|
||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
indication = null,
|
indication = null,
|
||||||
onClick = internalOnDismissRequest,
|
onClick = internalOnDismissRequest,
|
||||||
@ -85,6 +95,11 @@ fun AdaptiveSheet(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.predictiveBackAnimation(
|
||||||
|
enabled = remember { derivedStateOf { alpha > 0f } }.value,
|
||||||
|
transformOrigin = TransformOrigin.Center,
|
||||||
|
onBack = internalOnDismissRequest,
|
||||||
|
)
|
||||||
.requiredWidthIn(max = 460.dp)
|
.requiredWidthIn(max = 460.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
@ -97,7 +112,6 @@ fun AdaptiveSheet(
|
|||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
content = {
|
content = {
|
||||||
BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
|
|
||||||
content()
|
content()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -107,16 +121,14 @@ fun AdaptiveSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val decayAnimationSpec = rememberSplineBasedDecay<Float>()
|
val anchoredDraggableState = rememberSaveable(saver = AnchoredDraggableState.Saver()) {
|
||||||
val anchoredDraggableState = remember {
|
AnchoredDraggableState(initialValue = 1)
|
||||||
AnchoredDraggableState(
|
|
||||||
initialValue = 1,
|
|
||||||
positionalThreshold = { with(density) { 56.dp.toPx() } },
|
|
||||||
velocityThreshold = { with(density) { 125.dp.toPx() } },
|
|
||||||
snapAnimationSpec = sheetAnimationSpec,
|
|
||||||
decayAnimationSpec = decayAnimationSpec,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
val flingBehavior = AnchoredDraggableDefaults.flingBehavior(
|
||||||
|
state = anchoredDraggableState,
|
||||||
|
positionalThreshold = { _: Float -> with(density) { 56.dp.toPx() } },
|
||||||
|
animationSpec = sheetAnimationSpec,
|
||||||
|
)
|
||||||
val internalOnDismissRequest = {
|
val internalOnDismissRequest = {
|
||||||
if (anchoredDraggableState.settledValue == 0) {
|
if (anchoredDraggableState.settledValue == 0) {
|
||||||
scope.launch { anchoredDraggableState.animateTo(1) }
|
scope.launch { anchoredDraggableState.animateTo(1) }
|
||||||
@ -141,6 +153,11 @@ fun AdaptiveSheet(
|
|||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.predictiveBackAnimation(
|
||||||
|
enabled = anchoredDraggableState.targetValue == 0,
|
||||||
|
transformOrigin = TransformOrigin(0.5f, 1f),
|
||||||
|
onBack = internalOnDismissRequest,
|
||||||
|
)
|
||||||
.widthIn(max = 460.dp)
|
.widthIn(max = 460.dp)
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
@ -151,9 +168,9 @@ fun AdaptiveSheet(
|
|||||||
if (enableSwipeDismiss) {
|
if (enableSwipeDismiss) {
|
||||||
Modifier.nestedScroll(
|
Modifier.nestedScroll(
|
||||||
remember(anchoredDraggableState) {
|
remember(anchoredDraggableState) {
|
||||||
anchoredDraggableState.preUpPostDownNestedScrollConnection(
|
anchoredDraggableState.preUpPostDownNestedScrollConnection {
|
||||||
onFling = { scope.launch { anchoredDraggableState.settle(it) } },
|
scope.launch { anchoredDraggableState.settle(sheetAnimationSpec) }
|
||||||
)
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -174,16 +191,13 @@ fun AdaptiveSheet(
|
|||||||
state = anchoredDraggableState,
|
state = anchoredDraggableState,
|
||||||
orientation = Orientation.Vertical,
|
orientation = Orientation.Vertical,
|
||||||
enabled = enableSwipeDismiss,
|
enabled = enableSwipeDismiss,
|
||||||
|
flingBehavior = flingBehavior,
|
||||||
)
|
)
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.statusBarsPadding(),
|
.statusBarsPadding(),
|
||||||
shape = MaterialTheme.shapes.extraLarge,
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
content = {
|
content = {
|
||||||
BackHandler(
|
|
||||||
enabled = anchoredDraggableState.targetValue == 0,
|
|
||||||
onBack = internalOnDismissRequest,
|
|
||||||
)
|
|
||||||
content()
|
content()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -238,7 +252,11 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection(
|
|||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
onFling(available.toFloat())
|
onFling(available.toFloat())
|
||||||
return available
|
return if (targetValue != settledValue) {
|
||||||
|
available
|
||||||
|
} else {
|
||||||
|
Velocity.Zero
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Float.toOffset(): Offset = Offset(0f, this)
|
private fun Float.toOffset(): Offset = Offset(0f, this)
|
||||||
@ -249,3 +267,37 @@ private fun <T> AnchoredDraggableState<T>.preUpPostDownNestedScrollConnection(
|
|||||||
@JvmName("offsetToFloat")
|
@JvmName("offsetToFloat")
|
||||||
private fun Offset.toFloat(): Float = this.y
|
private fun Offset.toFloat(): Float = this.y
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Modifier.predictiveBackAnimation(
|
||||||
|
enabled: Boolean,
|
||||||
|
transformOrigin: TransformOrigin,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) = composed {
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
PredictiveBackHandler(enabled = enabled) { progress ->
|
||||||
|
try {
|
||||||
|
progress.collect { backEvent ->
|
||||||
|
scale = lerp(1f, 0.85f, PredictiveBack.transform(backEvent.progress))
|
||||||
|
}
|
||||||
|
// Completion
|
||||||
|
onBack()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// Cancellation
|
||||||
|
} finally {
|
||||||
|
animate(
|
||||||
|
initialValue = scale,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessLow),
|
||||||
|
) { value, _ ->
|
||||||
|
scale = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Modifier.graphicsLayer {
|
||||||
|
this.scaleX = scale
|
||||||
|
this.scaleY = scale
|
||||||
|
this.transformOrigin = transformOrigin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sheetAnimationSpec = tween<Float>(durationMillis = 350)
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
package tachiyomi.presentation.core.util
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.CubicBezierEasing
|
||||||
|
|
||||||
|
private val PredictiveBackEasing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
|
||||||
|
|
||||||
|
object PredictiveBack {
|
||||||
|
fun transform(progress: Float): Float = PredictiveBackEasing.transform(progress)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user