最近参加了compo挑战赛的终极挑战,使用compo完成了一个天气app。之前几轮挑战也都有参与,每次都学到不少新东西。如今迎来最终挑战,希望能将这段时间的积累活学活用,做出更加成熟的作品。
项目挑战
因为没有美工协助,所以我考虑通过代码实现app中的所有ui元素例如各种icon等,这样的ui在任何分辨率下都不会失真,跟重要的是可以灵活地实现各种动画效果。
为了降低实现成本,我将app中的ui元素定义成偏卡通的风格,可以更容易地通过代绘实现:
上面的动画没有使用gif、lottie或者其他静态资源,所有图形都是基于compo代码绘制的。
app界面比较简洁,采用单页面呈现(挑战赛要求),卡通风格的天气动画算是相对于同类app的特色:
项目地址:https://github.com/vitaviva/compo-weather
app纵向划分为几个功能区域,每个区域都涉及到一些不同的compo api的使用
涉及技术点较多,本文主要介绍如何使用compo绘制自定义图形、并基于这些图形实现动画,其他内容有机会再单独介绍。
像常规的android开发一样,除了提供各种默认的composable
控件以外,compo
也提供了canvas
用来绘制自定义ui。
其实canvas相关api在各个平台都大同小异,但在compo上的使用有以下特点:
用声明式的方式创建和使用canvas通过drawscope提供必要的state及各种apisapi更简单易用compo中,canvas
作为composable
,可以声明式地添加到其他composable中,并通过modifier
进行配置
canvas(modifier = modifier.fillmaxsize()){ // this: drawscope //内部进行自定义绘制}
传统方式需要获取canvas句柄命令式的进行绘制,而canvas{...}
通过状态驱动的方式在block内执行绘制逻辑、刷新ui。
canvas{...}
内部通过drawscope
提供必要的state用来获取当前绘制所需环境变量,例如我们最常用的size。drawscope
还提了各种常用的绘制api,例如drawline
等
canvas(modifier = modifier.fillmaxsize()){ //通过size获取当前canvas的width和height val canvaswidth = size.width val canvasheight = size.height //绘制直线 drawline( start = offt(x=canvaswidth, y = 0f), end = offt(x = 0f, y = canvasheight), color = color.blue, strokewidth = 5f //设置直线宽度 )}
上面代码绘制效果如下:
传统的canvas api需要进行paint等配置;drawscope提供的api更简单,使用更友好。
例如绘制一个圆,传统的api是这样:
public void drawcircle(float cx, float cy, float radius, @nonnull paint paint) { //... }
drawscope提供的api:
fun drawcircle( color: color, radius: float = size.mindimension / 2.0f, center: offt = this.center, alpha: float = 1.0f, style: drawstyle = fill, colorfilter: colorfilter? = null, blendmode: blendmode = defaultblendmode) {...}
看起来参数变多了,但是其实已经通过size等设置了合适的默认值,同时省去了对paint的创建和配置,使用起来更方便。
目前drawscope提供的api还不及原生canvas丰富(比如不支持drawtext等),当不满足使用需求时,也可以直接使用原生canvas对象进行绘制
drawintocanvas { canvas -> //nativecanvas是原生canvas对象,android平台即android.graphics.canvas val nativecanvas = canvas.nativecanvas }
上面介绍了compo canvas
的基本知识,下面结合app中的具体示例看一下实际使用效果
首先,看一下雨水的绘制过程。
雨天天气的关键是如何绘制不断下落的雨水
我们先绘制构成雨水的基本单元:雨滴
经拆解后,雨水效果可由三组雨滴构成,每一组雨滴分成上下两端,这样在运动时就可以形成接连不断的雨水效果。我们使用drawline绘制每一段黑线,设置适当的stokewidth
,并通过cap设置端点的圆形效果:
@composablefun raindrop() { canvas(modifier) { val x: float = size.width / 2 //x坐标:1/2的位置 drawline( color.black, offt(x, line1y1), //line1 的起点 offt(x, line1y2), //line1 的终点 strokewidth = width, //设置宽度 cap = strokecap.round//头部圆形 ) // line2同上 drawline( color.black, offt(x, line2y1), offt(x, line2y2), strokewidth = width, cap = strokecap.round ) }}
完成基本图形的绘制后,接下来为两线段实现循环往复的位移动画,形成雨水的流动效果。
以两线段中间空隙为动画的锚点,根据animationstate
设置其y轴位置,让其从绘制区域的顶端移动到低端(0 ~ size.hight)
,然后restart这个动画。
以锚点为基准绘制上下两线段,就可以行成接连不断的雨滴效果了
代码如下:
@composablefun raindrop() { //循环播放的动画 ( 0f ~ 1f) val animatetween by rememberinfinitetransition().animatefloat( initialvalue = 0f, targetvalue = 1f, 吸血鬼恒星 animationspec = infiniterepeatable( tween(durationmillis, easing = lineareasing), repeatmode.restart //start动画 ) ) canvas(modifier) { // scope : 绘制区域 val width = size.width val x: float = size.width / 2 // width/2是strokcap的宽度,scopeheight处预留strokcap宽度,让雨滴移出时保持正圆,提高视觉效果 val scopeheight = size.height - width / 2 // space : 两线段的间隙 val space = size.height / 2.2f + width / 2 //间隙size val spacepos = scopeheight * animatetween //锚点位置随animationstate变化 val sy1 = spacepos - space / 2 val sy2 = spacepos + space / 2 // line length val lineheight = scopeh中国历史年表eight - space // line1 val line1y1 = max(0f, sy1 - lineheight) val line1y2 = max(line1y1, sy1) // line2 val line2y1 = min(sy2, scopeheight) val line2y2 = min(line2y1 + lineheight, scopeheight) // draw drawline( color.black, offt(x, line1y1), offt(x, line1y2), strokewidth = width, colorfilter = colorfilter.tint( color.black ), cap = strokecap.round ) drawline( color.black, offt(x, line2y1), offt(x, line2y2), strokewidth = width, colorfilter = colorfilter.tint( color.black ), cap = strokecap.round ) }}
上面完成了单个雨滴的图形和动画,接下来我们使用三个雨滴组成雨水的效果。
首先可以使用row+space
的方式进行组装,但是这种方式缺少灵活性,仅通过modifier
很难准确布局三个雨滴的相对位置。因此考虑转而使用compo
的自定义布局,以提高灵活性和准确性:
layout( modifier = modifier.rotate(30f), //雨滴旋转角度 content = { // 定义子composable raindrop(modifier.fillmaxsize()) raindrop(modifier.fillmaxsize()) raindrop(modifier.fillmaxsize()) }) { measurables, constraints -> // list of measured children val placeables = measurables.mapindexed { index, measurable -> // measure each children val height = when (index) { //让三个雨滴的height不同,增加错落感 0 -> constraints.maxheight * 0.8f 1 -> constraints.maxheight * 0.9f 2 -> constraints.maxheight * 0.6f el -> 0f } measurable.measure( constraints.copy( minwidth = 0, minheight = 0, maxwidth = constraints.maxwidth / 10, // raindrop width maxheight = height.toint(), ) ) } // t the size of the layout as big as it can layout(constraints.maxwidth, constraints.maxheight) { var xposition = cons1瓦特traints.maxwidth / ((placeables.size + 1) * 2) // place children in the parent layout placeables.foreachindexed { index, placeable -> // position item on the screen placeable.place(x = xposition, y = 0) // record the y co-ord placed up to xposition += (constraints.maxwidth / ((placeables.size + 1) * 0.8f)).roundtoint() } }}
compo中,可以通过layout{...}
对composable进行自定义布局,content{...}
中定义参与布局的子composable。
跟传统android视图一样,自定义布局需要先后经历measure
、layout
两步。
measrue
:measurables返回所有待测量的子composable,constraints类似于measurespec,封装父容器对子元素的布局约束。measurable.measure()
中对子元素进行测量
layout
:placeables返回测量后的子元素,依次调用placeable.place()
对雨滴进行布局,通过xposition
预留雨滴在x轴的间隔
经过layout之后,通过modifier.rotate(30f)
对composable
进行旋转,完成最终效果:
雪天效果的关键在于雪花的飘落。
雪花的绘制非常简单,用一个圆圈代表一个雪花
canvas(modifier) { val radius = size / 2 drawcircle( //白色填充 color = color.white, radius = radius, style = fill ) drawcircle(// 黑色边框 color = color.black, radius = radius, style = stroke(width = radius * 0.5f) )}
雪花飘落的过程相对于雨滴坠落要复杂一些,由三个动画组成:
下降:通过改变y轴位置实现 (0f ~ 2.5f)左右飘移:通过该表x轴的offt实现 (-1f ~ 1f)逐渐消失:通过改变alpha实现(1f ~ 0f)借助infinitetransition
同步控制多个动画,代码如下:
@composableprivate fun snowdrop( modifier: modifier = modifier, durationmillis: int = 1000 // 雪花飘落动画的druation) { //循环播放的transition val transition = rememberinfinitetransition() //1\. 下降动画:restart动画 val animatey by transition.animatefloat( initialvalue = 0f, targetvalue = 2.5f, animationspec = infiniterepeatable( tween(durationmillis, easing = lineareasing), repeatmode.restart ) ) //2\. 左右飘移:rever动画 val animatex by transition.animatefloat( initialvalue = -1f, targetvalue = 1f, animationspec = infiniterepeatable( tween(durationmillis / 3, easing = lineareasing), repeatmode.rever ) ) //3\. alpha值:restart动画,以0f结束 val animatealpha by transition.animatefloat( initialvalue = 1f, targetvalue = 0f, animationspec = infiniterepeatable( tween(durationmillis, easing = fastoutslowineasing), ) ) canvas(modifier) { val radius = size.width / 2 // 圆心位置随animationstate改变,实现雪花飘落的效果 val _center = center.copy( x = center.x + center.x * animatex, y = center.y + center.y * animatey ) drawcircle( color = color.white.copy(alpha = animatealpha),//alpha值的变化实现雪花消失效果 center = _center, radius = radius, ) drawcircle( color = color.black.copy(alpha = animatealpha), center = _center, radius = radius, style = stroke(width = radius * 0.5f) ) }}
animatey
的targetvalue
设为2.5f
,让雪花的运动轨迹更长,看起来更加真实
像雨滴一样,对雪花也使用layout自定义布局
@composablefun snow( modifier: modifier = modifier, animate: boolean = fal,) { layout( modifier = modifier, content = { //摆放三个雪花,分别设置不同duration,增加随机性 snowdrop( modifier.fillmaxsize(), 2200) snowdrop( modifier.fillmaxsize(), 1600) snowdrop( modifier.fillmaxsize(), 1800) } ) { measurables, constraints -> val placeables = measurables.mapindexed { index, measurable -> val height = when (index) { // 雪花的height不同,也是为了增加随机性 0 -> constraints.maxheight * 0.6f 1 -> constraints.maxheight * 1.0f 2 -> constraints.maxheight * 0.7f el -> 0f } measurable.measure( constraints.copy( minwidth = 0, minheight = 0, maxwidth = constraints.maxwidth / 5, // snowdrop width maxheight = height.roundtoint(), ) ) } layout(constraints.maxwidth, constraints.maxheight) { var xposition = constraints.maxwidth / ((placeables.size + 1)) placeables.foreachindexed { index, placeable -> placeable.place(x = xposition, y = -(constraints.maxheight * 0.2).roundtoint()) xposition += (constraints.maxwidth / ((placeables.size + 1) * 0.9f)).roundtoint() } } }}
最终效果如下:
通过一个旋转的太阳代表晴天效果
太阳的图形由中间的圆形和围绕圆环的等分竖线组成。
@composablefun sun(modifier: modifier = modifier) { canvas(modifier) { val radius = size.width / 6 val stroke = size.width / 20 // draw circle drawcircle( color = color.black, radius = radius + stroke / 2, style = stroke(width = stroke), ) drawcircle( color = color.white, radius = radius, style = fill, ) // draw line 关于春节的歌曲 val linelength = radius * 0.2f val lineofft = radius * 1.8f (0..7).foreach { i -> val radians = math.toradians(i * 45.0) val offtx = lineofft * cos(radians).tofloat() val offty = lineofft * sin(radians).tofloat() val x1 = size.width / 2 + offtx val x2 = x1 + linelength * cos(radians).tofloat() val y1 = size.height / 2 + offty val y2 = y1 + linelength * sin(radians).tofloat() drawline( color = color.black, start = offt(x1, y1), end = offt(x2, y2), strokewidth = stroke, cap = strokecap.round ) } }}
均分360度,每间隔45度画一条竖线,cos计算x轴坐标,sin计算y轴坐标。
太阳的旋转动画很简单,通过modifier.rotate
不断转动canvas即可。
@composablefun sun(modifier: modifier = modifier) { //循环动画 val animatetween by rememberinfinitetransition().animatefloat( initialvalue = 0f, targetvalue = 360f, animationspec = infiniterepeatable(tween(5000), repeatmode.restart) ) canvas(modifier.rotate(animatetween)) {// 旋转动画 val radius = size.width / 6 val stroke = size.width / 20 val centerofft = offt(size.width / 30, size.width / 30) //圆心偏移量 // draw circle drawcircle( color = color.black, radius = radius + stroke / 2, style = stroke(width = stroke), center = center + centerofft //圆心偏移 ) //...略 }}
此外,drawscope
也提供了rotate
的api,也可以实现旋转效果。
最后我们给太阳的圆心增加一个偏移量,让转动更加活泼:
上面分别实现了rain、snow、sun等图形,接下来使用这些元素组合成各种天气效果。
compo的声明式语法非常有利于ui的组合:
比如,多云转阵雨,我们摆放sun
、cloud
、rain
等元素后,通过modifier调整各自位置即可:
@composablefun cloudyrain(modifier: modifier) { box(modifier.size(200.dp)){ sun(modifier.size(120.dp).offt(140.dp, 40.dp)) rain(modifier.size(80.dp).offt(80.dp, 60.dp)) cloud(modifier.align(aligment.center)) }}
让动画切换更加自然
当在多个天气动画之间进行切换时,我们希望能实现更自然的过渡。实现思路是将组成天气动画的各元素的modifier
信息变量化好看的情侣qq分组,然后通过animation
进行改变state 假设所有的天气都可以由cloud、sun、rain组合而成,无非就是offt
、size
、alpha
值的不同:
compoinfodata class iconinfo( val size: float = 1f, val offt: offt = offt(0f, 0f), val alpha: float = 1f,) //天气组合信息,即sun、cloud、rain的位置信息data class compoinfo( val sun: iconinfo, val cloud: iconinfo, val rains: iconinfo,) { operator fun times(float: float): compoinfo = copy( sun = sun * float, cloud = cloud * float, rains = rains * float ) operator fun minus(compoinfo: compoinfo): compoinfo = copy( sun = sun - compoinfo.sun, cloud = cloud - compoinfo.cloud, rains = rains - compoinfo.rains, ) operator fun plus(compoinfo: compoinfo): compoinfo = copy( sun = sun + compoinfo.sun, cloud = cloud + compoinfo.cloud, rains = rains + compoinfo.rains, )}
如上,compoinfo
中持有各种元素的位置信息,运算符重载使其可以在animation
中计算当前最新值。
接下来,使用compoinfo为不同天气定义各元素的位置信息
//晴天val sunnycompoinfo = compoinfo( sun = iconinfo(1f), cloud = iconinfo(0.8f, offt(-0.1f, 0.1f), 0f), rains = iconinfo(0.4f, offt(0.225f, 0.3f), 0f),)//多云val cloudycompoinfo = compoinfo( sun = iconinfo(0.1f, offt(0.75f, 0.2f), alpha = 0f), cloud = iconinfo(0.8f, offt(0.1f, 0.1f)), rains = iconinfo(0.4f, offt(0.225f, 0.3f), alpha = 0f),)//雨天val raincompoinfo = compoinfo( sun = iconinfo(0.1f, offt(0.75f, 0.2f), alpha = 0f), cloud = iconinfo(0.8f, offt(0.1f, 0.1f)), rains = iconinfo(0.4f, offt(0.225f, 0.3f), alpha = 1f),)
接着,定义compodicon,根据compoinfo实现不同的天气组合
@composablefun compodicon(modifier: modifier = modifier, compoinfo: compoinfo) { //各元素的compoinfo val (sun, cloud, rains) = compoinfo box(modifier) { //应用compoinfo到modifier val _modifier = remember(unit) { { icon: iconinfo -> modifier .offt( icon.size * icon.offt.x, icon.size * icon.offt.y ) .size(icon.size) .alpha(icon.alpha) } } sun(_modifier(sun)) rains(_modifier(rains)) animatablecloud(_modifier(cloud)) }}
最后,定义compodweather
记录当前compodicon
,并在其发生更新时使用动画进行过度:
@composablefun compodweather(modifier: modifier, compodicon: compodicon) { val (cur, tcur) = remember { mutablestateof(compodicon) } var trigger by remember { mutablestateof(0f) } disposableeffect(compodicon) { trigger = 1f ondispo { } } //创建动画(0f ~ 1f),用于更新compoinfo val animatefloat by animatefloatasstate( targetvalue = trigger, animationspec = tween(1000) ) { //当动画结束时,更新compoweather到最新state tcur(compodicon) trigger = 0f } //根据animationstate计算当前compoinfo val compoinfo = remember(animatefloat) { cur.compodicon + (weathericon.compodicon - cur.compodicon) * animatefloat }
以上就是利用jetpack compo绘制可爱的天气动画的详细内容,更多关于jetpack compo绘制动画的资料请关注www.887551.com其它相关文章!
本文发布于:2023-04-04 16:28:17,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/fanwen/zuowen/be4fe572566f09ad95e6e46b0e4d994e.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文word下载地址:利用Jetpack Compose绘制可爱的天气动画.doc
本文 PDF 下载地址:利用Jetpack Compose绘制可爱的天气动画.pdf
留言与评论(共有 0 条评论) |