使用Jetpack Compose和Motion Layout创建交互式UI

news2024/11/16 12:29:49

使用Jetpack Compose和Motion Layout创建交互式UI

通过阅读本博客,您将学会使用Motion Layout实现这种精致的动画效果:
让我们从简单的介绍开始。

介绍

作为Android开发者,您可能会遇到需要布局动画的情况,有时甚至需要变形样式的布局动画。这就是Motion Layout的用武之地。

它填补了布局转换和复杂动作处理之间的空白,提供了一系列位于属性动画框架功能之间的功能。

虽然Motion Layout在XML视图中已经存在了一段时间,但在Jetpack Compose中还是相对较新,并且仍在不断发展。在这份全面的指南中,我们将探讨Jetpack Compose中的Motion Layout,并以折叠工具栏为例。

在使用Motion Layout之前,折叠工具栏在Android中一直是一个有趣的主题。相信您对如何使用旧的基于XML的视图系统实现折叠工具栏并附带复杂动画的情况已经很熟悉了。

我们将重点讨论如何使用Motion Layout在Jetpack Compose中实现这种复杂的折叠效果。

一些常见的动作术语

  • Motion Layout - 用于旧视图系统的MotionLayout API。
  • Motion Compose - 用于Jetpack Compose的MotionLayout API。
  • Motion Scene - 定义MotionLayout动画的各种约束集、过渡和关键帧的文件。
  • ConstraintSet - 一组约束,用于为MotionLayout定义初始和最终布局状态以及任何中间状态。
  • Transition - 在MotionLayout中的两个或多个Constraint Set之间发生的动画序列。
  • KeyAttribute - 在MotionLayout转换期间可以对视图进行动画处理的属性,例如位置、大小或透明度值。
    在本博客中,我们将学习如何将Motion Compose结合到Jetpack Compose中。

在Compose之前

首先,简单地说一下。在基于XML的视图系统中,我们使用AppBarLayoutCollapsingToolbarLayout创建折叠的应用栏/工具栏,同时将CoordinatorLayout作为父布局。

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

在Compose中的使用

在Jetpack Compose中我们可以实现相同的效果,几乎一切都可以完全自定义和简单实现!

在这里,我们使用了一个名为MotionLayout的专用Composable函数。MotionLayout Composable作为父布局Composable的子元素添加,而子视图则直接作为MotionLayout Composable的直接子元素添加。

过渡和动画是使用MotionScene对象定义的,该对象是以Kotlin编程方式创建的。

为什么需要MotionLayout?

在压缩信息以便用户在浏览应用程序时不会感到不知所措时,视觉效果非常重要。

动画无缝地工作,无论是否有刘海屏、硬件导航等等。虽然您不需要MotionLayout来实现这一点,但它提供了一个简洁的解决方案,通过允许您约束视图的位置与布局对齐。

有时我们可能需要根据动画的关键帧来对多个组合进行动画处理,或者可能需要进行复杂的动画处理。这就是MotionLayout的优势所在,它通过定义ConstraintSets来简化整个过程,告诉动画在开始时布局/界面的外观如何,在结束时布局/界面的外观又如何,然后MotionLayout会在这些集合之间进行动画处理。

开始

本文档基于Compose Constraint Layout版本1.0.1。

在模块级build.gradledependencies部分中包含以下依赖项。

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

从逻辑上讲,我们需要使用constraint layout依赖项,因为MotionLayout是Constraint layout的子类。

让我们来看一下Compose版本,并探索它与传统MotionLayout方法的不同之处。

MotionLayout与MotionCompose的区别

MotionLayoutMotionCompose之间的第一个不同之处在于,MotionLayout允许开发者在XML中定义动画,而MotionCompose是随Jetpack Compose引入的新的动画库。它提供了一种声明式的方式来创建和控制Compose UI中的动画。

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

MotionCompose相比MotionLayout的优势:

  • 更灵活
  • 更易于使用
  • 更简化的语法用于创建动画
  • 更容易在运行时修改动画
  • 支持创建高度响应和交互式的动画,有助于无缝创建引人入胜的用户体验。

总的来说,MotionLayoutMotionCompose都是在Android中处理动作和动画的强大工具。MotionLayout更适用于具有大量视图和约束的复杂动画,而MotionCompose更适用于以声明式和可组合的方式创建平滑流畅动画。但暂时我们将其称为MotionLayout以避免混淆。

重载

MotionLayout有不同类型的函数,具有不同的签名。某些函数接受MotionScene,而另一种对应的方法则可以直接将MotionScene字符串作为内容添加。

