又想做屏保了,这次用Compose做个蜂窝墙

4,697 阅读11分钟

距离上次做飘动的小球的屏保已经过去了一个多月,发现那些小球看多了也腻,然而想再做个屏保么也没啥思路,但是前阵子在陪娃看了个新出的动画片,叫啥量子战队的,里面主角总部的墙引起了我的注意,那是一个个小格子,每个格子是六边形形状,就像蜂窝一样,然后整体颜色还会慢慢改变,这一下子灵感就挠得一下就上来了,我也来撸个蜂窝墙出来

源码地址

设计思路

整体的一个设计过程分以下几步

  1. 整个过程分为打开屏保与关闭屏保
  2. 打开屏保的时候,首先展示一个蜂窝墙的一个架子
  3. 架子展示完之后,逐渐给一个个蜂窝格子填充颜色
  4. 填充完颜色之后,整个蜂窝墙背景呈现一个多色渐变并且背景顺时针旋转
  5. 关闭屏保时,上述过程反向操作,最终退出程序

第一步:先做屏保开关

Window函数中创建一个MenuBar,并且里面有两个Item,分别用来执行打开与关闭屏保的操作,我们在创建一个switch变量,并让它作为参数传入屏保的Compose函数中,点击Item的时候,改变switch的值,代码如下

image.png

第二步:蜂窝墙架子

一个蜂窝墙架子,其实就是一个六边形的网格布局,平时让我们画一个圆点布局,会画,画一个正方形格子布局,也能画,但是六边形布局咋画呢,DrawScope里翻了一圈也没找到对应的Api,那么只能“徒手画”了,先分析下六边形网格布局的特点,它看起来是由若干个点朝着圆周上三个方向延伸出去三根直线,当每一个点的三根直线与其他点的三根直线重合的时候,一个六边形网格布局就形成了,我们只需要将这些点找出来,然后在根据角度计算出延伸方向的终点坐标,将两点连起来就好了,听起来像是这么一回事,但是先看看下面的图

image.png

我们看到中间的横线左右两个端点延伸出去的直线,角度是不一样的,也就是说我们不仅要找出所有的点,还必须确定好每一个点延伸出去的方向才能绘制不同的直线,那这个工作量无疑是庞大的,我们得找找其他方案,得找出一个固定不变的绘制规律,有吗?我们再看看下面这张图

image.png

这是一个六边形网格布局的一部分,我们看中间那个六边形,这个六边形的中心点我们用黑色标出来了,我们发现这个黑点到六边形六个角的角度分别为0,60,120,180,240,300,并且每一个六边形都有这样的规律,那么我们是不是可以找出所有六边形的中心点坐标,然后计算出六个角的点坐标,并且将相邻的角度计算出来的坐标连起来,六边形布局就完成了呢,那么先创建出一个存放角度的数组,以及六边形的边长

image.png

anglist就是存放角度的数组,而radius就是六边形的边长,同时也是中心点到每一个角的距离,接下来就是思考下如何将所有的中心点找出来,我们再看看下图

image.png

请无视我粗糙的画功,我们将之前那张图补充点元素就是上面这个图,其中第一排与第三排的中心点,它们第一个点都与画布边距一个radius的距离,紧接着就是3 * radius的距离递增,而第二排与第四排的中心点略有不同,第一个点没画出来,它在画布外面,与画布边距是负的0.5 * radius的距离,然后也是3 * radius的距离递增,通过这些可以将中心点的横坐标确定下来

image.png

xUnit为中心点之间横坐标的距离,xLength是单排横向上的格子数量,加一是为了防止边上出现不完整的格子,xListxList2是单排与双排的横坐标数组,我们再来看看上图来确定纵坐标,两个中心点的纵向距离为两倍的中心点到六边形底部的距离,那这个距离怎么算呢,我们使用勾股定理就能算出来,radius的平方减去一半radius的平方开根号就得到了,代码如下

image.png

yUnit就是我们刚刚使用勾股定理算出来的两点纵向距离,yLengthxLength一样,加上一防止有不完整的格子出现,yListyList2是单列与双列的纵坐标数组,现在就可以通过遍历单排单列坐标与双排双列坐标将所有六边形画出来了

image.png

其中pointXpointY函数是用来通过半径,中心点与角度计算出终点横纵坐标,代码如下

image.png

运行一遍程序之后,就获得了我们蜂窝墙的一个架子了

image.png

现在我们结合之前做好的开关,让这个架子能够在打开开关之后才出来,那么未打开的时候网格就是透明色的,打开才是白色,所以这里有个颜色的过渡过程,我们使用animateColorAsState函数来实现

image.png

这里新建了一个Int的变量gridSwitch来作为我们动画的真正开关,当不为0的时候,格子颜色永远是白色,这样做的目的是为了当我们关闭屏保的时候,如果直接使用布尔变量switch,那么就会直接让格子消失,就不会有设计思路里面说的将打开动画反向再执行一遍的过程,现在我们将gridColor代入到drawLine的函数里面,就初步得到一个打开网格的过程

0712aa1.gif

现在在网格出现的时候增加一个效果,现在我们看到的是整个网格都展示出来,现在只让网格的交叉点先出来,等交叉点出来之后,两点之间再延伸出一根白线将点连在一起,怎么做呢?是动态改变drawLinestart点或者end点吗?不是的,我们这里使用PathEffect里面的dashPathEffect函数,这个函数看名字就知道是用来画虚线的,怎么画呢,看下它源码里面咋说的

