Skip to content

MotionLayout based Collapsing toolbar

John Hoford edited this page Oct 5, 2022 · 3 revisions

MotionLayout based Collapsing toolbar

This is an example of how to use MotionLayout to create a collapsing toolbar

swipe

swipe.mp4

This is an example of how to use Compose MotionLayout as a Collapsing toolbar.

The basic structure

  • s = remberScrollState() - to access the scrolling value
  • Column(Modifier.verticalScroll(s)) - provide the scrolling list
    • Spacer() - pad the start of the list
    • Text() - after the padding
  • MotionLayout( progress=s.value/(open_height-close_height)) - motionLayout to setup
    • Composable being animated

Sample Code implemented using JSON5 syntax

@OptIn(ExperimentalMotionApi::class)
@Preview(group = "scroll", device = "spec:shape=Normal,width=480,height=800,unit=dp,dpi=440")
@Composable
fun ScrollText() {
    val scroll = rememberScrollState(0)

    var scene = """
      {
        ConstraintSets: {
          start: {
            title: {
              bottom: ['image', 'bottom', 16],                
              start: [ 'image','start', 16],
              },
            image: {
              width: 'parent',
              height: 250,
              top: ['parent', 'top', 0],
              custom: {
                cover: '#000000FF'
              }
            },
            icon: {
              top: ['image', 'top', 16],
              start: [ 'image','start', 16],
              alpha: 0,
            },
          },
          end: {
            title: {
              centerVertically: 'image',
              start: ['icon', 'end', 0],
              scaleX: 0.7,
              scaleY: 0.7,
            },
            image: {
              width: 'parent',
              height: 50,
              top: ['parent', 'top', 0],
              custom: {
                cover: '#FF0000FF'
              }
            },
            icon: {
              top: ['image', 'top', 16],
              start: [ 'image','start', 16],
            },
          },
        },
        Transitions: {
          default: {
            from: 'start',
            to: 'end',
            pathMotionArc: 'startHorizontal',
          },
        },
      }
      """

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.verticalScroll(scroll)
    ) {
        Spacer(Modifier.height(250.dp))
        repeat(5) {
            Text(
                text = LoremIpsum(222).values.first(),
                modifier = Modifier
                    .background(Color.White)
                    .padding(16.dp)
            )
        }
    }

    val progress = min(scroll.value / (3f * (250 - 50)), 1f);

    MotionLayout(
        modifier = Modifier.fillMaxSize(),
        motionScene = MotionScene(content = scene),
        progress = min((scroll.value) / 600f, 1f)
    ) {
        Image(
            modifier = Modifier.layoutId("image"),
            painter = painterResource(R.drawable.bridge),
            contentDescription = null,
            contentScale = ContentScale.Crop
        )
        Box(modifier = Modifier
            .layoutId("image")
            .background(motionProperties("image").value.color("cover"))) {
        }
        Image(
            modifier = Modifier.layoutId("icon"),
            painter = painterResource(R.drawable.menu),
            contentDescription = null
        )
        Text(
            modifier = Modifier.layoutId("title"),
            text = "San Francisco",
            fontSize = 30.sp,
            color = Color.White
        )
    }
}

Sample Code implemented using MotionLayout DSL syntax

@Composable
fun ScrollTextDSL() {
    val scroll = rememberScrollState(0)
    val big = 250.dp
    val small = 50.dp
    var scene = MotionScene() {
        val start1 = constraintSet {
            val title = createRefFor("title")
            val image = createRefFor("image")
            val icon = createRefFor("icon")
            constrain(title) {
                bottom.linkTo(image.bottom)
                start.linkTo(image.start)
            }
            constrain(image) {
                width = Dimension.matchParent
                height = Dimension.value(big)
                top.linkTo(parent.top)
                customColor("cover", Color(0x000000FF))
            }
            constrain(icon) {
                top.linkTo(image.top, 16.dp)
                start.linkTo(image.start, 16.dp)
                alpha = 0f
            }
        }
        val end1 = constraintSet {
            val title = createRefFor("title")
            val image = createRefFor("image")
            val icon = createRefFor("icon")
            constrain(title) {
                bottom.linkTo(image.bottom)
                start.linkTo(icon.end)
                centerVerticallyTo(image)
                scaleX = 0.7f
                scaleY = 0.7f
            }
            constrain(image) {
                width = Dimension.matchParent
                height = Dimension.value(small)
                top.linkTo(parent.top)
                customColor("cover", Color(0xFF0000FF))
            }
            constrain(icon) {
                top.linkTo(image.top, 16.dp)
                start.linkTo(image.start, 16.dp)
            }
        }
        transition("default", start1, end1) {}
    }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.verticalScroll(scroll)
    ) {
        Spacer(Modifier.height(big))
        repeat(5) {
            Text(
                text = LoremIpsum(222).values.first(),
                modifier = Modifier
                    .background(Color.White)
                    .padding(16.dp)
            )
        }
    }
    val gap =  with(LocalDensity.current){big.toPx() - small.toPx()}
    val progress = min(scroll.value / gap, 1f);

    MotionLayout(
        modifier = Modifier.fillMaxSize(),
        motionScene = scene,
        progress = progress
    ) {
        Image(
            modifier = Modifier.layoutId("image"),
            painter = painterResource(R.drawable.bridge),
            contentDescription = null,
            contentScale = ContentScale.Crop
        )
        Box(modifier = Modifier
            .layoutId("image")
            .background(motionProperties("image").value.color("cover"))) {
        }
        Image(
            modifier = Modifier.layoutId("icon"),
            painter = painterResource(R.drawable.menu),
            contentDescription = null
        )
        Text(
            modifier = Modifier.layoutId("title"),
            text = "San Francisco",
            fontSize = 30.sp,
            color = Color.White
        )
    }
}