Create Interactive UI with Jetpack Compose and Motion Layout

Create Interactive UI with Jetpack Compose and Motion Layout

By reading this blog, you will learn to use Motion Layout to achieve this exquisite animation effect:
let's start with a simple introduction.

introduce

As an Android developer, you may come across situations where you need layout animations, sometimes even anamorphic style layout animations. This is where Motion Layout comes in.

It fills the gap between layout transitions and complex motion handling, providing a set of functions that sit between those of the property animation framework.

While Motion Layout has been around in XML Views for a while, it is relatively new in Jetpack Compose and is still evolving. In this comprehensive guide, we'll explore Motion Layout in Jetpack Compose, using an accordion toolbar as an example.

Collapsing toolbars have been an interesting theme in Android before Motion Layout. I'm sure you're already familiar with how to implement collapsing toolbars with complex animations using the old XML-based view system.

We will focus on how to achieve this complex folding effect in Jetpack Compose using Motion Layout.

some common action terms

  • Motion Layout- MotionLayout API for legacy view system.
  • Motion Compose- MotionLayout API for Jetpack Compose.
  • Motion Scene- Files that define various constraint sets, transitions, and keyframes for MotionLayout animations.
  • ConstraintSet- A set of constraints that define the initial and final layout states and any intermediate states for MotionLayout.
  • Transition- An animation sequence that occurs between two or more Constraint Sets in MotionLayout.
  • KeyAttribute- Properties that can animate views during MotionLayout transitions, such as position, size, or transparency values.
    In this blog, we will learn how to incorporate Motion Compose into Jetpack Compose.

Before Compose

First, a quick word. In XML based view system we use AppBarLayoutand CollapsingToolbarLayoutcreate collapsed app bar/toolbar which will also act CoordinatorLayoutas parent layout.

MotionLayoutThe XML file contains information about the transitions and animations of subviews.

Use in Compose

In Jetpack Compose we can achieve the same effect, almost everything is fully customizable and simple to implement!

Here, we've used a MotionLayoutdedicated Composablefunction called . MotionLayout Composable is added as a child element of the parent layout Composable, and child views are added directly as direct child elements of MotionLayout Composable.

Transitions and animations are MotionScenedefined using objects, which are created programmatically in Kotlin.

Why do you need MotionLayout?

Visuals are very important when it comes to compressing information so that users don't feel overwhelmed while navigating through an application.

Animations work seamlessly, with or without notches, hardware navigation, and more. While you don't need MotionLayoutto implement this, it provides a neat solution by allowing you to constrain the view's position to align with the layout.

Sometimes we may need to animate multiple combinations based on the keyframes of the animation, or we may need to do complex animations. That's MotionLayoutwhere the advantage is, it ConstraintSetssimplifies the whole process by defining, telling the animation what the layout/UI will look like at the beginning, how the layout/UI will look at the end, and then it will animate between MotionLayoutthose sets.

start

This document is based on Compose Constraint Layout version 1.0.1.

Include the following dependencies in the module-level build.gradlesection .dependencies

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"

Logically, we need to use the constraint layout dependency, since MotionLayout is a subclass of Constraint layout.

Let's take a look at the Compose version and explore how it differs from the traditional MotionLayout approach.

The difference between MotionLayout and MotionCompose

MotionLayoutMotionComposeThe first difference between and , MotionLayoutwhich allows developers to define animations in XML, MotionComposeis the new animation library introduced with Jetpack Compose. It provides a declarative way to create and control animations in Compose UI.

MotionComposeAims to provide MotionLayoutsimilar control and flexibility to , but in a more declarative and composable way.

MotionComposeAdvantages compared to MotionLayout:

  • more flexible
  • easier to use
  • More simplified syntax for creating animations
  • Easier to modify animations at runtime
  • Supports the creation of highly responsive and interactive animations that help create engaging user experiences seamlessly.

Overall, MotionLayoutand MotionComposeare powerful tools for working with motion and animation in Android. MotionLayoutMore for complex animations with lots of views and constraints, and MotionComposemore for creating smooth, fluid animations in a declarative and composable way. But for now let's call it MotionLayoutjust to avoid confusion.

overload

MotionLayoutThere are different types of functions, with different signatures. Some functions accept it MotionScene, while another corresponding method can directly MotionSceneadd the string as content.

MotionLayoutThere is a powerful set of properties, and the table below is a great resource that can help you with confusion about choosing the right method.

