Jetpack Compose教程-水位控制小部件

news2024/11/15 11:44:17

Jetpack Compose教程-水位控制小部件


Apple的应用程序和小部件一直是设计的典范,也给我们的"复制系列:活动应用"和"卡片应用"提供了灵感。当他们发布了新款苹果手表Ultra时,它里面深度测量小部件的设计引起了我们的兴趣,我们觉得在安卓手机上也能模仿一下!就像我们通常在安卓复制的挑战中一样,我们使用了Jetpack Compose框架。

本文将向您介绍我们是如何实现以下内容的:创建波浪效果、使水围绕文本流动,并混合颜色。无论您是初学者还是已经熟悉Jetpack Compose的人,这些内容都会对您有所帮助。

Water Level

首先,我们来考虑一个最简单的问题——如何计算和实现水位的动画效果。

enum class WaterLevelState {
    StartReady,
    Animating,
}

接下来,我们定义了动画的持续时间和初始状态。

val waveDuration by rememberSaveable { mutableStateOf(waveDurationInMills) }
var waterLevelState by remember { mutableStateOf(WaterLevelState.StartReady) }

之后,我们需要确定水的变化方式。这样可以在屏幕上以文本形式记录进展,并绘制水位。

val waveProgress by waveProgressAsState(
    timerState = waterLevelState,
    timerDurationInMillis = waveDuration
)

现在,我们来仔细看一下waveProgressAsState。我们使用animatable是因为它能提供更多的控制和自定义选项。例如,我们可以为不同的状态指定不同的动画参数。

接下来,我们要计算需要在屏幕上绘制水边缘的坐标:

val waterLevel by remember(waveProgress, containerSize.height) {
    derivedStateOf {
        (waveProgress * containerSize.height).toInt()
    }
}

所有这些初步工作完成后,我们可以开始创建真实的波浪。

Waves

模拟波浪最常见的方法是使用以一定速度水平移动的正弦图。


我们希望它看起来更加逼真,而且它必须流动到屏幕上的元素上,因此我们需要一种更复杂的方法。实现的主要思想是定义一组表示波浪高度的点。这些值被动画化以创建波浪效果。

首先,我们创建一个包含点以存储值的列表。

val points = remember(spacing, containerSize) {
    derivedStateOf {
        (-spacing..containerSize.width + spacing step spacing).map { x ->
            PointF(x.toFloat(), waterLevel)
        }
    }
}

接下来,让我们来讨论正常情况下水流顺畅、没有阻碍物的情况。这时,我们只需将水位数值填入其中。其他情况我们稍后再讨论。

LevelState.PlainMoving -> {
    points.value.map {
        it.y = waterLevel
    }
}

考虑一个动画,它将改变每个点的高度。如果对所有的点都进行动画处理,会严重耗费性能和电池。为了节省资源,我们只用了一小部分浮点数动画数值。

@Composable
fun createAnimationsAsState1(
    pointsQuantity: Int,
): MutableList<State<Float>> {
    val animations = remember { mutableListOf<State<Float>>() }
    val random = remember { Random(System.currentTimeMillis()) }
    val infiniteAnimation = rememberInfiniteTransition()

    repeat(pointsQuantity / 2) {
        val durationMillis = random.nextInt(2000, 6000)
        animations += infiniteAnimation.animateFloat(
            initialValue = 0f,
            targetValue = 1f,
            animationSpec = infiniteRepeatable(
                animation = tween(durationMillis),
                repeatMode = RepeatMode.Reverse,
            )
        )
    }
    return animations
}

为了防止动画每15个点重复一次,并使波浪不相同,我们可以设置initialMultipliers

@Composable
fun createInitialMultipliersAsState(pointsQuantity: Int): MutableList<Float> {
    val random = remember { Random(System.currentTimeMillis()) }
    return remember {
        mutableListOf<Float>().apply {
            repeat(pointsQuantity) { this += random.nextFloat() }
        }
    }
}

现在要添加波浪 - 遍历所有点并计算新的高度。

points.forEachIndexed { index, pointF ->
    val newIndex = index % animations.size
    
    var waveHeight = calculateWaveHeight(
        animations[newIndex].value,
        initialMultipliers[index],
        maxHeight
    )
    pointF.y = pointF.y - waveHeight
}
return points

initialMultipliers添加到currentSize将减少重复数值的可能性。同时,使用线性插值将有助于平滑地改变高度。

