From 96631930bbd6db14556cc8f1d738802626c6aa0a Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Sun, 12 Jun 2022 15:53:01 +0100 Subject: [PATCH 1/5] Animate the changes between different question screens, as well as the end result screen. --- .../jetsurvey/survey/SurveyFragment.kt | 32 +++++++-- .../compose/jetsurvey/survey/SurveyScreen.kt | 69 +++++++++++++++---- 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt index ae4d680fd..188053b7d 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt @@ -21,6 +21,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts.TakePicture +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.with import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment @@ -41,6 +48,7 @@ class SurveyFragment : Fragment() { } } + @OptIn(ExperimentalAnimationApi::class) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -56,20 +64,34 @@ class SurveyFragment : Fragment() { ) setContent { JetsurveyTheme { - viewModel.uiState.observeAsState().value?.let { surveyState -> - when (surveyState) { + val state = viewModel.uiState.observeAsState().value ?: return@JetsurveyTheme + AnimatedContent( + targetState = state, + transitionSpec = { + fadeIn() + + slideInVertically(animationSpec = + tween(400 * 3), + initialOffsetY = { fullWidth -> fullWidth }) with + fadeOut(animationSpec = tween(200 * 3)) + + } + ) { targetState -> + // It's important to use targetState and not state, as its critical to ensure + // a successful lookup of all the incoming and outgoing content during + // content transform. + when (targetState) { is SurveyState.Questions -> SurveyQuestionsScreen( - questions = surveyState, + questions = targetState, shouldAskPermissions = viewModel.askForPermissions, onAction = { id, action -> handleSurveyAction(id, action) }, onDoNotAskForPermissions = { viewModel.doNotAskForPermissions() }, - onDonePressed = { viewModel.computeResult(surveyState) }, + onDonePressed = { viewModel.computeResult(targetState) }, onBackPressed = { activity?.onBackPressedDispatcher?.onBackPressed() } ) is SurveyState.Result -> SurveyResultScreen( - result = surveyState, + result = targetState, onDonePressed = { activity?.onBackPressedDispatcher?.onBackPressed() } diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt index c9bdb1bd9..c6667935f 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt @@ -16,7 +16,13 @@ package com.example.compose.jetsurvey.survey +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.with import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -56,6 +62,8 @@ import com.example.compose.jetsurvey.R import com.example.compose.jetsurvey.theme.progressIndicatorBackground import com.example.compose.jetsurvey.util.supportWideScreen +private const val CONTENT_ANIMATION_DURATION = 500 +@OptIn(ExperimentalAnimationApi::class) @Composable fun SurveyQuestionsScreen( questions: SurveyState.Questions, @@ -79,22 +87,53 @@ fun SurveyQuestionsScreen( ) }, content = { innerPadding -> - Question( - question = questionState.question, - answer = questionState.answer, - shouldAskPermissions = shouldAskPermissions, - onAnswer = { - if (it !is Answer.PermissionsDenied) { - questionState.answer = it + AnimatedContent( + targetState = questionState, + transitionSpec = { + if (targetState.questionIndex > initialState.questionIndex) { + // Going forwards in the survey: Set the initial offset to start + // at the size of the content so it slides in from right to left, and + // slides out from the left of the screen to -fullWidth + slideInHorizontally( + animationSpec = tween(CONTENT_ANIMATION_DURATION), + initialOffsetX = { fullWidth -> fullWidth } + ) with + slideOutHorizontally( + animationSpec = tween(CONTENT_ANIMATION_DURATION), + targetOffsetX = { fullWidth -> -fullWidth } + ) + } else { + // Going back to the previous question in the set, we do the same + // transition as above, but with different offsets - the inverse of + // above, negative fullWidth to enter, and fullWidth to exit. + slideInHorizontally( + animationSpec = tween(CONTENT_ANIMATION_DURATION), + initialOffsetX = { fullWidth -> -fullWidth } + ) with + slideOutHorizontally( + animationSpec = tween(CONTENT_ANIMATION_DURATION), + targetOffsetX = { fullWidth -> fullWidth } + ) } - questionState.enableNext = true - }, - onAction = onAction, - onDoNotAskForPermissions = onDoNotAskForPermissions, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) + } + ) { targetState -> + Question( + question = targetState.question, + answer = targetState.answer, + shouldAskPermissions = shouldAskPermissions, + onAnswer = { + if (it !is Answer.PermissionsDenied) { + targetState.answer = it + } + targetState.enableNext = true + }, + onAction = onAction, + onDoNotAskForPermissions = onDoNotAskForPermissions, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) + } }, bottomBar = { SurveyBottomBar( From ab5f441c99baff080ad6220b2dfddb92d7198c7c Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Sun, 12 Jun 2022 18:05:24 +0100 Subject: [PATCH 2/5] spotless --- .../example/compose/jetsurvey/survey/SurveyFragment.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt index 188053b7d..203559df7 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt @@ -69,11 +69,12 @@ class SurveyFragment : Fragment() { targetState = state, transitionSpec = { fadeIn() + - slideInVertically(animationSpec = + slideInVertically( + animationSpec = tween(400 * 3), - initialOffsetY = { fullWidth -> fullWidth }) with - fadeOut(animationSpec = tween(200 * 3)) - + initialOffsetY = { fullWidth -> fullWidth } + ) with + fadeOut(animationSpec = tween(200 * 3)) } ) { targetState -> // It's important to use targetState and not state, as its critical to ensure From f28079518045d3271a36a84047c154ba78af665e Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Sun, 12 Jun 2022 19:27:50 +0100 Subject: [PATCH 3/5] Change duration --- .../com/example/compose/jetsurvey/survey/SurveyFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt index 203559df7..bfdc3ecf7 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt @@ -71,10 +71,10 @@ class SurveyFragment : Fragment() { fadeIn() + slideInVertically( animationSpec = - tween(400 * 3), + tween(400), initialOffsetY = { fullWidth -> fullWidth } ) with - fadeOut(animationSpec = tween(200 * 3)) + fadeOut(animationSpec = tween(200)) } ) { targetState -> // It's important to use targetState and not state, as its critical to ensure From fd679d4d6da4094814daf3d34c86cf9d9f7a835d Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Sun, 12 Jun 2022 20:04:02 +0100 Subject: [PATCH 4/5] Switch to slideIntoContainer --- .../jetsurvey/survey/SurveyFragment.kt | 13 +++-- .../compose/jetsurvey/survey/SurveyScreen.kt | 52 +++++++++---------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt index bfdc3ecf7..4e259f0d5 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt @@ -22,11 +22,11 @@ import android.view.View import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts.TakePicture import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically import androidx.compose.animation.with import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.platform.ComposeView @@ -68,12 +68,11 @@ class SurveyFragment : Fragment() { AnimatedContent( targetState = state, transitionSpec = { - fadeIn() + - slideInVertically( - animationSpec = - tween(400), - initialOffsetY = { fullWidth -> fullWidth } - ) with + fadeIn() + slideIntoContainer( + towards = AnimatedContentScope + .SlideDirection.Up, + animationSpec = tween(600) + ) with fadeOut(animationSpec = tween(200)) } ) { targetState -> diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt index c6667935f..b6de9d1d3 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyScreen.kt @@ -17,11 +17,11 @@ package com.example.compose.jetsurvey.survey import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.with import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -57,12 +57,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.example.compose.jetsurvey.R import com.example.compose.jetsurvey.theme.progressIndicatorBackground import com.example.compose.jetsurvey.util.supportWideScreen private const val CONTENT_ANIMATION_DURATION = 500 + @OptIn(ExperimentalAnimationApi::class) @Composable fun SurveyQuestionsScreen( @@ -90,31 +92,27 @@ fun SurveyQuestionsScreen( AnimatedContent( targetState = questionState, transitionSpec = { - if (targetState.questionIndex > initialState.questionIndex) { - // Going forwards in the survey: Set the initial offset to start - // at the size of the content so it slides in from right to left, and - // slides out from the left of the screen to -fullWidth - slideInHorizontally( - animationSpec = tween(CONTENT_ANIMATION_DURATION), - initialOffsetX = { fullWidth -> fullWidth } - ) with - slideOutHorizontally( - animationSpec = tween(CONTENT_ANIMATION_DURATION), - targetOffsetX = { fullWidth -> -fullWidth } - ) - } else { - // Going back to the previous question in the set, we do the same - // transition as above, but with different offsets - the inverse of - // above, negative fullWidth to enter, and fullWidth to exit. - slideInHorizontally( - animationSpec = tween(CONTENT_ANIMATION_DURATION), - initialOffsetX = { fullWidth -> -fullWidth } - ) with - slideOutHorizontally( - animationSpec = tween(CONTENT_ANIMATION_DURATION), - targetOffsetX = { fullWidth -> fullWidth } - ) - } + val animationSpec: TweenSpec = tween(CONTENT_ANIMATION_DURATION) + val direction = + if (targetState.questionIndex > initialState.questionIndex) { + // Going forwards in the survey: Set the initial offset to start + // at the size of the content so it slides in from right to left, and + // slides out from the left of the screen to -fullWidth + AnimatedContentScope.SlideDirection.Left + } else { + // Going back to the previous question in the set, we do the same + // transition as above, but with different offsets - the inverse of + // above, negative fullWidth to enter, and fullWidth to exit. + AnimatedContentScope.SlideDirection.Right + } + slideIntoContainer( + towards = direction, + animationSpec = animationSpec + ) with + slideOutOfContainer( + towards = direction, + animationSpec = animationSpec + ) } ) { targetState -> Question( From e13e0071631e7632954a032ee499a60e09bbbd15 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Mon, 20 Jun 2022 14:46:18 +0100 Subject: [PATCH 5/5] PR Feedback --- .../example/compose/jetsurvey/survey/SurveyFragment.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt index 4e259f0d5..412f0b628 100644 --- a/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt +++ b/Jetsurvey/app/src/main/java/com/example/compose/jetsurvey/survey/SurveyFragment.kt @@ -71,9 +71,9 @@ class SurveyFragment : Fragment() { fadeIn() + slideIntoContainer( towards = AnimatedContentScope .SlideDirection.Up, - animationSpec = tween(600) + animationSpec = tween(ANIMATION_SLIDE_IN_DURATION) ) with - fadeOut(animationSpec = tween(200)) + fadeOut(animationSpec = tween(ANIMATION_FADE_OUT_DURATION)) } ) { targetState -> // It's important to use targetState and not state, as its critical to ensure @@ -132,4 +132,9 @@ class SurveyFragment : Fragment() { private fun selectContact(questionId: Int) { // TODO: unsupported for now } + + companion object { + private const val ANIMATION_SLIDE_IN_DURATION = 600 + private const val ANIMATION_FADE_OUT_DURATION = 200 + } }