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..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 @@ -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.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.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() + slideIntoContainer( + towards = AnimatedContentScope + .SlideDirection.Up, + animationSpec = tween(ANIMATION_SLIDE_IN_DURATION) + ) with + fadeOut(animationSpec = tween(ANIMATION_FADE_OUT_DURATION)) + } + ) { 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() } @@ -110,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 + } } 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..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 @@ -16,7 +16,13 @@ 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.with import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -51,11 +57,15 @@ 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( questions: SurveyState.Questions, @@ -79,22 +89,49 @@ fun SurveyQuestionsScreen( ) }, content = { innerPadding -> - Question( - question = questionState.question, - answer = questionState.answer, - shouldAskPermissions = shouldAskPermissions, - onAnswer = { - if (it !is Answer.PermissionsDenied) { - questionState.answer = it - } - questionState.enableNext = true - }, - onAction = onAction, - onDoNotAskForPermissions = onDoNotAskForPermissions, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) + AnimatedContent( + targetState = questionState, + transitionSpec = { + 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( + 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(