diff --git a/app/src/main/java/me/ash/reader/ui/ext/NavGraphBuilderExt.kt b/app/src/main/java/me/ash/reader/ui/ext/NavGraphBuilderExt.kt index 47143fea3..f09eb3016 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/NavGraphBuilderExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/NavGraphBuilderExt.kt @@ -16,36 +16,67 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink import androidx.navigation.NavGraphBuilder import com.google.accompanist.navigation.animation.composable +import me.ash.reader.ui.motion.materialSharedAxisXIn +import me.ash.reader.ui.motion.materialSharedAxisXOut @OptIn(ExperimentalAnimationApi::class) +@Deprecated(message = "Migrate to Forward and backward transition", replaceWith = ReplaceWith("forwardAndBackwardComposable(route = route, arguments = arguments, deepLinks = deepLinks) { content() }") +) fun NavGraphBuilder.animatedComposable( - route: String, - arguments: List = emptyList(), - deepLinks: List = emptyList(), - content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit, + route: String, + arguments: List = emptyList(), + deepLinks: List = emptyList(), + content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit, +) = composable( + route = route, + arguments = arguments, + deepLinks = deepLinks, + enterTransition = { + fadeIn(animationSpec = tween(220, delayMillis = 90)) + + scaleIn( + initialScale = 0.92f, + animationSpec = tween(220, delayMillis = 90) + ) + }, + exitTransition = { + fadeOut(animationSpec = tween(90)) + }, + popEnterTransition = { + fadeIn(animationSpec = tween(220, delayMillis = 90)) + + scaleIn( + initialScale = 0.92f, + animationSpec = tween(220, delayMillis = 90) + ) + }, + popExitTransition = { + fadeOut(animationSpec = tween(90)) + }, + content = content +) + +private const val INITIAL_OFFSET_FACTOR = 0.10f + +@OptIn(ExperimentalAnimationApi::class) +fun NavGraphBuilder.forwardAndBackwardComposable( + route: String, + arguments: List = emptyList(), + deepLinks: List = emptyList(), + content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit ) = composable( - route = route, - arguments = arguments, - deepLinks = deepLinks, - enterTransition = { - fadeIn(animationSpec = tween(220, delayMillis = 90)) + - scaleIn( - initialScale = 0.92f, - animationSpec = tween(220, delayMillis = 90) - ) - }, - exitTransition = { - fadeOut(animationSpec = tween(90)) - }, - popEnterTransition = { - fadeIn(animationSpec = tween(220, delayMillis = 90)) + - scaleIn( - initialScale = 0.92f, - animationSpec = tween(220, delayMillis = 90) - ) - }, - popExitTransition = { - fadeOut(animationSpec = tween(90)) - }, - content = content + route = route, + arguments = arguments, + deepLinks = deepLinks, + enterTransition = { + materialSharedAxisXIn(initialOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) + }, + exitTransition = { + materialSharedAxisXOut(targetOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) + }, + popEnterTransition = { + materialSharedAxisXIn(initialOffsetX = { -(it * INITIAL_OFFSET_FACTOR).toInt() }) + }, + popExitTransition = { + materialSharedAxisXOut(targetOffsetX = { (it * INITIAL_OFFSET_FACTOR).toInt() }) + }, + content = content ) diff --git a/app/src/main/java/me/ash/reader/ui/motion/MaterialSharedAxis.kt b/app/src/main/java/me/ash/reader/ui/motion/MaterialSharedAxis.kt new file mode 100644 index 000000000..b66af3f44 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/motion/MaterialSharedAxis.kt @@ -0,0 +1,116 @@ +package me.ash.reader.ui.motion + +/* + * Copyright 2021 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + + +/** + * Returns the provided [Dp] as an [Int] value by the [LocalDensity]. + * + * @param slideDistance Value to the slide distance dimension, 30dp by default. + */ +@Composable +public fun rememberSlideDistance( + slideDistance: Dp = MotionConstants.DefaultSlideDistance, +): Int { + val density = LocalDensity.current + return remember(density, slideDistance) { + with(density) { slideDistance.roundToPx() } + } +} + +private const val ProgressThreshold = 0.35f + +private val Int.ForOutgoing: Int + get() = (this * ProgressThreshold).toInt() + +private val Int.ForIncoming: Int + get() = this - this.ForOutgoing + +/** + * [materialSharedAxisX] allows to switch a layout with shared X-axis transition. + * + */ +@OptIn(ExperimentalAnimationApi::class) +public fun materialSharedAxisX( + initialOffsetX: (fullWidth: Int) -> Int, + targetOffsetX: (fullWidth: Int) -> Int, + durationMillis: Int = MotionConstants.DefaultMotionDuration, +): ContentTransform = ContentTransform(materialSharedAxisXIn( + initialOffsetX = initialOffsetX, + durationMillis = durationMillis +), materialSharedAxisXOut( + targetOffsetX = targetOffsetX, + durationMillis = durationMillis +)) + +/** + * [materialSharedAxisXIn] allows to switch a layout with shared X-axis enter transition. + */ +public fun materialSharedAxisXIn( + initialOffsetX: (fullWidth: Int) -> Int, + durationMillis: Int = MotionConstants.DefaultMotionDuration, +): EnterTransition = slideInHorizontally( + animationSpec = tween( + durationMillis = durationMillis, + easing = FastOutSlowInEasing + ), + initialOffsetX = initialOffsetX +) + fadeIn( + animationSpec = tween( + durationMillis = durationMillis.ForIncoming, + delayMillis = durationMillis.ForOutgoing, + easing = LinearOutSlowInEasing + ) +) + +/** + * [materialSharedAxisXOut] allows to switch a layout with shared X-axis exit transition. + * + */ +public fun materialSharedAxisXOut( + targetOffsetX: (fullWidth: Int) -> Int, + durationMillis: Int = MotionConstants.DefaultMotionDuration, +): ExitTransition = slideOutHorizontally( + animationSpec = tween( + durationMillis = durationMillis, + easing = FastOutSlowInEasing + ), + targetOffsetX = targetOffsetX +) + fadeOut( + animationSpec = tween( + durationMillis = durationMillis.ForOutgoing, + delayMillis = 0, + easing = FastOutLinearInEasing + ) +) diff --git a/app/src/main/java/me/ash/reader/ui/motion/MotionConstants.kt b/app/src/main/java/me/ash/reader/ui/motion/MotionConstants.kt new file mode 100644 index 000000000..162a1295c --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/motion/MotionConstants.kt @@ -0,0 +1,28 @@ +package me.ash.reader.ui.motion + +/* + * Copyright 2021 SOUP + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +public object MotionConstants { + public const val DefaultMotionDuration: Int = 300 + public const val DefaultFadeInDuration: Int = 150 + public const val DefaultFadeOutDuration: Int = 75 + public val DefaultSlideDistance: Dp = 30.dp +} \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt index 5bb4e91fa..08b533a7d 100644 --- a/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt +++ b/app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt @@ -120,86 +120,86 @@ fun HomeEntry( startDestination = if (context.isFirstLaunch) RouteName.STARTUP else RouteName.FEEDS, ) { // Startup - animatedComposable(route = RouteName.STARTUP) { + forwardAndBackwardComposable(route = RouteName.STARTUP) { StartupPage(navController) } // Home - animatedComposable(route = RouteName.FEEDS) { + forwardAndBackwardComposable(route = RouteName.FEEDS) { FeedsPage(navController = navController, homeViewModel = homeViewModel) } - animatedComposable(route = RouteName.FLOW) { + forwardAndBackwardComposable(route = RouteName.FLOW) { FlowPage( navController = navController, homeViewModel = homeViewModel, ) } - animatedComposable(route = "${RouteName.READING}/{articleId}") { + forwardAndBackwardComposable(route = "${RouteName.READING}/{articleId}") { ReadingPage(navController = navController, homeViewModel = homeViewModel) } // Settings - animatedComposable(route = RouteName.SETTINGS) { + forwardAndBackwardComposable(route = RouteName.SETTINGS) { SettingsPage(navController) } // Accounts - animatedComposable(route = RouteName.ACCOUNTS) { + forwardAndBackwardComposable(route = RouteName.ACCOUNTS) { AccountsPage(navController) } - animatedComposable(route = "${RouteName.ACCOUNT_DETAILS}/{accountId}") { + forwardAndBackwardComposable(route = "${RouteName.ACCOUNT_DETAILS}/{accountId}") { AccountDetailsPage(navController) } - animatedComposable(route = RouteName.ADD_ACCOUNTS) { + forwardAndBackwardComposable(route = RouteName.ADD_ACCOUNTS) { AddAccountsPage(navController) } // Color & Style - animatedComposable(route = RouteName.COLOR_AND_STYLE) { + forwardAndBackwardComposable(route = RouteName.COLOR_AND_STYLE) { ColorAndStylePage(navController) } - animatedComposable(route = RouteName.DARK_THEME) { + forwardAndBackwardComposable(route = RouteName.DARK_THEME) { DarkThemePage(navController) } - animatedComposable(route = RouteName.FEEDS_PAGE_STYLE) { + forwardAndBackwardComposable(route = RouteName.FEEDS_PAGE_STYLE) { FeedsPageStylePage(navController) } - animatedComposable(route = RouteName.FLOW_PAGE_STYLE) { + forwardAndBackwardComposable(route = RouteName.FLOW_PAGE_STYLE) { FlowPageStylePage(navController) } - animatedComposable(route = RouteName.READING_PAGE_STYLE) { + forwardAndBackwardComposable(route = RouteName.READING_PAGE_STYLE) { ReadingStylePage(navController) } - animatedComposable(route = RouteName.READING_DARK_THEME) { + forwardAndBackwardComposable(route = RouteName.READING_DARK_THEME) { ReadingDarkThemePage(navController) } - animatedComposable(route = RouteName.READING_PAGE_TITLE) { + forwardAndBackwardComposable(route = RouteName.READING_PAGE_TITLE) { ReadingTitlePage(navController) } - animatedComposable(route = RouteName.READING_PAGE_TEXT) { + forwardAndBackwardComposable(route = RouteName.READING_PAGE_TEXT) { ReadingTextPage(navController) } - animatedComposable(route = RouteName.READING_PAGE_IMAGE) { + forwardAndBackwardComposable(route = RouteName.READING_PAGE_IMAGE) { ReadingImagePage(navController) } - animatedComposable(route = RouteName.READING_PAGE_VIDEO) { + forwardAndBackwardComposable(route = RouteName.READING_PAGE_VIDEO) { ReadingVideoPage(navController) } // Interaction - animatedComposable(route = RouteName.INTERACTION) { + forwardAndBackwardComposable(route = RouteName.INTERACTION) { InteractionPage(navController) } // Languages - animatedComposable(route = RouteName.LANGUAGES) { + forwardAndBackwardComposable(route = RouteName.LANGUAGES) { LanguagesPage(navController = navController) } // Tips & Support - animatedComposable(route = RouteName.TIPS_AND_SUPPORT) { + forwardAndBackwardComposable(route = RouteName.TIPS_AND_SUPPORT) { TipsAndSupportPage(navController) } }