Keep in mind that using JSON5 will be easier to understand and tidy as the content of the screen grows. You can check out the overload options provided below according to your use case.

Motion Signature — 1

@ExperimentalMotionApi
@Composable
fun MotionLayout(
    start: ConstraintSet,
    end: ConstraintSet,
    transition: androidx.constraintlayout.compose.Transition? = null,
    progress: Float,
    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    crossinline content: @Composable MotionLayoutScope.() -> Unit
)

Motion Signature — 2

@ExperimentalMotionApi
@Composable
fun MotionLayout(
    motionScene: MotionScene,
    progress: Float,
    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    crossinline content: @Composable (MotionLayoutScope.() -> Unit),
)

Motion Signature — 3

@ExperimentalMotionApi
@Composable
fun MotionLayout(
    motionScene: MotionScene,
    constraintSetName: String? = null,
    animationSpec: AnimationSpec<Float> = tween<Float>(),
    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    noinline finishedAnimationListener: (() -> Unit)? = null,
    crossinline content: @Composable (MotionLayoutScope.() -> Unit)
)

Motion Signature — 4

@ExperimentalMotionApi
@Composable
fun MotionLayout(
    start: ConstraintSet,
    end: ConstraintSet,
    transition: androidx.constraintlayout.compose.Transition? = null,
    progress: Float,
    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
    informationReceiver: LayoutInformationReceiver? = null,
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    crossinline content: @Composable MotionLayoutScope.() -> Unit
)

In MotionLayout, there are two states to be animated. One is the start state and the other is the end state.

ProgressUsed to determine the progress of the current animation between the start state and the end state:

  • 0 means the current progress is at "beginning".
  • 1 indicates that the progress has reached "end".
  • 0.5 means you are currently in an intermediate state between the two, and so on.

MotionLayout for ComposeRealization constraint set for

It can be defined in two ways:

  1. MotionScenes Inside MotionLayout.
  2. JSON5 approach.
    Both approaches have pros and cons.

A description of the method MotionLayoutused inMotionScene

MotionSceneWe can add a string as content like this :

MotionLayout(
            start = ConstraintSet {
    
    
                ...
            },
            end = ConstraintSet {
    
    
                ...
            },
            progress = progress,
            modifier = Modifier
        ) {
    
    
          ...
        }

The downside of taking this approach is that it can become complex as the content grows.

Let's see an example:

@Composable
fun MyMotionLayout() {
    
    
    val motionScene = remember {
    
     MotionScene() }

    MotionLayout(
        modifier = Modifier.fillMaxSize(),
        motionScene = motionScene
    ) {
    
    
        Box(
            modifier = Modifier
                .constrainAs(box) {
    
    
                    start.linkTo(parent.start)
                    top.linkTo(parent.top)
                    end.linkTo(parent.end)
                    bottom.linkTo(parent.bottom)
                }
        ) {
    
    
            // Add your UI elements here
        }
    }

    // Define the start and end constraint sets
    motionScene.constraints(
        createConstraints(
            R.id.box,
            start = ConstraintSet {
    
    
                // Define your start constraints here
            },
            end = ConstraintSet {
    
    
                // Define your end constraints here
            }
        )
    )

    // Define the motion animations
    motionScene.transition(
        createTransition(
            R.id.box,
            fromState = R.id.start,
            toState = R.id.end
        ) {
    
    
            // Define your motion animations here
        }
    )
}

JSON5 method

This blog focuses on this approach, and you'll see an example of this approach in a moment.

First, create a JSON5file for storage MotionScene, the path is res/raw/motion_scene.json5.

The structure of the file might resemble the following:

{
    
    
  ConstraintSets: {
    
    
    start: {
    
    
      ....
    },
    end: {
    
    
      ....
    }
  }
}

Here, the start part contains all the constraints for the initial state of the animation, and the end part contains the constraints for the final state.

Now, we need to JSON5integrate the contents of the file into the Compose file.

You can openRawResourceinstantiate files located in the raw folder using the method JSON5.

Associating MotionSceneobjects with corresponding composable items can be achieved as follows:

val context = LocalContext.current
val motionScene = remember {
    
    
    context.resources
        .openRawResource(R.raw.motion_scene)
        .readBytes()
        .decodeToString()
}

MotionLayout(
    motionScene = MotionScene(content = motionScene),
) {
    
     ... }

Time to understand MotionScene

