使用JetPack组成的运动布局创建交互式UI
#android #jetpackcompose #motionlayout #motionanimation

浏览了此博客后,您将使用MotionLayout实现这种抛光动画:

让婴儿脚步踢起来,从介绍开始。

介绍

作为Android开发人员,您可能遇到了对布局动画的需求,有时甚至是变形风格的布局动画。那是Motionlayout进入图片的位置。

它填补了布局过渡和复杂运动处理之间的鸿沟,提供了各种功能,这些功能位于属性动画框架的功能之间。

虽然Motionlayout在XML视图中已经存在了一段时间,但它在JetPack构图中相当新,并且仍在成熟。在本综合指南中,我们将探索JetPack中的MotionLayout,其中包含一个折叠工具栏的示例。

在进行Motionlayout之前,崩溃的工具栏一直是Android中的一个有趣的主题。我相信您非常熟悉如何延长具有棘手动画的崩溃工具栏的实现,并且对基于XML的旧视图系统有点单调。

我们将关注如何使用JetPack组成的Motionlayout实现如此复杂的崩溃效果。

一些常见的运动术语

  1. Motionlayout 旧视图系统的MotionLayout API。

  2. MotionCompose jetpack组成的Motionlayout API

  3. Motionscene 定义了动画动画的各种约束集,过渡和密钥帧。

  4. 约束集 一组限制因素,该约束定义初始和最终的布局状态以及任何中间状态,用于MotionLayout。

  5. 过渡 - 在运动层中两个或多个约束集之间发生的动画序列。

  6. keyAttribute - 在MotionLayout过渡期间可以动画的视图属性,例如其位置,大小或alpha值。

在此博客中,我们将学习如何将MotionCompose纳入Jetpack Compose的世界中。

在撰写之前的时间雾中

首先,快速绕道。在基于XML的视图系统中,我们使用AppBarlayout和CollapsingToolBarlayout创建了折叠的应用程序栏/工具栏,同时将CoordinatorLayout作为父布局。

MotionLayout XML文件包含有关儿童视图的过渡和动画的信息。

它如何撰写

我们可以在JetPack组合中实现同样的成就,几乎所有内容都是完全可定制且易于实现的!

在这里,它使用称为MotionLayout的专用组合函数实现。 MotionLayout组合作为子元素添加到父元件中,并添加了儿童视图作为Motionlayout Composoble的直接儿童。

使用Motionscene对象定义了过渡和动画,该对象是在Kotlin中编程创建的。

Tenor giphy

为什么需要Motionlayout?

视觉插图在凝结信息时非常重要,因此用户在通过您的应用程序冲浪时不会感到不知所措。

动画无缝地工作,无论档位,硬件导航等是否存在或缺乏。现在,您不需要Motionlayout来实现此目的,但是,它可以通过允许您约束视图的位置以与布局保持一致。

有时,我们可能需要根据动画的关键帧来对可组合的众多属性/属性进行动画,或者我们可能希望拥有一个复杂的动画。这是MotionLayout真正发光的地方,通过定义约束点来简化整个过程,这些约束点可以说明布局/UI如何看待动画的起点以及在末尾的样子,并且Simply Motionlayout将通过这些集合进行动画。

开始

此文档基于组合约束布局1.0.1。

在模块级构建中包含以下依赖性。Gradle的依赖项部分。

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

从逻辑上讲,我们需要使用约束布局依赖关系,因为Motionlayout是约束布局的子类。

让我们看看撰写的对应物并探索其与传统的Motionlayout方法的独特性。

Tenor giphy

MotionLayout与MotionCompose

MotionLayout和MotionCompose之间的第一个差异是,MotionLayout允许开发人员在XML中定义动画,另一方面,MotionCompose是一个新的动画库,该动画库是用JetPack组成的。它提供了一种声明性的方式来创建和控制Compose UI中的动画。

MotionCompose旨在提供与MotionLayout相似的控制和灵活性水平,但以更有声明性和可组合的方式提供。

运动compose超过运动layout的好处:

  • 更灵活性

  • 易于使用

  • 用于创建动画的简化语法

  • 在运行时易于修改动画

  • 赋予了高度响应和互动的动画的创建,从而促进了迷人的用户体验的无缝创造。

总而言之,MotionLayout和MotionCompose都是在Android中处理运动和动画的强大工具。 MotionLayout更适合具有大量视图和约束的复杂动画,而MotionCompose更适合以声明性且可复合的方式创建平滑而流畅的动画。但是目前,我们将其称为Motionlayout,以避免任何混乱。

超载

有不同类型的MotionLayout功能,具有不同的签名。某些功能接受Motionscene和另一个功能,您可以直接添加一个Motionscene字符串作为内容。

MotionLayout在其箭袋中具有强大的属性,下表是一个重要的资源,可以解决您选择正确的方法的混乱。

请记住,随着屏幕内容的增长,它变得令人困惑,因此,为了使其变得容易和干净,JSON5是可取的。您可以根据用例仔细阅读下面显示的过载选项。

运动签名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
)

运动签名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),
)

运动签名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)
)

运动签名 - 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
)

第一个过载是最原始的。

在Motionlayout中,有两个状态可以动画。一个是开始状态,另一个是结束状态。

进度用于确定起始状态和结束状态之间动画的当前状态:

  • 0表示当前的进度在开始。

  • 1意味着进步已达到末端。

  • 0.5表示电流位于两个的中间,等等。

Compose的Motionlayout

约束集可以通过两种方式定义:

  1. Motionscenes Motionlayout内部。

  2. JSON5方法。