private fun calculateWaveHeight(
    currentSize: Float,
    initialMultipliers: Float,
    maxHeight: Float
): Float {
    var waveHeightPercent = initialMultipliers + currentSize
    if (waveHeightPercent > 1.0f) {
        val diff = waveHeightPercent - 1.0f
        waveHeightPercent = 1.0f - diff
    }

    return lerpF(maxHeight, 0f, waveHeightPercent)
}

现在让我们来看最有趣的部分——如何让水围绕着用户界面元素流动。

交互式水流动

首先,我们定义了水在下降过程中的三种状态。PlainMoving表示水平常流的状态,WaveIsComing表示水逐渐抬升到需要展示流动效果的用户界面元素的时刻,而FlowsAround则表示实际上水已经开始围绕着UI元素流动了。

sealed class LevelState {
    object PlainMoving : LevelState()
    object FlowsAround : LevelState()
    object WaveIsComing: LevelState()
}

我们了解到,如果水位低于物品位置减去缓冲区,那么水位就比物品高。下图中以红色标示了该区域。

fun isAboveElement(waterLevel: Int, bufferY: Float, position: Offset) = waterLevel < position.y - bufferY


当水位与元素水平相同时,开始流动还为时过早。下图中的区域以灰色显示。

fun atElementLevel(
    waterLevel: Int,
    buffer: Float,
    elementParams: ElementParams,
) = (waterLevel >= (elementParams.position.y - buffer)) &&
        (waterLevel < (elementParams.position.y + elementParams.size.height * 0.33))

fun isWaterFalls(
    waterLevel: Int,
    elementParams: ElementParams,
) = waterLevel >= (elementParams.position.y + elementParams.size.height * 0.33) &&
        waterLevel <= (elementParams.position.y + elementParams.size.height)

还有一个问题需要考虑 —— 如何计算水流的时间?当水位在蓝色区域时,瀑布和波浪的动画会增加。因此,我们需要确定水位达到元素高度的2/3时的时间。

@Composable
fun rememberDropWaterDuration(
    elementSize: IntSize,
    containerSize: IntSize,
    duration: Long,
): Int {
    return remember(
        elementSize,
        containerSize
    ) { (((duration * elementSize.height * 0.66) / (containerSize.height))).toInt() }
}

我们来仔细看一下元素周围的水流情况。水的流动形状是基于一个抛物线的,为了教程的简单性我们选择了一个简单的形状。我们用图片中的那些点来描述抛物线的轨迹。我们并没有把抛物线延伸到当前的水位以下(即水平的红线处)。

is LevelState.FlowsAround -> {
    val point1 = PointF(
        position.x,
        position.y - buffer / 5
    )
    val point2 = point1.copy(x = position.x + elementSize.width)
    val point3 = PointF(
        position.x + elementSize.width / 2,
        position.y - buffer
    )
    val p = Parabola(point1, point2, point3)
    points.value.forEach {
        val pr = p.calculate(it.x)
        if (pr > waterLevel) {
            it.y = waterLevel
        } else {
            it.y = pr
        }
    }

让我们来看看瀑布动画:我们将使用相同的抛物线,改变它的高度从初始位置开始,并使用OvershootInterpolator实现更柔和的下落效果。

val parabolaHeightMultiplier = animateFloatAsState(
    targetValue = if (levelState == LevelState.WaveIsComing) 0f else -1f,
    animationSpec = tween(
        durationMillis = dropWaterDuration,
        easing = { OvershootInterpolator(6f).getInterpolation(it) }
    )
)

在这种情况下,我们使用高度倍增动画,以便最终抛物线的高度变为0。

val point1 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) {
    mutableStateOf(
        PointF(
            position.x,
            waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value
        )
    )
}
val point2 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) {
    mutableStateOf(
        PointF(
            position.x + elementSize.width,
            waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value
        )
    )
}
val point3 by remember(position, elementSize, parabolaHeightMultiplier, waterLevel) {
    mutableStateOf(
        PointF(
            position.x + elementSize.width / 2,
            waterLevel + (elementSize.height / 3f + buffer) * parabolaHeightMultiplier.value
        )
    )
}
return produceState(
    initialValue = Parabola(point1, point2, point3),
    key1 = point1,
    key2 = point2,
    key3 = point3
) {
    this.value = Parabola(point1, point2, point3)
}