image.png

一大堆注释,概括一下就是这个函数需要传入两个参数,第一个参数intervals是一个float数组,通常这个数组里面放两个值,第一个是实线长度,第二个是实线间的间隔大小,第二个参数phase,感觉这个命名改成offset更合适,意思就是绘制线的起始位置朝反方向挪动phase距离,默认是0,那么我先用PathEffect给网格的线加个虚线效果试试

image.png image.png

这里给虚线效果设置的是实线长为10,间隔长也为10,原因是我们单条线的radius之前是已经设置好了30,所以间隔与实线长度关系是实线长度=(radius-间隔长)/2,当间隔长刚好等于radius的时候,那么只剩交叉点,没有线了,反之当间隔为0的时候,那么间隔就看不到了,通过这两个临界值以及实线与间隔的关系,我们可以做出一个点与点相连变成网格的动效

image.png

我们给虚线的动效新建个开关dashState,并且在gridColor动画结束之后,将dashState开关打开开启连接动画,另外将dash代入到绘制drawLine函数中

image.png 0712aa3.gif

第三步:给蜂窝填充颜色

这里的填充颜色不是简单的将整个画布设置个颜色就完事了,而是需要在每一个格子都设置个颜色进去,那么问题来了,我们之前画格子是只画了线,格子内部其实是啥也没有的,所以与其说是填充颜色,还不如说是画一个带颜色的六边形,还是逃不了啊,那就只能画了,之前说drawScope里面没有现成的api来画六边形,那么我们可以多走一步,通过绘制Path来形成六边形,道理也是一样的,先是moveTo到一个点,然后drawLine到下一个点,一圈下来也是个六边形了,那么第一步,需要收集好所有六边形的中心点,这样才能达到可以在随意一个位置就画六边形的效果

image.png

这里的pointList就是拿来放所有中心点的数组,pathList用来存放最终绘制六边形Path的数组,然后我们在绘制六边形格子的时候,顺便将每个中心点add到这个pointList里面去,像这样

image.png

然后通过判断pointList非空的前提下,将每个六边形的Path创建出来并addpathList

image.png

这里添加完pathList之后还shuffle()一下的原因是将pathList打乱后,在遍历绘制六边形的时候可以随机的挑选格子来填充,而填充的时候,是先绘制0个,然后绘制数量递增下去直到pathList.size,同样后面关闭屏保时候,数量也是从pathList.size递减下去直到0,所以这个过程也可以用animateIntAsState函数来创建

image.png

这里新增一个开关pathState,在dash动画结束之后打开,将两个动画衔接在一起,而在Canvas中绘制六边形色块的时候,每次绘制的数量不能超过pathIndex,所以绘制六边形色块的代码如下

image.png

现在再看下效果图

0713aa1.gif

看到色块已经都填到格子里了,接下去就是让整个蜂窝墙背景色转动起来

第四步:背景色转动起来

现在是要让背景色呈现一个多色渐变并且转动起来的效果,那么先将渐变色值创建出来

image.png

这里就来个四色渐变,什么时候开启渐变呢?就是当色块都填充完毕的时候,所以再创建个开关,并且在色块填充完毕后打开

image.png

gradientColorState开关打开之后,在刚才绘制色块的地方加个逻辑判断,如果开关打开就开启渐变,下面是代码与运行效果

image.png 0713aa2.gif

然后是如何让背景色转起来,Brush.linearGradient的参数里面还有两个参数,分别是startend,代表着渐变方向,默认是从左上角到右下角方向,我们可以通过不断改变startend的值,来让整体效果看起来像是在旋转一样,改变的方式就是以画布中心会圆心,定长为半径,在一个圆周上不断得到对应的坐标点,这个点就作为start,而在对应start点的角度上加上180度计算出来的就是end点,所以我们需要一个0到360度的循环改变动画

image.png

然后使用gradientAngle分别计算出start与end点,代入到Brush.linearGradient函数中

image.png

整体效果就能让它转动起来了

0713aa3.gif

第五步:关闭屏保

刚刚我们完成了打开屏保的所有过程,现在我们希望关闭屏保的时候,上述过程可以方向进行知道六边形格子消失,再退出程序,那么首先当关闭屏保的时候,switch参数就变成false了,我们在这个时候就需要将背景色关闭,并且去除六边形色块

image.png

这里else分支还加上gridSwitch.value == 1的判断是为了防止程序一开始就进行关闭屏保的流程,然后当pathIndex动画结束的时候再将六边形格子的连接线动画关闭,那么格子的连接线就又会断开了

image.png

断开之后才是让格子从白色变回刚开始的透明色

image.png

最后当格子全部消失时候,就该退出程序了,这里需要将事件传递给外面的Window函数,然后去执行exitApplication()操作,所以我们给BeehiveWall函数新增一个高阶函数的参数callback,在格子消失之后调用callback,那么事件就传递出去了

image.png image.png

完整的一个开启关闭的过程就出来了

0713aa4.gif

然后再把整个背景弄成透明的,先把Canvas的背景色去掉,然后在Window函数中加入这些代码

image.png

这样就能保证我们整个窗口是全屏加透明的了,我们看下最终效果图

0717aa3.gif

总结

这个蜂窝墙的屏保就算完成了,感兴趣的小伙伴可以把源码下下来然后自己改改样式玩玩看,后面如果想到其他有意思的屏保也会尝试做出来给大家看