前言
最近打算做一波东西巩固一下自己近期所学所得。话不多说,先看一下最终完成的效果图:
下雨.gif
这里比较懒……第二个图片中还是降雨……不过这不是关键点……
下雪.gif
录制的mp4,转成了gif。第一个gif设置了帧率,所以看起来可能掉帧比较严重,但是实际上并不会,因为这里我也注意了1s要绘制60帧的问题。阅读本文需要一些基本的View知识和会一些基础Kotlin语法。说实话,就知识点来说,跟Kotlin是没多大关系的,只要懂基本的语法就可以了。
理清思路
在动手前先要理一下思路,从以下几个方面来分析一下该采用什么方案来实现这个效果:
工作线程:首先要想到的是:这个下雨的效果需要通过不停的绘制来实现,如果在主线程做这个操作,很有可能会阻塞主线程,导致ANR或者异常卡顿。所以需要一个能在子线程进行绘制的View,毫无疑问SurfaceView可以满足这个需求。
如何实现:分析一下一颗雨滴的实现。首先,简单的效果其实可以用画线的方式代替。并不是每个人都有写轮眼,动态视力那么好的,一旦动起来谁还知道他是条线还是雨滴……当然了,Canvas绘制的API有很多,并不一定非要用这种方式来实现。所以在在设计类的时候我们将draw的方法设置成可以让子类复写就可以了,你不满意我的实现?没问题,我给你改的自由~
下落的实现:让雨滴动起来,有两种方式,一种是纯按坐标来绘制,另外一种是利用属性动画,自己重写估值器,动态改变y值。最终我还是采用了前一种方案,后一种属性动画的方案我为什么放弃了呢?原因是:这里的绘制的方式是靠外部不断的触发绘制事件来实现动态绘制的,很显然第一种方式更加符合这里的情况。
以上就是我初期的一些关于实现的思考了,接下来是代码实现分析。
代码实现分析
先放代码结构图:
代码结构
WeatherShape所有天气的父类,Rain和Snow是两个具体实现类。
看一下父类的代码:
package com.xiasuhuei321.gank_kotlin.customview.weather import android.graphics.Canvas import android.graphics.Paint import android.graphics.PointF import com.xiasuhuei321.gank_kotlin.context import com.xiasuhuei321.gank_kotlin.extension.getScreenWidth import java.util.* /** * Created by xiasuhuei321 on 2017/9/5. * author:luo * e-mail:xiasuhuei321@163.com * * desc: All shape's parent class.It describes a shape will have * what feature.It's draw flows are: * 1.Outside the class init some value such as the start and the * end point. * 2.Invoke draw(Canvas) method, in this method, there are still * two flows: * 1) Get random value to init paint, this will affect the shape * draw style. * 2) When the shape is not used, invoke init method, and when it * is not used invoke drawWhenInUse(Canvas) method. It should be * override by user and to implement draw itself. * */ abstract class WeatherShape(val start: PointF, val end: PointF) { open var TAG = "WeatherShape" /** * 是否是正在被使用的状态 */ var isInUse = false /** * 是否是随机刷新的Shape */ var isRandom = false /** * 下落的速度,特指垂直方向,子类可以实现自己水平方向的速度 */ var speed = 0.05f /** * shape的宽度 */ var width = 5f var shapeAlpha = 100 var paint = Paint().apply { strokeWidth = width isAntiAlias = true alpha = alpha } // 总共下落的时间 var lastTime = 0L // 原始x坐标位置 var originX = 0f /** * 根据自己的规则计算加速度,如果是匀速直接 return 0 */ abstract fun getAcceleration(): Float /** * 绘制自身,这里在Shape是非使用的时候进行一些初始化操作 */ open fun draw(canvas: Canvas) { if (!isInUse) { lastTime += randomPre() initStyle() isInUse = true } else { drawWhenInUse(canvas) } } /** * Shape在使用的时候调用此方法 */ abstract fun drawWhenInUse(canvas: Canvas) /** * 初始化Shape风格 */ open fun initStyle() { val random = Random() // 获取随机透明度 shapeAlpha = random.nextInt(155) + 50 // 获得起点x偏移 val translateX = random.nextInt(10).toFloat() + 5 if (!isRandom) { start.x = translateX + originX end.x = translateX + originX } else { // 如果是随机Shape,将x坐标随机范围扩大到整个屏幕的宽度 val randomWidth = random.nextInt(context.getScreenWidth()) start.x = randomWidth.toFloat() end.x = randomWidth.toFloat() } speed = randomSpeed(random) // 初始化length的工作留给之后对应的子类去实现 // 初始化color也留给子类去实现 paint.apply { alpha = shapeAlpha strokeWidth = width isAntiAlias = true } // 如果有什么想要做的,刚好可以在追加上完成,就使用这个函数 wtc(random) } /** * Empty body, this will be invoke in initStyle * method.If current initStyle method can satisfy your need * but you still add something, by override this method * will be a good idea to solve the problem. */ open fun wtc(random:Random): Unit { } abstract fun randomSpeed(random: Random): Float /** * 获取一个随机的提前量,让shape在竖屏上有一个初始的偏移 */ open fun randomPre(): Long { val random = Random() val pre = random.nextInt(1000).toLong() return pre } }
说起这个代码,恩,还是经历过一番重构的……周六去找同学玩的路上顺便重构了一下,将一些可以放到基类中的操作都抽取到了基类中。这样虽然灵活不足,但是子类可以很方便的通过继承实现一个需要类似功能的东西,就比如这里的下雨和下雪。顺便吐槽一下……我注释的风格不太好,中英混搭……如果你仔细观察,可以看到gif中的雨点或者雪花形态可能都有一些些的不一样,是的,每一滴雨和雪花,都经过了一些随机的转变。
里面比较重要的两个属性是isInUse和isRandom,本来想用一个容器来作为Shape的管理类,统一管理,但是这样肯定会让使用和复用的流程更加复杂。最后还是决定用简单一点的方法,Shape内部保存一个使用状态和是否是随机的。isRandoma表示这个Shape是否是随机的,随机在目前的代码中会体现在Shape的x坐标上。如果随机标识是true,那么x坐标将是0 ~ ScreenWidth中的任意值。那么不是随机的呢?在我的实现中,同一类Shape将会被分为两类,一类常量组。会拥有相对固定的x值,但是也会有10~15px的随机偏移。另一类就是随机组,x值全屏自己随机,这样就尽量让屏幕各处都有雨滴(雪花)但会有疏密之别。initStyle就是这一随机的过程,有兴趣可以看看实现~
start和end是Shape的左上角点和右下角点,如果你对于Cavans的api有了解,就应该知道通过对start和end的转换和计算,可以绘制出大部分的形状。
接下来看一下具体实现的Snow类:
package com.xiasuhuei321.gank_kotlin.customview.weather import android.graphics.* import com.xiasuhuei321.gank_kotlin.context import com.xiasuhuei321.gank_kotlin.extension.getScreenHeight import java.util.* /** * Created by xiasuhuei321 on 2017/9/5. * author:luo * e-mail:xiasuhuei321@163.com */ class Snow(start: PointF, end: PointF) : WeatherShape(start, end) { /** * 圆心,用户可以改变这个值 */ var center = calcCenter() /** * 半径 */ var radius = 10f override fun getAcceleration(): Float { return 0f } override fun drawWhenInUse(canvas: Canvas) { // 通过圆心与半径确定圆的位置及大小 val distance = speed * lastTime center.y += distance start.y += distance end.y += distance lastTime += 16 canvas.drawCircle(center.x, center.y, radius, paint) if (end.y > context.getScreenHeight()) clear() } fun calcCenter(): PointF { val center = PointF(0f, 0f) center.x = (start.x + end.x) / 2f center.y = (start.y + end.y) / 2f return center } override fun randomSpeed(random: Random): Float { // 获取随机速度0.005 ~ 0.01 return (random.nextInt(5) +