此外,我们需要改变与界面元素重叠的位置的波浪大小,因为在水落下的瞬间它们会增大,然后缩小至正常尺寸。

val point1 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) {
    mutableStateOf(
        PointF(
            position.x,
            waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value
        )
    )
}
val point2 by remember(position, elementSize, waterLevel, parabolaHeightMultiplier) {
    mutableStateOf(
        PointF(
            position.x + elementSize.width,
            waterLevel + (elementSize.height / 3f + buffer / 5) * parabolaHeightMultiplier.value
        )
    )
}
val point3 by remember(position, elementSize, parabolaHeightMultiplier, waterLevel) {
    mutableStateOf(
        PointF(
            position.x + elementSize.width / 2,
            waterLevel + (elementSize.height / 3f + buffer) * parabolaHeightMultiplier.value
        )
    )
}
return produceState(
    initialValue = Parabola(point1, point2, point3),
    key1 = point1,
    key2 = point2,
    key3 = point3
) {
    this.value = Parabola(point1, point2, point3)
}

波浪的高度在UI元素周围的半径内增加,增加真实感。

val elementRangeX = (position.x - bufferX)..(position.x + elementSize.width + bufferX)
points.forEach { index, pointF ->
    if (levelState.value is LevelState.WaveIsComing && pointF.x in elementRangeX) {
        waveHeight *= waveMultiplier
    }
}

现在是将我们所学的所有内容结合起来,并添加颜色混合。

将所有元素组合在一起

在画布上进行绘画时,有几种方式可以使用混合模式。

首先,我想到的方法是使用位图来绘制路径,并在位图画布上使用混合模式来绘制文本。这种方法使用了 Android 视图中旧的画布实现,所以我们决定采用更直接的方式——使用混合模式来进行颜色混合。首先,我们在画布上绘制了波浪。

Canvas(
    modifier = Modifier
        .background(Water)
        .fillMaxSize()
) {
    drawWaves(paths)
}

在实施过程中,我们使用drawIntoCanvas,以便我们可以使用paint.pathEffectCornerPathEffect来平滑波浪。

fun DrawScope.drawWaves(
    paths: Paths,
) {
    drawIntoCanvas {
        it.drawPath(paths.pathList[1], paint.apply {
            color = Blue
        })
        it.drawPath(paths.pathList[0], paint.apply {
            color = Color.Black
            alpha = 0.9f
        })
    }
}

为了了解文本占据的空间大小,我们在一个盒子里放置了文本元素。由于布局中的Text元素不支持混合模式,所以我们需要利用混合模式在画布上绘制文本。为此,我们使用drawWithContent修饰器,只在画布上绘制文本,而不会绘制文本元素本身。

为了使混合模式生效,需要创建一个新的图层。我们可以使用.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)*来实现这一点。无论在图形图层上配置了哪些参数,内容的渲染都会首先渲染到一个离屏缓冲区,然后再绘制到目标位置上。

*(这是对我们之前实现的更新,之前我们使用了.graphicsLayer(alpha = 0.99f)的技巧,但在评论中,@romainguy 帮助我们找到了更清晰的解决方案)。

Box(
    modifier = modifier
        .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
        .drawWithContent {
            drawTextWithBlendMode(
                mask = paths.pathList[0],
                textStyle = textStyle,
                unitTextStyle = unitTextStyle,
                textOffset = textOffset,
                text = text,
                unitTextOffset = unitTextProgress,
                textMeasurer = textMeasurer,
            )
        }
) {
    Text(
        modifier = content().modifier
            .align(content().align)
            .onGloballyPositioned {
                elementParams.position = it.positionInParent()
                elementParams.size = it.size
            },
        text = "46FT",
        style = content().textStyle
    )
}

首先我们绘制文本,然后绘制一条波浪,用作蒙版。这是有关可供开发人员使用的不同混合模式的官方文档。

https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/BlendMode#SrcIn()

fun DrawScope.drawTextWithBlendMode(
    mask: Path,
    textMeasurer: TextMeasurer,
    textStyle: TextStyle,
    text: String,
    textOffset: Offset,
    unitTextOffset: Offset,
    unitTextStyle: TextStyle,
) {
    drawText(
        textMeasurer = textMeasurer,
        topLeft = textOffset,
        text = text,
        style = textStyle,
    )
    drawText(
        textMeasurer = textMeasurer,
        topLeft = unitTextOffset,
        text = "FT",
        style = unitTextStyle,
    )

    drawPath(
        path = mask,
        color = Water,
        blendMode = BlendMode.SrcIn
    )
}