MotionScene files contain the following components:

  1. ConstraintSets(constraint set):
  • ConstraintSetsYes MotionScenebuilding blocks. They define the layout and style properties of UI elements.
  • One ConstraintSetcontains a set of constraints that specify the position, size, margins, padding, and other layout properties of each UI element.
  1. Transitions(transition):
  • A transition defines ConstraintSetsan animation or transition between two. They specify duration, easing, and other animation properties.
  • A transition can contain multiple keyframes ( KeyFrame), which define intermediate states of an animation or transition.
  • In the next sections, we will discuss in depth the properties used inside Transitions.
  1. KeyFrames(Keyframe):
  • Keyframes define the intermediate states of the transition. They specify properties of UI elements at specific points in an animation or transition.
  • A keyframe can contain a PropertySetsset of properties for specifying UI elements.
  1. PropertySets(property set):
  • PropertySetsSpecifies the property of the UI element in the keyframe.
  • They can contain properties such as position, size, margins, padding, background color, text color, etc.

Let's look at transitions
Think of transitions as containers that contain as many transitions as you want.

Each transition has a name. The "default" name is special, it defines the initial transition.

Below is an example of a transition. Please review Transitionsthe attributes used in the block and their meaning.

Transitions: {
    
    
    default: {
    
    
        from: 'start',
        to: 'end',
        pathMotionArc: 'startHorizontal',
        duration: 900
        staggered: 0.4,
        onSwipe: {
    
    
                anchor: 'box1',
                maxVelocity: 4.2,
                maxAccel: 3,
                direction: 'end',
                side: 'start',
                mode: 'velocity'
         }
        KeyFrames: {
    
    
        KeyPositions: [
            {
    
    
            target: ['a'],
            frames: [25, 50, 75],
            percentX: [0.4, 0.8, 0.1],
            percentY: [0.4, 0.8, 0.3]
            }
        ],
        KeyCycles: [
            {
    
    
                target: ['a'],
                frames: [0, 50, 100],
                period: [0 , 2 , 0],
                rotationX: [0, 45, 0],
                rotationY: [0, 45, 0], 
            }
        ]
    }
}

The above is the transition path from ConstraintSet "start" to "end".

Now let's look at transition terms

  1. from — Indicates ConstraintSetthe ID of the starting point.
  2. to – Indicates ConstraintSetthe ID of the end point.
  3. duration— Time required for transition.
  4. pathMotionArc— Move along a quarter ellipse arc.
  5. staggered— Objects move in a staggered fashion, which can be adjusted based on starting position or stagger value.
  6. onSwipe— Enable drag gestures to control transitions.
  7. KeyFrames(Keyframes) — Modify the points between transitions.

Some commonly used transition key attributes

  1. Alpha (transparency):
    You can apply the alpha attribute on a frame-by-frame basis within "KeyAttributes" in the JSON5 script.

alpha: [0.3, 0.5, 0.9, 0.5, 0.3]

  1. Visibility:

You can apply this property to subviews that we define as objects inside the start and end ConstraintSets.

  1. Scale:

Want to change the scaling of an image as it moves? This is where the scaleX and scaleY properties come into play.
scaleX — scales an object, such as an image, horizontally.
scaleY — scales the object vertically.
You can apply scaling attributes as follows, inside KeyAttributes as follows:
scaleX: [1, 2, 2.5, 2, 1], scaleY: [1, 2, 2.5, 2, 1]

  1. Elevation

It provides height, which is self-explanatory, right!

  1. Rotation:
  • rotationX— Rotate/flip/distort the object along the X axis.
  • rotationY— Rotate/flip/distort the object along the Y axis.
  1. Translation:

It allows you to control the positioning of the view on different axes.

  • translationX— for horizontal positioning.
  • translationY— for vertical positioning.
  • translationZ— The transition value is added to its height.

custom attributes

Compose provides a series of custom properties that can be used to achieve additional customization in the UI. However, it is important to note that these properties need to be extracted and set manually.

Typical collection of custom properties:

custom: {
    
    
    background: '#0000FF',
    textColor: '#FFFFFF',
    textSize: 12
}

To briefly understand how to use custom attributes, the following is an example using text color.

We use textColorattributes to apply the desired color attributes.

You can directly apply this property to the appropriate subview to make the desired changes.

Just add the hexadecimal color code after the "#". For example:#DF1F2D

motion_text: {
    
    
        end: ['motion_divider', 'end'],
        top: ['motion_divider', 'bottom', 16],
        custom: {
    
    
          textColor: '#2B3784'
        }
      }
      ```
您可以按以下方式设置自定义属性:
```kt
var myCustomProperties = motionProperties(id = "motion_text")

Text(text = "Hello Mind Dots!", modifier = Modifier
    .layoutId(myCustomProperties.value.id())
    .background(myCustomProperties.value.color("background"))
    ,color = myCustomProperties.value.color("textColor")
    ,fontSize = myCustomProperties.value.fontSize("textSize")
)

Debug animation path

To ensure accurate animations, MotionLayout provides a debug feature that shows the animation paths involved in all components.

To enable debugging, we just need to use the "debug" parameter.

It should be noted that by default, the debug value is set to
EnumSet.of(MotionLayoutDebugFlags.NONE).

Here you can see the path is represented with a dotted line.

These dotted lines will be very useful when working with complex animations, especially if you are looking for precision and consistency across devices with different sizes and resolutions.

Now it's time to dive into the code part

  1. Let's MotionScenestart with the definition file.
{
    
    
  ConstraintSets: {
    
     //Two constraint sets - Start and End
    //1. Collapsed
    start: {
    
    
      collapsing_box: {
    
    
        width: 'parent',
        height: 200,
        start: ['parent', 'start'],
        end: ['parent', 'end'],
        bottom: ['parent', 'top', -50],
        translationZ: -10,
        alpha: 0
      },
      data_content: {
    
    
        top: ['collapsing_box', 'bottom'],
        bottom: ['parent', 'bottom'],
        start: ['parent', 'start'],
        end: ['parent', 'end']
      },
      content_img: {
    
      // Assigned ID for profile pic, which we'll use in the code for the reference
        width: 90,
        height: 142,
        top: ['parent', 'top', 100], //top Constraint => [Constraining to what, where to, Margin value]
        start: ['parent', 'start', 16], //start Constraint
      },
      motion_text: {
    
    
        top: ['parent', 'top', 20],
        start: ['parent', 'start', 16],
        translationZ: -7
      },
      piranha_flower: {
    
    
        width: 40,
        height: 90,
        top: ['collapsing_box', 'bottom', -70],
        end: ['parent', 'end', 20],
        translationZ: -8
      },
      piranha_tunnel: {
    
    
        width: 60,
        height: 100,
        top: ['collapsing_box', 'bottom', -30],
        end: ['parent', 'end', 10],
        translationZ: -8
      }
    },
    //2. Expanded
    end: {
    
    
      collapsing_box: {
    
      //Background
        width: 'parent', 
        height: 200,
        start: ['parent', 'start'],
        end: ['parent', 'end'],
        top: ['parent', 'top'],
        translationZ: -10,
        alpha: 1
      },
      content_img: {
    
    
        width: 90,
        height: 142,
        top: ['data_content', 'top', -70], 
        start: ['parent', 'start', 4],
      },
      data_content: {
    
    
        top: ['collapsing_box', 'bottom'],
        start: ['collapsing_box', 'start'],
        end: ['collapsing_box', 'end']
      },
      motion_text: {
    
    
        bottom: ['collapsing_box', 'bottom', 10],
        start: ['content_img', 'end', 2]
      },
      piranha_flower: {
    
    
        width: 40,
        height: 90,
        top: ['collapsing_box', 'bottom', 80],
        end: ['parent', 'end', 20],
        translationZ: -10
      },
      piranha_tunnel: {
    
    
        width: 60,
        height: 100,
        top: ['collapsing_box', 'bottom', -20],
        end: ['parent', 'end', 10],
        translationZ: -10
      }
    }
  },
  Transitions: {
    
      //to set transition properties between Start and End point.
    default: {
    
    
      from: 'start',
      to: 'end',
      pathMotionArc: 'startHorizontal', // Text will move down with slight circular arc
      KeyFrames: {
    
    
        KeyAttributes: [  //We define different Attr and how we want this to Animate, during transition for a specific composable
          {
    
    
            target: ['content_img'],
            //[collapsed -> expanded]
            frames: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100], //For frames we pass a List containing number between 0 - 100
            rotationZ: [0, 9, 18, 27, 36, 45, 54, 63, 72, 81, 90, 81, 72, 63, 54, 45, 36, 27, 18, 9, 0],  //For dangling effect
            translationX: [0, 9, 18, 27, 36, 45, 54, 63, 72, 81, 90, 81, 72, 63, 54, 45, 36, 27, 18, 9, 0],
            translationY: [0, -14, -28, -42, -56, -70, -84, -98, -112, -126, -130, -126, -112, -98, -84, -70, -56, -42, -28, -14, 0],
            translationZ: [-1.0, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0, 0.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
          },
          {
    
    
            target: ['data_content'],
            frames: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100],  //For frames we pass a List containing number between 0 - 100
            translationY: [110, 98, 92, 87, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 2]
          }
        ]
      }
    }
  }
}
  1. Now we use Scaffold to implement the folding function. For this we need one file to represent the top bar and another file to represent the rest.
