1

I am trying to display splash screen where elements would have enter animation and when data would be loaded for the first time then minimize it to top app bar and display data.

Edit:

I managed to put both animation to one screen, but I am not sure if this is okay to use AnimatedVisibility inside MotionLayout.

My SplashAppBar looks this way:

fun SplashAppBar(
    collapsed: Boolean,
    hideEnterAnimation: Boolean = true,
) {
    val context = LocalContext.current
    val motionScene = remember {
        context.resources.openRawResource(R.raw.splash_app_bar_motion_scene).readBytes()
            .decodeToString()
    }

    val progress by animateFloatAsState(
        targetValue = if (collapsed) 1f else 0f,
        tween(2000)
    )
    val motionHeight by animateDpAsState(
        targetValue = if (collapsed) 56.dp else LocalConfiguration.current.screenHeightDp.dp,
        tween(2000)
    )
    val fontSize by animateIntAsState(
        targetValue = if (collapsed) 20 else 96,
        tween(2000)
    )

    var visible by remember { mutableStateOf(hideEnterAnimation) }
    val density = LocalDensity.current
    LaunchedEffect(key1 = true) {
        visible = true
    }

    MotionLayout(
        motionScene = MotionScene(content = motionScene),
        progress = progress,
        modifier = Modifier
            .fillMaxWidth()
            .height(motionHeight)
            .background(
                Brush.verticalGradient(
                    listOf(
                        Color.Black,
                        BlueGray500
                    )
                ),
            )
    ) {
        AnimatedVisibility(
            visible = visible,
            enter = slideInVertically(
                animationSpec = tween(
                    durationMillis = 1000,
                    easing = EaseOutCirc
                )
            ) {
                with(density) { 200.dp.roundToPx() }
            } + fadeIn(initialAlpha = 0.3f),
            modifier = Modifier.layoutId("title"),
        ) {
            Text(
                text = "Foodie",
                style = if (progress > 0.5f) MaterialTheme.typography.h6 else MaterialTheme.typography.h1,
                fontSize = fontSize.sp,
                color = Color.White,
            )
        }
        AnimatedVisibility(
            visible = visible,
            enter = slideInVertically(
                animationSpec = tween(
                    durationMillis = 2000,
                    easing = EaseOutBounce
                )
            ) {
                with(density) { -500.dp.roundToPx() }
            } + fadeIn(initialAlpha = 0.3f),
            modifier = Modifier.layoutId("logo"),
        ) {
            Image(
                painter = painterResource(R.drawable.foodie_logo),
                contentDescription = "Foodie logo",
            )
        }
    }
}

With splash_app_bar_motion_scene:

{
  ConstraintSets: {
    start: {
      logo:{
        width: 200,
        height: 200,
        start: ['parent', 'start'],
        end: ['parent', 'end'],
        top: ['parent', 'top'],
        bottom: ['parent', 'bottom']
      },
      title: {
        start: ['logo', 'start'],
        end: ['logo', 'end'],
        top: ['logo', 'bottom']
      }
    },
    end: {
      logo:{
        width: 24,
        height: 24,
        start: ['parent', 'start', 16],
        top: ['parent', 'top', 12],
        bottom: ['parent', 'bottom', 12],
      },
      title: {
        start: ['logo', 'end', 16],
        top: ['logo', 'top'],
        bottom: ['logo', 'bottom']
      }
    }
  },
  Transitions: {
    default: {
      from: 'start',
      to: 'end',
      pathMotionArc: 'startVertical',
      KeyFrames: {
        KeyAttributes: [
          {
            target: ['logo'],
            frames: [0, 100],
            rotationZ: [0,  360]
          },
          {
            target: ['title'],
            frames: [0, 20, 50, 80, 100],
            translationY: [0,-100, -45, -10, 0],
            translationX: [0, 0, -80, -85, 0],
          }
        ]
      }
    }
  }
}

The way I present it is:

@Composable
fun MainView(
    uiState: UiState<Data>,
) {
    Column {
        SplashAppBar(
            collapsed = uiState.initialized,
            hideEnterAnimation = false,
        )

        when {
            !uiState.initialized -> Unit
            uiState.errorMessage.isNotEmpty() -> ErrorRetryView()
            uiState.successData != null -> SuccessView(uiState.successData)
            uiState.loading -> LoadingView()
            else -> RetryView()
        }
    }
}
gabilcious
  • 38
  • 5

1 Answers1

1

If you are already using ConstraintLayout consider MotionLayout. It allows huge customizability. Here is an example of coding a fly in effect. This will be up soon on Compose MotionLayout Examples )

@OptIn(ExperimentalMotionApi::class)
@Preview(group = "motion101")
@Composable
fun M3MultiState() {
    val titleId = "title"


    var scene = MotionScene() {
        val titleRef = createRefFor(titleId)
        val a = constraintSet {
            constrain(titleRef) {
                centerHorizontallyTo(parent,0f)
                centerVerticallyTo(parent,0f)
            }
        }

        val b = constraintSet {
            constrain(titleRef) {
                centerHorizontallyTo(parent,1f)
                 centerVerticallyTo(parent,0f)
            }
        }
        val c = constraintSet {
            constrain(titleRef) {
                centerHorizontallyTo(parent,1f)
                centerVerticallyTo(parent,1f)
            }
        }
        transition(  a,b,"loading") {
        }
        transition(  b,c,"normal") {
        }
    }
    val painter = painterResource(id = R.drawable.pepper)

    var transitionName by remember {
        mutableStateOf("loading")
    }
    var animateToEnd by remember { mutableStateOf(true) }
    val progress = remember { Animatable(0f) }
    LaunchedEffect(animateToEnd) {
        val result = progress.animateTo(
            if (animateToEnd) 1f else 0f,
            animationSpec = tween(5000)
        )
        transitionName = "normal"
        progress.snapTo(0f)
        progress.animateTo(
            if (animateToEnd) 1f else 0f,
            animationSpec = tween(5000)
        )
    }
    MotionLayout(
        modifier = Modifier
            .background(Color(0xFF221010))
            .fillMaxSize()
            .padding(1.dp),
        motionScene = scene,
        transitionName = transitionName,
        progress = progress.value
    ) {

        Text(
            modifier = Modifier.layoutId(titleId),
            text = transitionName,
            fontSize = 30.sp,
            color = Color.White
        )

    }
}

Here is a 3 state example using MotionLayout. The key here is the LaunchedEffect does both transitions. You would need to block the second transition till data is loaded.

hoford
  • 4,918
  • 2
  • 19
  • 19
  • Hi! I already used `MotionLayout` in `SplashAppBar`. I just don't see how to combine my 2 animations - entering screen and collapsing when data are loaded - into one `MotionLayout`. – gabilcious Apr 04 '23 at 14:42
  • 1
    Probably the simplest way is to use ConstraintLayout constraintSets with animateChanges=true (Which will then use MotionLayout under the hood.) You create 3 constraintSets pick the right one on state change. – hoford Apr 04 '23 at 16:22
  • 1
    Updated to show 3 constraintSets – hoford Apr 04 '23 at 20:44