完整的水位效果如下:

结论

这个实现事实上相当复杂,但这是可以预料的,因为原始素材本身就很复杂。幸运的是,我们能够充分利用原生的Compose工具进行大量操作。你还可以根据需要调整参数,以获得更吸引人的水效果,但我们决定停留在这个概念验证阶段。像往常一样,完整的实现可以在存储库中找到。如果你喜欢这个教程,你还可以在我们的个人资料中找到更多有趣的内容,或者查看如何在Jetpack Compose中复制一个酷炫的dribbble音频应用。

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

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

相关文章

加快奔向“国际数字之都” CDEC2023中国数字智能生态大会走进上海

数智闪耀长三角&#xff0c;风云际会上海滩。 6月14日上午&#xff0c;以汇聚数字产业动能、打造区域合作为主旨的 CDEC2023中国数字智能生态大会上海站活动在浦东软件园创新体验中心举行。 大会以“共建AI智能生态”为主题&#xff0c;吸引致远互联、SAP、浪潮等龙头企业&…

2022年山东省职业院校技能大赛网络搭建与应用赛项网络搭建与安全部署服务器配置及应用

2022年山东省职业院校技能大赛 网络搭建与应用赛项 第二部分 网络搭建与安全部署&服务器配置及应用 竞赛说明&#xff1a; 一、竞赛内容分布 竞赛共分二个模块&#xff0c;其中&#xff1a; 第一模块&#xff1a;网络搭建及安全部署项目 第二模块&#xff1a;服务器…

C#里的var和dynamic区别到底是什么,你真的搞懂了嘛

前言 这个var和dynamic都是不确定的初始化类型&#xff0c;但是这两个本质上的不同。不同在哪儿呢?var编译阶段确定类型&#xff0c;dynamic运行时阶段确定类型。这种说法对不对呢&#xff1f;本篇看下,文章原文地址&#xff1a;在这里 概括 以下详细叙述下这两个(var,dynamic…

CVE-2023-33246命令执行复现分析

简介 RocketMQ是一款低延迟、高并发、高可用、高可靠的分布式消息中间件。既可为分布式应用系统提供异步解耦和削峰填谷的能力&#xff0c;同时也具备互联网应用所需的海量消息堆积、高吞吐、可靠重试等特性。 影响版本 <RocketMQ 5.1.0 <RocketMQ 4.9.5 环境搭建 docker…

Leetcode 剑指 Offer II 031. 最近最少使用缓存

题目难度: 中等 原题链接 今天继续更新 Leetcode 的剑指 Offer&#xff08;专项突击版&#xff09;系列, 大家在公众号 算法精选 里回复 剑指offer2 就能看到该系列当前连载的所有文章了, 记得关注哦~ 题目描述 运用所掌握的数据结构&#xff0c;设计和实现一个 LRU (Least Re…

Python 类型检测:isinstance() 与 type()

文章目录 参考描述面向对象编程概念类与实例继承super() 与代理对象方法的自动继承属性的继承 isinstance 与 type 内置函数isinstance()可迭代对象仅能为元组可能产生的 TypeError嵌套的元组 typeisinstance() 与 type() 的区别 参考 项目描述Python 官方文档https://docs.py…

【C语言初阶】分支语句If与switch的具体用法,有这篇博客就够了

君兮_的个人主页 勤时当勉励 岁月不待人 C/C 游戏开发 Hello,这里是君兮_,今天又来给大家更新0基础学习C语言中的文章啦&#xff01; 今天带来的是对分支语句的详解&#xff0c;初学者建议先看看总集哦, 这里是链接: 【C语言初阶】万字解析,带你0基础快速入门C语言(上) 【C语…

图片转excel表格算法之霍夫变换法原理浅析

大家伙都知道&#xff0c;图片转excel表格是金鸣识别中一项非常重要的功能&#xff0c;金鸣识别的OCR在识别图片中的表格时&#xff0c;会用到一种叫霍夫变换法的算法&#xff0c;那这个算法到底是怎么回事&#xff1f;它的原理又是什么呢&#xff1f; 一、霍夫变换法的概念 …

深入了解模板知识(c++)