@Composable
fun MainScreenContent() {
    
    
    val marioToolbarHeightRange = with(LocalDensity.current) {
    
    
        MinToolbarHeight.roundToPx()..MaxToolbarHeight.roundToPx()
    }
    val toolbarState = rememberSaveable(saver = MiExitUntilCollapsedState.Saver) {
    
    
        MiExitUntilCollapsedState(marioToolbarHeightRange)
    }
    val scrollState = rememberScrollState()
    toolbarState.scrollValue = scrollState.value

    Scaffold(
        modifier = Modifier
            .fillMaxSize(),
        content = {
    
    
            MarioMotionHandler(
                list = populateList(),
                columns = 2,
                modifier = Modifier.fillMaxSize(),
                scrollState = scrollState,
                progress = toolbarState.progress
            )
        })
}
  1. Finally, add the list item content along with the collapse animation component. Here, we will use MotionScene files.
@Composable
fun MarioMotionHandler(
    list: List<MiItem>,
    columns: Int,
    modifier: Modifier = Modifier,
    scrollState: ScrollState = rememberScrollState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    progress: Float
) {
    
    
    val context = LocalContext.current
    val chunkedList = remember(list, columns) {
    
    
        list.chunked(columns)
    }
    // To include raw file, the JSON5 script file
    val motionScene = remember {
    
    
        context.resources.openRawResource(R.raw.motion_scene_mario)
            .readBytes()
            .decodeToString()   //readBytes -> cuz we want motionScene in a String format
    }

    MotionLayout(
        motionScene = MotionScene(content = motionScene),
        progress = progress,
        modifier = Modifier
            .fillMaxSize()
            .background(MarioRedLight)
    ) {
    
    

        /**
         * bg - image
         **/
        Image(
            painter = painterResource(id = R.drawable.ic_mario_level),
            contentDescription = "Toolbar Image",
            contentScale = ContentScale.FillBounds,
            modifier = Modifier
                .layoutId("collapsing_box")
                .fillMaxWidth()
                .drawWithCache {
    
    
                    val gradient = Brush.verticalGradient(
                        colors = listOf(Color.Transparent, Color.Black),
                        startY = size.height / 3,
                        endY = size.height
                    )
                    onDrawWithContent {
    
    
                        drawContent()
                        drawRect(gradient, blendMode = BlendMode.Multiply)
                    }
                },
            alignment = BiasAlignment(0f, 1f - ((1f - progress) * 0.50f)),
        )

        /**
         * Text - Collapsing
         */
        Text(
            text = stringResource(id = R.string.collapsing_text_minion),
            color = MarioRedDark,
            modifier = Modifier
                .layoutId("motion_text")
                .zIndex(1f),
            fontFamily = FontFamily(
                Font(R.font.super_mario_bros, FontWeight.Light)
            ),
            fontSize = 14.sp
        )

        /**
         * Main image
         **/
        Image(
            painter = painterResource(id = R.drawable.ic_mario_reversed),
            contentScale = ContentScale.Fit,
            modifier = Modifier
                .layoutId("content_img")
                .clip(RoundedCornerShape(5.dp)),
            contentDescription = "Animating Mario Image"
        )

        /**
         * Grid
         **/
        Column(
            modifier = modifier
                .verticalScroll(scrollState)
                .layoutId("data_content")
                .background(MarioRedLight),
        ) {
    
    
            Spacer(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(contentPadding.calculateTopPadding())
            )

            chunkedList.forEach {
    
     chunk ->
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .wrapContentHeight()
                ) {
    
    

                    Spacer(
                        modifier = Modifier
                            .fillMaxHeight()
                            .width(contentPadding.calculateStartPadding(LocalLayoutDirection.current))
                    )

                    chunk.forEach {
    
     listItem ->
                        GridCharacterCard(
                            miItem = listItem,
                            modifier = Modifier
                                .padding(2.dp)
                                .weight(1f)
                        )
                    }

                    val emptyCells = columns - chunk.size
                    if (emptyCells > 0) {
    
    
                        Spacer(modifier = Modifier.weight(emptyCells.toFloat()))
                    }

                    Spacer(
                        modifier = Modifier
                            .fillMaxHeight()
                            .width(contentPadding.calculateEndPadding(LocalLayoutDirection.current))
                    )
                }
            }

            Spacer(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(140.dp)
            )
        }

        /**
         * piranha flower
         **/
        Image(
            painter = painterResource(id = R.drawable.ic_piranha_flower),
            contentScale = ContentScale.Fit,
            modifier = Modifier
                .layoutId("piranha_flower"),
            contentDescription = "Piranha Flower"
        )

        /**
         * piranha tunnel
         **/
        Image(
            painter = painterResource(id = R.drawable.ic_piranha_tunnel),
            contentScale = ContentScale.Fit,
            modifier = Modifier
                .layoutId("piranha_tunnel"),
            contentDescription = "Piranha Tunnel"
        )
    }
}