MotionLayout有一系列强大的属性,下表是一个重要的资源,可以帮助您解决选择正确方法时的困惑。

请记住,随着屏幕内容的增长,使用JSON5将会更易于理解和整洁。您可以根据您的用例查看下面所提供的重载选项。

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
)

MotionLayout中,有两个要进行动画处理的状态。一个是起始状态,另一个是结束状态。

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

  • 0 表示当前进度在“开始”处。
  • 1 表示进度已达到“结束”。
  • 0.5 表示当前位于两者之间的中间状态,依此类推。

MotionLayout for Compose的实现约束集

可以通过以下两种方式定义:

  1. MotionScenes Inside MotionLayout.
  2. JSON5 approach.
    这两种方法各有优缺点。

MotionLayout中使用MotionScene的方法的描述

我们可以像这样添加一个MotionScene字符串作为内容:

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

本博客主要关注此方法,并且您将在片刻后看到此方法的示例。

首先,创建一个JSON5文件,用于存放MotionScene,路径为res/raw/motion_scene.json5

文件的结构可能类似于以下内容:

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

这里,start部分包含了动画的初始状态的所有约束,而end部分包含了最终状态的约束。

现在,我们需要将JSON5文件的内容整合到Compose文件中。

您可以使用openRawResource方法实例化位于raw文件夹中的JSON5文件。

MotionScene对象与相应的可组合项进行关联,可以按照以下方式实现:

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

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

时间来理解MotionScene

MotionScene文件包含以下组件:

  1. ConstraintSets(约束集):
  • ConstraintSetsMotionScene的构建块。它们定义了UI元素的布局和样式属性。
  • 一个ConstraintSet包含一组约束,这些约束指定了每个UI元素的位置、大小、边距、内边距和其他布局属性。
  1. Transitions(过渡):
  • 过渡定义了两个ConstraintSets之间的动画或过渡。它们指定了持续时间、缓动和其他动画属性。
  • 一个过渡可以包含多个关键帧(KeyFrame),用于定义动画或过渡的中间状态。
  • 在接下来的部分中,我们将深入讨论在Transitions内部使用的属性。
  1. KeyFrames(关键帧):
  • 关键帧定义了过渡的中间状态。它们指定了动画或过渡中特定时间点上UI元素的属性。
  • 一个关键帧可以包含一组PropertySets,用于指定UI元素的属性。
  1. PropertySets(属性集):
  • PropertySets指定关键帧中UI元素的属性。
  • 它们可以包含位置、大小、边距、内边距、背景颜色、文本颜色等属性。

让我们来看看过渡
将过渡视为根据需要包含任意数量的过渡的容器。

每个过渡都有一个名称。“default”名称是特殊的,它定义了初始过渡。

下面是一个过渡的示例。请查看Transitions块中使用的属性及其含义。

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

以上是从ConstraintSet“start”到“end”的过渡路径。

现在来研究一下过渡术语

  1. from — 指示起始点的ConstraintSet的ID。
  2. to — 指示结束点的ConstraintSet的ID。
  3. duration — 过渡所需的时间。
  4. pathMotionArc — 沿四分之一椭圆弧移动。
  5. staggered — 对象以交错方式移动,可以基于起始位置或stagger值进行调整。
  6. onSwipe — 启用拖动手势来控制过渡。
  7. KeyFrames(关键帧) — 修改过渡之间的点。

一些常用的过渡关键属性

  1. Alpha(透明度):
    您可以在JSON5脚本中的“KeyAttributes”内逐帧应用透明度属性。

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

  1. Visibility(可见性):

您可以将此属性应用于我们在起始和结束ConstraintSets内定义为对象的子视图。

  1. Scale(缩放):

想要在图像移动时改变其缩放比例?这就是scaleX和scaleY属性发挥作用的地方。
scaleX — 水平缩放对象,例如图像。
scaleY — 垂直缩放对象。
您可以按照以下方式应用缩放属性,如下所示在KeyAttributes内:
scaleX: [1, 2, 2.5, 2, 1], scaleY: [1, 2, 2.5, 2, 1]

  1. Elevation(高度)

它提供了高度,这是不言自明的,对吧!

  1. Rotation(旋转):
  • rotationX — 沿X轴旋转/翻转/扭曲对象。
  • rotationY — 沿Y轴旋转/翻转/扭曲对象。
  1. Translation(平移):