前言 在c中模板是很重的&#xff0c;泛型编程就是模板最好的体现&#xff0c;模板的出现就是为了更好的复用代码&#xff0c;有了它&#xff0c;我们不必写各种逻辑相同只是逻辑中的数据的类型的不同的代码&#xff0c;使得我们编写代码变得更加高效&#xff0c;下面让我们一起…

若依权限系统分析(前后端分离版)

若依权限系统分析 一&#xff1a;故事背景二&#xff1a;具体权限控制2.1 页面权限控制2.2 页面元素权限控制 三&#xff1a;实现前端鉴权3.1 封装js与权限交互3.1.1 uni-app自带uni-request与权限交互 3.2 vux状态管理3.2.1 自定义状态3.2.2 在vuex的store配置内添加我们新增的…

rust切片

这里s的不可变引用借用给了wordIndex&#xff0c;而s.clear()又想用可变引用&#xff0c;所以报错。而第一个例子中返回的usize并没有返回不可变引用。

客户端负载均衡工具Ribbon

一 什么是Ribbon Ribbon介绍 目前主流的负载方案分为以下两种&#xff1a; 集中式负载均衡&#xff0c;在消费者和服务提供方中间使用独立的代理方式进行负载&#xff0c;有硬件的&#xff08;比如 F5&#xff09;&#xff0c;也有软件的&#xff08;比如 Nginx&#xff09;…

Ubuntu系统中分布式安装配置HBase-2.3.7

HBase是一个基于Hadoop的分布式列式数据库&#xff0c;可以存储海量的结构化和半结构化数据。本文介绍如何在三个Ubuntu系统上搭建一个HBase集群&#xff0c;并进行简单的数据操作。 在三个Ubuntu系统上分布式安装配置HBase-2.3.7&#xff0c;主要步骤包括&#xff1a; 准备工…

MySQL的执行原理

一、单表访问之索引合并 我们前边说过MySQL在一般情况下执行一个查询时最多只会用到单个二级索引&#xff0c;但存在有特殊情况&#xff0c;在这些特殊情况下也可能在一个查询中使用到多个二级索引&#xff0c;MySQL中这种使用到多个索引来完成一次查询的执行方法称之为&#…

Qgis加载在线XYZ瓦片影像服务的实践操作

目录 背景 一、XYZ瓦片相关知识 1、xyz瓦片金字塔 2、 瓦片编号 3、瓦片访问 二、在Qgis中加载在线地图 1、Qgis版本 2、瓦片加载 3、地图属性预览 总结 背景 在做电子地图应用的时候&#xff0c;很常见的会提到瓦片&#xff08;tile&#xff09;的概念&#xff0c;瓦片…

Java实训日志07

文章目录 八、项目开发实现步骤&#xff08;十&#xff09;创建应用程序类1、创建app子包2、创建Application类 &#xff08;十一&#xff09;创建窗口界面类1、创建主界面窗口&#xff08;1&#xff09;做一个空白的主界面窗口&#xff08;2&#xff09;退出时弹出消息框询问用…

【cutlass】cuTe layout操作

简介 cuTe提供了对Layout操作的算法&#xff0c;可以混合执行来构建更复杂的Layout操作&#xff0c;比如在其他layout之间切分和平铺layout 在host或者device上打印cuTe cuTe的打印函数可以在host和device端打印。cute::print 重载了几乎所有 CuTe 类型&#xff0c;包括指针…

MT8168/MTK8168核心板,4G安卓核心板

MT8168是一款集成度很高的高性能应用处理器&#xff0c;具有低功耗特性&#xff0c;并且提供卓越的多媒体体验&#xff0c;适用于平板电脑、智能手持终端以及智能家居和物联网应用等嵌入式设备。这款芯片采用了先进的12纳米工艺&#xff0c;将四核Arm-Cortex A53 MPCore TM CPU…

关于JAVA中 方法中无法改变String的分析

package com.atguigu.String01;public class String01 {public static void main(String[] args) {// 字符串不变性String str "hello";// 对象成员数组是finalchange(str);System.out.println("change后的str:"str);int[] a {1,3,5,7,9};int[] b {2,3,…

【V4L2】 v4l2框架分析之v4l2_fh

一、v4l2_fh简介 &#x1f53a;相关源码文件&#xff1a; /drivers/media/v4l2-fh.c /drivers/media/v4l2-fh.h 在V4L2中&#xff0c;struct v4l2_fh结构用于保存V4L2框架中使用的文件句柄&#xff08;File Handle&#xff09;的数据&#xff0c;即每个打开的视频设备都会对…