Skip to content

Commit 3d0b54f

Browse files
authored
[JetSurvey] Use AnimatedContent to slide in and out changes between questions (#842)
* Animate the changes between different question screens, as well as the end result screen. * spotless * Change duration * Switch to slideIntoContainer * PR Feedback
1 parent 203a582 commit 3d0b54f

File tree

2 files changed

+85
-21
lines changed

2 files changed

+85
-21
lines changed

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ import android.view.LayoutInflater
2121
import android.view.View
2222
import android.view.ViewGroup
2323
import androidx.activity.result.contract.ActivityResultContracts.TakePicture
24+
import androidx.compose.animation.AnimatedContent
25+
import androidx.compose.animation.AnimatedContentScope
26+
import androidx.compose.animation.ExperimentalAnimationApi
27+
import androidx.compose.animation.core.tween
28+
import androidx.compose.animation.fadeIn
29+
import androidx.compose.animation.fadeOut
30+
import androidx.compose.animation.with
2431
import androidx.compose.runtime.livedata.observeAsState
2532
import androidx.compose.ui.platform.ComposeView
2633
import androidx.fragment.app.Fragment
@@ -41,6 +48,7 @@ class SurveyFragment : Fragment() {
4148
}
4249
}
4350

51+
@OptIn(ExperimentalAnimationApi::class)
4452
override fun onCreateView(
4553
inflater: LayoutInflater,
4654
container: ViewGroup?,
@@ -56,20 +64,34 @@ class SurveyFragment : Fragment() {
5664
)
5765
setContent {
5866
JetsurveyTheme {
59-
viewModel.uiState.observeAsState().value?.let { surveyState ->
60-
when (surveyState) {
67+
val state = viewModel.uiState.observeAsState().value ?: return@JetsurveyTheme
68+
AnimatedContent(
69+
targetState = state,
70+
transitionSpec = {
71+
fadeIn() + slideIntoContainer(
72+
towards = AnimatedContentScope
73+
.SlideDirection.Up,
74+
animationSpec = tween(ANIMATION_SLIDE_IN_DURATION)
75+
) with
76+
fadeOut(animationSpec = tween(ANIMATION_FADE_OUT_DURATION))
77+
}
78+
) { targetState ->
79+
// It's important to use targetState and not state, as its critical to ensure
80+
// a successful lookup of all the incoming and outgoing content during
81+
// content transform.
82+
when (targetState) {
6183
is SurveyState.Questions -> SurveyQuestionsScreen(
62-
questions = surveyState,
84+
questions = targetState,
6385
shouldAskPermissions = viewModel.askForPermissions,
6486
onAction = { id, action -> handleSurveyAction(id, action) },
6587
onDoNotAskForPermissions = { viewModel.doNotAskForPermissions() },
66-
onDonePressed = { viewModel.computeResult(surveyState) },
88+
onDonePressed = { viewModel.computeResult(targetState) },
6789
onBackPressed = {
6890
activity?.onBackPressedDispatcher?.onBackPressed()
6991
}
7092
)
7193
is SurveyState.Result -> SurveyResultScreen(
72-
result = surveyState,
94+
result = targetState,
7395
onDonePressed = {
7496
activity?.onBackPressedDispatcher?.onBackPressed()
7597
}
@@ -110,4 +132,9 @@ class SurveyFragment : Fragment() {
110132
private fun selectContact(questionId: Int) {
111133
// TODO: unsupported for now
112134
}
135+
136+
companion object {
137+
private const val ANIMATION_SLIDE_IN_DURATION = 600
138+
private const val ANIMATION_FADE_OUT_DURATION = 200
139+
}
113140
}

Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616

1717
package com.example.compose.jetsurvey.survey
1818

19+
import androidx.compose.animation.AnimatedContent
20+
import androidx.compose.animation.AnimatedContentScope
21+
import androidx.compose.animation.ExperimentalAnimationApi
22+
import androidx.compose.animation.core.TweenSpec
1923
import androidx.compose.animation.core.animateFloatAsState
24+
import androidx.compose.animation.core.tween
25+
import androidx.compose.animation.with
2026
import androidx.compose.foundation.layout.Box
2127
import androidx.compose.foundation.layout.Column
2228
import androidx.compose.foundation.layout.Row
@@ -51,11 +57,15 @@ import androidx.compose.ui.res.stringResource
5157
import androidx.compose.ui.text.buildAnnotatedString
5258
import androidx.compose.ui.text.font.FontWeight
5359
import androidx.compose.ui.text.withStyle
60+
import androidx.compose.ui.unit.IntOffset
5461
import androidx.compose.ui.unit.dp
5562
import com.example.compose.jetsurvey.R
5663
import com.example.compose.jetsurvey.theme.progressIndicatorBackground
5764
import com.example.compose.jetsurvey.util.supportWideScreen
5865

66+
private const val CONTENT_ANIMATION_DURATION = 500
67+
68+
@OptIn(ExperimentalAnimationApi::class)
5969
@Composable
6070
fun SurveyQuestionsScreen(
6171
questions: SurveyState.Questions,
@@ -79,22 +89,49 @@ fun SurveyQuestionsScreen(
7989
)
8090
},
8191
content = { innerPadding ->
82-
Question(
83-
question = questionState.question,
84-
answer = questionState.answer,
85-
shouldAskPermissions = shouldAskPermissions,
86-
onAnswer = {
87-
if (it !is Answer.PermissionsDenied) {
88-
questionState.answer = it
89-
}
90-
questionState.enableNext = true
91-
},
92-
onAction = onAction,
93-
onDoNotAskForPermissions = onDoNotAskForPermissions,
94-
modifier = Modifier
95-
.fillMaxSize()
96-
.padding(innerPadding)
97-
)
92+
AnimatedContent(
93+
targetState = questionState,
94+
transitionSpec = {
95+
val animationSpec: TweenSpec<IntOffset> = tween(CONTENT_ANIMATION_DURATION)
96+
val direction =
97+
if (targetState.questionIndex > initialState.questionIndex) {
98+
// Going forwards in the survey: Set the initial offset to start
99+
// at the size of the content so it slides in from right to left, and
100+
// slides out from the left of the screen to -fullWidth
101+
AnimatedContentScope.SlideDirection.Left
102+
} else {
103+
// Going back to the previous question in the set, we do the same
104+
// transition as above, but with different offsets - the inverse of
105+
// above, negative fullWidth to enter, and fullWidth to exit.
106+
AnimatedContentScope.SlideDirection.Right
107+
}
108+
slideIntoContainer(
109+
towards = direction,
110+
animationSpec = animationSpec
111+
) with
112+
slideOutOfContainer(
113+
towards = direction,
114+
animationSpec = animationSpec
115+
)
116+
}
117+
) { targetState ->
118+
Question(
119+
question = targetState.question,
120+
answer = targetState.answer,
121+
shouldAskPermissions = shouldAskPermissions,
122+
onAnswer = {
123+
if (it !is Answer.PermissionsDenied) {
124+
targetState.answer = it
125+
}
126+
targetState.enableNext = true
127+
},
128+
onAction = onAction,
129+
onDoNotAskForPermissions = onDoNotAskForPermissions,
130+
modifier = Modifier
131+
.fillMaxSize()
132+
.padding(innerPadding)
133+
)
134+
}
98135
},
99136
bottomBar = {
100137
SurveyBottomBar(

0 commit comments

Comments
 (0)