它允许您在不同的轴上控制视图的定位。

  • translationX — 用于水平定位。
  • translationY — 用于垂直定位。
  • translationZ — 过渡值被添加到其高度。

自定义属性

Compose提供了一系列自定义属性,可用于在UI中实现额外的定制。但是,需要注意的是这些属性需要手动提取和设置。

典型的自定义属性集合:

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

简要了解如何使用自定义属性,以下是一个使用文本颜色的例子。

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

您可以直接将此属性应用于要进行所需更改的相应子视图。

只需在“#”后面加上十六进制颜色代码。例如:#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")
)

调试动画路径

为了确保精确的动画,MotionLayout提供了一个调试功能,展示了所有组件涉及的动画路径。

要启用调试,我们只需要使用“debug”参数即可。

需要注意的是,默认情况下,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]
          }
        ]
      }
    }
  }
}
  1. 现在我们使用了 Scaffold 来实现折叠功能。为此,我们需要一个文件来表示顶部栏,另一个文件来表示其余内容。
@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)
            )
        )
    }
}

代码分析完成,看看最终效果

结论

到此为止,希望这篇博客能激发你对使用 Jetpack Compose 中的 MotionLayout 的无限可能性的探索。继续尝试并推动这个强大框架的边界。你可以从Github访问源代码。

GitHub

https://github.com/Mindinventory/MarioInMotion

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/806072.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux安装kafka3.5.1

要在Ubuntu上安装Apache Kafka&#xff0c;请按照以下步骤操作&#xff1a; 1、安装Java运行时环境(Ubuntu)&#xff1a; 如果已经安装jdk不用执行 sudo apt update sudo apt install default-jre2、下载Kafka&#xff1a; wget https://downloads.apache.org/kafka/3.5.1/…

Docker容器基本操作之启动-停止-重启

一、安装启动RabbitMQ容器 此处以rabbitmq容器为例 前提&#xff1a;需要安装配置好docker(设置镜像源、配置阿里云加速)、开启docker&#xff0c;停止(stop)或者禁用(disable)手动解压缩安装的rabbitmq,以防与docker中安装的rabbitmq冲突。 //查看docker状态 systemctl stat…

android Glide加载gif动图和本地视频,Java

droid Glide加载gif动图和本地视频&#xff0c;Java //从手机存储本地加载视频 String filePath "/storage/emulated/0/Pictures/my_video.mp4"; Glide .with( context ).load( Uri.fromFile( new File( filePath ) ) ).into( imageView );//加载gif Glide .with(…

用群晖NAS搭建个人音乐库

安装教程 勾选启动NTP服务 1. 群晖安装Audio Station 2. filestation会生成一个music文件夹&#xff0c;把下载好的音乐丢进music即可&#xff08;音乐平台听不到的歌也顺带通过下载解决了&#xff09;这时候你就可以在audio station直接听歌了不过咱们大部分时候听歌是在手机…

恒运资本:深市半年报预告规定?

依据深圳证券交易所等法律法规和业务规范的要求&#xff0c;应当及时、公平、精确发表成绩预告、成绩快报、中报、年度报告等信息。涉及中长期成绩情况的&#xff0c;还应当发表未来发展计划、运营战略、面对的风险与应战以及处理措施等中长期内容。 半年报成绩预告发表规则&am…

【PGMPY】 1. DAG基础结构

pgmpy 贝叶斯网络的纯python实现&#xff0c; 用途&#xff1a; 结构学习、 参数估计、 近似&#xff08;基于采样&#xff09; 精确推理 因果推理 安装 pip install pgmpyconda install -c ankurankan pgmpyconda install -c ankurankan pgmpy文档 https://pgmpy.org/index…

C++多线程编程(包含c++20内容)

C多线程编程(包含c20内容) 文章目录 C多线程编程(包含c20内容)线程通过函数指针创建线程通过函数对象创建线程通过lambda创建线程通过成员函数创建线程线程本地存储取消线程自动join线程从线程获得结果 原子操作库原子操作原子智能指针原子引用使用原子类型等待原子变量 互斥互…

Java【Spring】项目创建、存储和获取 Bean 的基本方式

文章目录 前言一、创建 Spring 项目1, 创建 Maven 项目2, 添加 Spring 依赖3, 创建启动类 二、存储 Bean 的基本方式1, 创建 Bean2, 存储 Bean 三、获取 Bean 的基本方式1, 获取上下文对象2, 获取 Bean3, 使用 Bean 总结 前言 各位读者好, 我是小陈, 这是我的个人主页, 希望我的…

