zoukankan      html  css  js  c++  java
  • 高效动画实现原理-Jetpack Compose 初探索

    一、简介

    Jetpack Compose是Google推出的用于构建原生界面的新Android 工具包,它可简化并加快 Android上的界面开发。Jetpack Compose是一个声明式的UI框架,随着该框架的推出,标志着Android 开始全面拥抱声明式UI开发。Jetpack Compose存在很多优点:代码更加简洁直观、应用开发效率显著提升、Kotlin API功能直观、预览工具强大等。

    二、开发环境

    为了获得更好的开发体验,笔者这里使用的是Android Studio Canary版本,这样可以无需配置一些设置和依赖。(下载地址

    打开工程,新建Empty Compose activity 模版,需要注意的是根目录下的build.gradle,相关的依赖com.android.tools.build和org.jetbrains.kotlin版本需要对应,否则可能出现出错的情形,这里使用的是:

    dependencies {
    	classpath "com.android.tools.build:gradle:7.0.0-alpha15"
    	classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30"
    }
    
    

    这样就完成了项目的新建。

    三、Jetpack Compose动画

    Jetpack Compose提供了一些功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。下文将会对Jetpack Compose Animations的常用方法进行介绍。

    3.1 状态驱动动画:State

    Jetpack Compose动画是通过对状态的监听,即监听状态值的变化,使UI能实现自动更新。可组合函数可以使用 remember或者 mutableStateOf监听状态值的变化。如果状态值是不变的,remember函数会在每次重新组合中保持该值;如果状态是可变的,它会在值发生变化的时候触发重组,mutableStateOf将得到一个MutableState对象,它是一个可观察类型。

    这种重组是创建状态驱动动画的关键。利用重组,它们会在可组合组件的状态发生任何变化时被触发。Compose动画是由State驱动的,动画相关的API也较容易上手,能比较容易创造出漂亮的声明式动画。

    3.2 可见性动画: AnimatedVisibility

    首先看下函数定义:

    @ExperimentalAnimationApi
    @Composable
    fun AnimatedVisibility(
        visible: Boolean,
        modifier: Modifier = Modifier,
        enter: EnterTransition = fadeIn() + expandIn(),
        exit: ExitTransition = shrinkOut() + fadeOut(),
        initiallyVisible: Boolean = visible,
        content: @Composable () -> Unit
    ) {
        AnimatedVisibilityImpl(visible, modifier, enter, exit, initiallyVisible, content)
    }
    
    

    可以看出默认的动画是淡入放大、淡出收缩,实际中通过传入不同函数实现各种动效。

    随着可见值的变化,AnimatedVisibility可为其内容的出现和消失设置动画。如下代码,可以通过点击Button,控制图片的出现和消失。

    @Composable
    fun AinmationDemo() {
    
        //AnimatedVisibility 可见动画
        var visible by remember { mutableStateOf(true) }
    
        Column(
            Modifier
                .fillMaxWidth()
                .fillMaxHeight(),
            Arrangement.Top,
            Alignment.CenterHorizontally
        ) {
            Button(
                onClick = { visible = !visible }
            ) {
                Text(text = if (visible) "Hide" else "Show")
            }
    
            Spacer(Modifier.height(16.dp))
    
            AnimatedVisibility(
                visible = visible,
                enter = slideInVertically() + fadeIn(),
                exit = slideOutVertically() + fadeOut()
            ) {
                Image(
                    painter = painterResource(id = R.drawable.pikaqiu),
                    contentDescription = null,
                    Modifier.fillMaxSize()
                )
            }
        }
    }
    
    

    通过监听visible的变化,可实现图片的可见性动画,效果如小图所示;

    3.3 布局大小动画:AnimateContentSize

    先看下函数的定义:

    fun Modifier.animateContentSize(
        animationSpec: FiniteAnimationSpec<IntSize> = spring(),
        finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null
    )
    
    

    可以为布局大小动画设置动画速度和监听值。

    由函数的定义可以看出这个函数本质上就Modefier的一个扩展函数。可以通过变量size监听状态变化实现布局大小的动画效果,代码如下:

    //放大缩小动画 animateContentSize
        var size by remember { mutableStateOf(Size(300F, 300F)) }
    
        Column(
            Modifier
                .fillMaxWidth()
                .fillMaxHeight(),
            Arrangement.Top,
            Alignment.CenterHorizontally
        ) {
            Spacer(Modifier.height(16.dp))
    
            Button(
                onClick = {
                    size = if (size.height == 300F) {
                        Size(500F, 500F)
                    } else {
                        Size(300F, 300F)
                    }
                }
            ) {
                Text(if (size.height == 300F) "Shrink" else "Expand")
            }
            Spacer(Modifier.height(16.dp))
    
            Box(
                Modifier
                    .animateContentSize()
            ) {
                Image(
                    painter = painterResource(id = R.drawable.pikaqiu),
                    contentDescription = null,
                    Modifier
                        .animateContentSize()
                        .size(size = size.height.dp)
                )
            }
    } //放大缩小动画 animateContentSize    var size by remember { mutableStateOf(Size(300F, 300F)) }​    Column(        Modifier            .fillMaxWidth()            .fillMaxHeight(),        Arrangement.Top,        Alignment.CenterHorizontally    ) {        Spacer(Modifier.height(16.dp))​        Button(            onClick = {                size = if (size.height == 300F) {                    Size(500F, 500F)                } else {                    Size(300F, 300F)                }            }        ) {            Text(if (size.height == 300F) "Shrink" else "Expand")        }        Spacer(Modifier.height(16.dp))​        Box(            Modifier                .animateContentSize()        ) {            Image(                painter = painterResource(id = R.drawable.pikaqiu),                contentDescription = null,                Modifier                    .animateContentSize()                    .size(size = size.height.dp)            )        }}
    
    

    通过Button的点击,监听size值的变化,利用animateContentSize()实现动画效果,具体动效如下图所示:

    3.4布局切换动画: Crossfade

    Crossfade可以通过监听状态值的变化,使用淡入淡出的动画在两个布局之间添加动画效果,函数自身就是一个Composable,代码如下:

    //Crossfade 淡入淡出动画
        var fadeStatus by remember { mutableStateOf(true) }
    
        Column(
            Modifier
                .fillMaxWidth()
                .fillMaxHeight(),
            Arrangement.Top,
            Alignment.CenterHorizontally
        ) {
            Button(
                onClick = { fadeStatus = !fadeStatus }
            ) {
                Text(text = if (fadeStatus) "Fade In" else "Fade Out")
            }
    
            Spacer(Modifier.height(16.dp))
    
            Crossfade(targetState = fadeStatus, animationSpec = tween(3000)) { screen ->
                when (screen) {
                    true -> Image(
                        painter = painterResource(id = R.drawable.pikaqiu),
                        contentDescription = null,
                        Modifier
                            .animateContentSize()
                            .size(300.dp)
                    )
                    false -> Image(
                        painter = painterResource(id = R.drawable.pikaqiu2),
                        contentDescription = null,
                        Modifier
                            .animateContentSize()
                            .size(300.dp)
                    )
                }
            }
    
        }
    
    

    同样通过监听fadeStatus的值,实现布局切换的动画,具体的动效如图所示:

    3.5单个值动画:animate*AsState

    为单个值添加动画效果。只需提供结束值(或目标值),该 API 就会从当前值开始向指定值播放动画。

    Jetpack Compose 提供了很多内置函数,可以为不同类型的数据制作动画,例如:animateColorAsState、animateDpAsState、animateOffsetAsState等,这里将介绍下animateFooAsState的使用,代码如下:

    //animate*AsState 单个值添加动画
        var transparent by remember { mutableStateOf(true) }
        val alpha: Float by animateFloatAsState(if (transparent) 1f else 0.5f)
    
        Column(
            Modifier
                .fillMaxWidth()
                .fillMaxHeight(),
            Arrangement.Top,
            Alignment.CenterHorizontally
        ) {
            Button(
                onClick = { transparent = !transparent }
            ) {
                Text(if (transparent) "Light" else "Dark")
            }
    
            Spacer(Modifier.height(16.dp))
    
            Box {
    
                Image(
                    painter = painterResource(id = R.drawable.pikaqiu),
                    contentDescription = null,
                    Modifier
                        .animateContentSize()
                        .graphicsLayer(alpha = alpha)
                        .size(300.dp)
                )
            }
    }
    
    
    

    动画效果如下图所示:

    3.6 组合动画:updateTransition

    Transition 可同时追踪一个或多个动画,并在多个状态之间同步这些动画。具体的代码如下:

    var imagePosition by remember { mutableStateOf(ImagePosition.TopLeft) }
    
        Column(
            Modifier
                .fillMaxWidth()
                .fillMaxHeight(),
            Arrangement.Top,
            Alignment.CenterHorizontally
        ) {
            Spacer(Modifier.height(16.dp))
    
            val transition = updateTransition(targetState = imagePosition, label = "")
            val boxOffset by transition.animateOffset(label = "") { position ->
                when (position) {
                    ImagePosition.TopLeft -> Offset(-60F, 0F)
                    ImagePosition.BottomRight -> Offset(60F, 120F)
                    ImagePosition.TopRight -> Offset(60F, 0F)
                    ImagePosition.BottomLeft -> Offset(-60F, 120F)
                }
            }
            Button(onClick = {
                imagePosition = ChangePosition(imagePosition)
            }) {
                Text("Change position")
            }
            Box {
    
                Image(
                    painter = painterResource(id = R.drawable.pikaqiu),
                    contentDescription = null,
                    Modifier
                        .offset(boxOffset.x.dp, boxOffset.y.dp)
                        .animateContentSize()
                        .size(300.dp)
                )
            }
    }
    
    

    其中,ImagePosition、ChangePosition分别为定义的枚举类、自定义函数。

    enum class ImagePosition {
        TopRight,
        TopLeft,
        BottomRight,
        BottomLeft
    }
    
    fun ChangePosition(position: ImagePosition) =
        when (position) {
            ImagePosition.TopLeft -> ImagePosition.BottomRight
            ImagePosition.BottomRight -> ImagePosition.TopRight
            ImagePosition.TopRight -> ImagePosition.BottomLeft
            ImagePosition.BottomLeft -> ImagePosition.TopLeft
        }
    
    

    动画的如下图所示:

    四、结语

    Jetpack Compose 已将动画简化到只需在我们的可组合函数中创建声明性代码的程度,只需编写希望 UI 动画的方式,其余部分由 Compose 管理。最后,这也是是 Jetpack Compose 的主要目标:创建一个声明式 UI 工具包来加速应用程序开发并提高代码可读性和逻辑性。

    Jetpack Compose提供的声明式UI工具包,能做到使用更少的代码实现更多的功能,且代码的可读性和逻辑性也大大提高了。

    作者:vivo互联网游戏客户端团队-Ke Jie

    分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。
  • 相关阅读:
    通信错误:(-1)[描述:无法解析路由器DDNS地址,请检查DDNS状态.] 解析办法
    小数量宽带用户的福音,Panabit 云计费easyradius 接口隆重发布,PA宽带计费系统
    送给那些经常问我如何设置360测速结果为电信的朋友,360测速模块原理简单分析
    Universal-Image-Loader(UIL)使用方法&流程图&源码分析 ----- 未完
    Android开发框架
    Android线程池的使用(未完)
    Android LruCache究竟是什么
    Java finally语句到底是在return之前还是之后执行?
    Android自定义图片加载框架
    Android 自定义View实现单击和双击事件
  • 原文地址:https://www.cnblogs.com/vivotech/p/15423785.html
Copyright © 2011-2022 走看看