Skip to content

Commit

Permalink
feat(ui): use forward and backward transition (#540)
Browse files Browse the repository at this point in the history
  • Loading branch information
JunkFood02 authored Jan 18, 2024
1 parent 867b9fc commit 34e6648
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 49 deletions.
87 changes: 59 additions & 28 deletions app/src/main/java/me/ash/reader/ui/ext/NavGraphBuilderExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit,
route: String,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = 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<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = 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
)
116 changes: 116 additions & 0 deletions app/src/main/java/me/ash/reader/ui/motion/MaterialSharedAxis.kt
Original file line number Diff line number Diff line change
@@ -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
)
)
28 changes: 28 additions & 0 deletions app/src/main/java/me/ash/reader/ui/motion/MotionConstants.kt
Original file line number Diff line number Diff line change
@@ -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
}
42 changes: 21 additions & 21 deletions app/src/main/java/me/ash/reader/ui/page/common/HomeEntry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down

0 comments on commit 34e6648

Please sign in to comment.