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.sizevar 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 + currentSizeif (waveHeightPercent > 1.0f) {val diff = waveHeightPercent - 1.0fwaveHeightPercent = 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.Blackalpha = 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音频应用。

Jetpack Compose教程-水位控制小部件相关推荐

  1. html使用element ui_Kendo UI for jQuery使用教程:自定义小部件(二)

    Kendo UI目前最新提供KendoUI for jQuery.KendoUI for Angular.KendoUI Support for React和KendoUI Support for V ...

  2. 基于 Thingsboard 平台自定义 RPC 控制类小部件示例

    基于 Thingsboard 平台自定义 RPC 控制类小部件示例 1. 小部件介绍 2. 创建小部件 3. 部件编辑器 3.1 简介 3.2 资源 / HTML / CSS 3.3 JavaScri ...

  3. Jetpack Compose入门详解(实时更新)

    Jetpack Compose入门详解 前排提醒 前言(Compose是什么) 1.实战准备 一.优势与缺点 二.前四课 三.标准布局组件 1.Column 2.Row 3.Box 四.xml和com ...

  4. 怎么将小部件图标添加回Windows11任务栏

    怎么将小部件图标添加回Windows11任务栏?Windows11是Windows操作系统的最新版本,附带了一项名为Widgets的功能.小部件功能并不是一个新功能.相反,它是Windows10中可用 ...

  5. 淘宝小部件:全新的开放卡片技术!

    简介: 淘宝的开放技术目前主要有两种形态,第一种是小程序,第二种是今天的主角小部件.它是基于小程序技术体系,面向标准化.轻量化.高性能的开放卡片场景.本文我们将通过技术设计策略.核心技术设施.业务场景 ...

  6. 淘宝小部件:全新的开放卡片技术

    私域,即品牌自运营的空间,可以帮助品牌持续运营自己的消费者. 淘宝也在快速调整私域的布局:淘宝也有非常多的私域产品,譬如店铺.客服.消息等.在这些场景中,品牌商家需要利用创意.内容和服务留住消费者群体 ...

  7. 【JavaScript UI库和框架】上海道宁与Webix为您提供用于跨平台Web应用程序开发的JS框架及UI小部件

    Webix是Javascript库 一种软件产品 用于加速Web开发的 JavaScript UI库和框架 Webix用于跨平台Web应用程序开发的JS框架,为您提供102个UI小部件和功能丰富的CS ...

  8. 使用Compose实现基于MVI架构、retrofit2、支持 glance 小部件的TODO应用

    前言 现在声明式 UI 已逐渐成为主流,在客户端上,已有成熟的 Flutter 和 SwiftUi ,而原生安卓上的声明式 UI 却在去年年底才姗姗来迟. 虽然 compose 姗姗来迟,但是关于它的 ...

  9. Android桌面小部件AppWidget:音乐播放器桌面控制部件Widget(3)

     Android桌面小部件AppWidget:音乐播放器桌面控制部件Widget(3) Android桌面小部件AppWidget比较常用的场景就是音乐播放器,音乐播放器虽然通常在后台播放,但需要 ...

最新文章

  1. 恒丰银行深夜紧急澄清!“我行与建设银行的报道严重不实”!恒丰银行称目前自身经营稳定有序
  2. 24.C#LINQ TO XML(十二章12.3)
  3. Utility Manager 的一些百度不了的操作
  4. AS 自定义 Gradle plugin 插件 案例 MD
  5. hadoop rpc客户端初始化和调用过程详解
  6. 美股周一暴跌触发熔断:苹果、特斯拉股价大跌
  7. 计算机音频和视频知识点,计算机基础的知识点.docx
  8. 一个流氓的SQL设计,备份(一个字段存多个数据)
  9. springboot问题记录
  10. 安装mysql输入_安装mysql
  11. FAL风控策略分析师怎么样?
  12. 443端口与80端口
  13. 定时器 + websocket
  14. 前端基础—— 选择器 、css
  15. spring boot电商系统前端界面设计与浏览器兼容性研究 毕业设计-附源码231058
  16. Vue 判断页面内图片是否渲染完成
  17. 有一个List集合,里面存储了5个Student对象。Student有姓名、年龄和成绩属性,按照多个属性进行排序(一行代码解决list对象集合排序问题)。
  18. 中兴美国事件回顾:崛起的骄傲与威胁
  19. 鸿蒙和宙斯谁厉害,对抗达克赛德第一次入侵的旧神联盟去哪了
  20. 正面管教之PHP_“PHP父母帮助父母解决问题”活动

热门文章

  1. 哈夫曼树的构造及应用
  2. 朝花夕拾----新组件的学习和使用
  3. 基于地理因式分解法的POI推荐排序算法(Ranking based Geographical Factorization Method,Rank-GeoFM)
  4. FreeRTOS信号量 基于STM32
  5. 安徽三连学院计算机考试,安徽三联学院2017年3月计算机等级考试报名时间
  6. shell编程-大杂烩
  7. 在私有云建立企业自己的行业模板中心
  8. 【stm32】ST-LINK的SWD接口
  9. 硬件创业的精益时代:从原型到生产都是泪
  10. CMake常用命令总结