两种方法都有其优点和缺点。

MotionLayout中的Motcene程序的描述

我们可以添加一个类似内容的Motcence字符串。

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

采用这种方法的缺点是,随着内容的增长,它可能会感到困惑。

让我们看一个示例:

@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方法

此博客主要关注这种方法,您将在片刻中看到此方法的示例。

首先,在 res/raw/motion_scene.json5

> <> res/raw/toper_scene.json.json5文件中创建一个JSON5文件。

文件的结构有点像这样:

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

这里开始包含最初运动状态的所有约束,结束包括最终状态的约束。

现在,我们必须将JSON5文件内容合并到撰写文件中。

使用OpenRawResource您可以实例化位于RAW文件夹中的JSON5文件。

可以以这种方式链接运动对象与适当的组合对象。

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

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

Tenor giphy

是时候理解Motionscene

Miotcene文件由以下组件组成:

1。约束点:

  • 约束分子是动议量的基础。他们定义了UI元素的布局和样式属性。
  • 约束集包含一组约束,该约束指定每个UI元素的位置,大小,边距,填充和其他布局属性。

2。过渡:

  • 过渡定义了两个约束点之间的动画或过渡。他们指定了持续时间,轻松和其他动画属性。
  • 过渡可以包含多个密钥帧,该框架定义了动画或过渡的中间状态。
  • 我们将深入讨论即将到来的部分中使用的属性。

3。关键框架:

  • 密钥帧定义过渡的中间状态。他们在动画或过渡中的特定点指定UI元素的属性。
  • 一个密钥帧可以包含一组propertyset,该属性集指定UI元素的属性。

4。 propertyset:

  • propertysets在密钥帧中指定UI元素的属性。
  • 它们可以包含位置,大小,边距,填充,背景颜色,文本颜色等属性。

让我们看一下过渡

根据必要性将过渡视为包含任意过渡量的容器。

每个过渡都有一个名称。默认值是特殊的,并定义了初始过渡。

下面给出了过渡的例证。查看过渡块中如何和哪些属性。

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], 
            }
        ]
    }
}

以上是从约束集开始到末端路径的过渡。

调查过渡术语的时间ð

  1. 来自约束集的表示起点。

  2. to

  3. 结束
  4. 持续时间过渡到发生的时间。

  5. pathmotionarc 移动四分之一椭圆形弧。

  6. 交错的物体以交错的方式移动。基于起始位置或交错值。

  7. onswipe 使阻力手势控制过渡。

  8. keyframes 修改过渡之间的点。

一些常用的过渡密钥属性

  1. Alpha:
  • 您可以在JSON5脚本内的KeyAttributes中应用Alpha属性框架。

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

2。可见性:

  • 您可以将此属性应用于我们在开始和最终约束中定义为对象的孩子的视图。

3。比例:

  • 在移动时更改图像的比例?

  • scalex <缩放像图像这样的对象,本质上是水平的。

  • 比例 垂直缩放对象。

  • 您可以以以下方式应用缩放属性,如下所示:

    scalex:[1,2,2.5,2,1],
    比例:[1,2,2.5,2,1]

4。海拔

  • 它提供了自我解释的高程,对!

5。旋转:

  • rotationx '旋转/翻转/旋转X轴上的对象。

  • rotationy - 旋转/翻转/旋转y轴上的对象。

6。翻译:

  • 它使您能够控制视图在不同轴上的定位。

  • translationx 用于水平定位。

  • 翻译用于垂直定位。

  • translationz transition值添加到其高程中。

自定义属性

撰写提供了一系列自定义属性,可用于在UI中实现其他自定义。但是,重要的是要注意,需要提取这些属性并手动设置。

典型的自定义属性集:

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

简而言

我们使用TextColor属性应用所需的颜色属性。

您可以将此属性直接应用于各自的儿童视图,您想进行所需的更改。

只需在主题标签之后使用六角形即可。例如#DF1F2D

motion_text: {
        end: ['motion_divider', 'end'],
        top: ['motion_divider', 'bottom', 16],
        custom: {
          textColor: '#2B3784'
        }
      }

您可以以下方式设置自定义属性:

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")
)

调试动画路径

确保精确的动画Motionlayout提供了一个调试设施,该设施将展示所有涉及的组件的动画路径。

为了启用调试,我们只能使用debug参数来做到这一点。

请注意,默认情况下,调试值设置为 enumset.of(MotionlayOutDebugflags.none)

在这里您可以看到路径用虚线表示。

这些虚线将进行救援,尤其是当您处理复杂的动画时,在各种设备之间寻求不同尺寸和分辨率的设备的精确性和一致性时。

现在是时候潜入代码部分了

  1. 让我们从定义Motionscene文件开始。

    {
      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]
              }
            ]
          }
        }
      }
    }
    
  2. 现在我们使用了脚手架来实现崩溃的功能。为此,我们需要一个文件来代表顶部栏,另一个文件为其余内容。

@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. 最终添加列表项目内容以及崩溃的动画组件。在这里,我们将利用Motionscene文件。
    @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"
            )
        }
    }

网格列表角色卡的文件。

@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)
                )
            )
        }
    }

要完全掌握代码段的影响,我建议您像矩阵中的Neo一样仔细。

关闭

现在是包装!

我希望这个博客能激发您探索JetPack撰写的Motionlayout的无尽可能性。通过这个强大的框架进行实验和突破可能的界限。

您可以从Github访问源代码。
GitHub - Mindinventory/MarioInMotion: Creating a collapsing toolbar with delightful animation in…

请继续关注更多的喷气背包构成技巧和技巧。 -