tinkerCAD案例:18. Glow Circuit Assembly 发光电路组件

tinkerCAD案例&#xff1a;18. Glow Circuit Assembly 发光电路组件 In this tutorial, we’ll walk through how to use the Glow circuit assembly. 在本教程中&#xff0c;我们将演练如何使用 Glow 电路程序集。 Using the Glow circuit assembly, you’ll build a pengui…

使用Jetpack Glance创建Android Widget

使用Jetpack Glance创建Android Widget Jetpack Glance发布&#xff0c;让我们使用Google提供的Jetpack Glance创建一个联系人列表小部件。 https://developer.android.com/jetpack/compose/glance 什么是Glance&#xff1f; Jetpack Glance是一个使用Kotlin API创建小型、轻…

APP流量变现逻辑,流量变现收益如何最大化?

开发者首先通过买量的方式将用户引入APP中&#xff1b;用户在使用APP过程中的行为&#xff0c;诸如与游戏关卡的互动等&#xff0c;则不断地产生流量&#xff1b;最后&#xff0c;流量将成为“商品”&#xff0c;被运输至Facebook Audience Network或Google AdMob等平台进行售卖…

cmd命令查看电脑端口

操作是window系统的 其他系统原理类似 但也是有细微差别的 操作 netstat -na //135只是一个举例 netstat -ano | findstr 135//查看被占用端口对应的PID asklist | findstr 135//查看是哪个进程或者程序占用了端口指令 在Windows上&#xff1a; 使用以下命令查找占用指定…

Docker创建tomcat容器实例后无法访问(HTTP状态 404 - 未找到)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

python基础1——环境安装

文章目录 一、Windows安装二、Linux安装三、pycharm安装3.1 软件安装3.2 个性化设置3.3 基本使用3.3.1 定义变量3.3.2 查看数据类型3.3.3 运算符3.3.4 操作符3.3.5 转义符 一、Windows安装 1、下载软件安装包&#xff0c;官网 2、开始安装。 2.查看是否安装成功。 3.安装…

【从零开始学习JAVA | 第三十三篇】File类

目录 前言&#xff1a; File类&#xff1a; 构造方法&#xff1a; 常见成员方法&#xff1a; 总结&#xff1a; 前言&#xff1a; 本文我们将为大家介绍JAVA中一个比较使用的类&#xff1a;File类&#xff0c;他为我们提供了存储数据的功能&#xff0c;使得程序的数据不至…

Python(一)

要做到坚韧不拔&#xff0c;最要紧的是坚持到底。——陀思妥耶夫斯基 2023 6 14~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --根据你自己的操作系统下载对应的。 -- pyhton 文档 --交互方式 使用的工具 --如何启动工具 -- 交互式方式一般在数据分析中…

认识什么是架构

目录 ​编辑 一、架构是什么 1.1 系统与子系统 1.1.1 系统 1.1.1.1 关联 1.1.1.2 规则 1.1.1.3 能力 1.1.2 子系统 1.2 模块与组件 1.2.1 模块 1.2.2 组件 1.3 框架与架构 1.3.1 框架 1.3.2 架构 1.3.2.1 架构定义 1.3.2.2 架构组成 1.3.2.2.1 要素 1.3.2.2.2 结构 1.3.2…

YAML+PyYAML笔记 7 | PyYAML源码之yaml.compose_all(),yaml.load(),yaml.load_all()

7 | PyYAML源码之yaml.compose_all&#xff0c;yaml.load,yaml.load_all 1 yaml.compose_all()2 yaml.load()3 yaml.load_all() 1 yaml.compose_all() 源码&#xff1a; 作用&#xff1a;分析流中的所有YAML文档&#xff0c;并产生相应的表示树。解析&#xff1a; # -*- codi…

IDEA配置远程docker解释器及无编码提示/关联不到python依赖问题

文章目录 1. 修改docker默认配置以支持远程连接2. 配置docker远程解释器3 .IDE配置project SDK4. 本地代码与Linux目录映射5.运行配置 1. 修改docker默认配置以支持远程连接 vim /lib/systemd/system/docker.service,修改docker启动参数 #ExecStart/usr/bin/dockerd -H fd://…

后台管理系统中重定向业务功能的实现

实现 退出登录组件中获取到当前路由路径并且传递query参数给登录页面 // 组件内 const logout()>{// 获取当前路由路径const redirectroute.fullPathuseStore.logout(redirect) }// pinia/store仓库 async logout(redirect:string){... // 清空仓库业务router.push({name:…