The grid list is implemented as follows:

@Composable
fun GridCharacterCard(
    miItem: MiItem,
    modifier: Modifier = Modifier
) {
    
    
    Card(
        modifier = modifier.aspectRatio(0.66f),
        shape = RoundedCornerShape(8.dp)
    ) {
    
    
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(Gray245)
        ) {
    
    
            miItem.itemImage?.let {
    
     painterResource(it) }?.let {
    
    
                Image(
                    painter = it,
                    contentDescription = miItem.itemDescription,
                    contentScale = ContentScale.FillWidth,
                    modifier = Modifier
                        .padding(35.dp)
                        .fillMaxWidth()
                )
            }
            TopBar()
            miItem.itemName?.let {
    
     BottomBar(it) }
        }
    }
}

@Composable
private fun BoxScope.TopBar() {
    
    
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(0.093f)
            .background(MarioRedDark)
            .padding(horizontal = 8.dp, vertical = 2.dp)
            .align(Alignment.TopCenter)
    ) {
    
    
        Row(
            modifier = Modifier
                .fillMaxHeight(0.75f)
                .wrapContentWidth()
                .align(Alignment.CenterStart),
            horizontalArrangement = Arrangement.spacedBy(2.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
    
    
            Icon(
                imageVector = Icons.Rounded.Star,
                contentDescription = "Golden star 1",
                tint = GoldYellow
            )
            Icon(
                imageVector = Icons.Rounded.Star,
                contentDescription = "Golden star 2",
                tint = GoldYellow
            )
            Icon(
                imageVector = Icons.Rounded.Star,
                contentDescription = "Golden star 3",
                tint = GoldYellow
            )
        }

        Row(
            modifier = Modifier
                .fillMaxHeight(0.75f)
                .wrapContentWidth()
                .align(Alignment.CenterEnd),
            horizontalArrangement = Arrangement.spacedBy(2.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
    
    
            Image(
                painter = painterResource(id = R.drawable.ic_coin),
                contentScale = ContentScale.Fit,
                modifier = Modifier
                    .clip(RoundedCornerShape(5.dp)),
                contentDescription = "Coin"
            )
            Text(
                text = "87",
                color = Color.Black,
                modifier = Modifier,
                fontFamily = FontFamily(
                    Font(R.font.super_mario_bros, FontWeight.Normal)
                ),
            )
        }
    }
}

@Composable
private fun BoxScope.BottomBar(text: String) {
    
    
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(0.14f)
            .background(MarioRedDark)
            .align(Alignment.BottomCenter)
    ) {
    
    
        Text(
            text = text,
            textAlign = TextAlign.Center,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
            modifier = Modifier
                .fillMaxWidth()
                .align(Alignment.Center),
            fontFamily = FontFamily(
                Font(R.font.super_mario_bros, FontWeight.Normal)
            )
        )
    }
}

Code analysis is complete, see the final effect

in conclusion

With that said, I hope this blog has inspired you to explore the endless possibilities of using in Jetpack Compose MotionLayout. Keep experimenting and pushing the boundaries of this powerful framework. You can access the source code from Github.

GitHub

https://github.com/Mindinventory/MarioInMotion

Guess you like

Origin blog.csdn.net/u011897062/article/details/131985043