利用SurfaceView實現(xiàn)下雨與下雪動畫效果詳解(Kotlin語法)
前言
最近打算做一波東西鞏固一下自己近期所學所得。話不多說,先看一下最終完成的效果圖:

下雨.gif
這里比較懶……第二個圖片中還是降雨……不過這不是關鍵點……

下雪.gif
錄制的mp4,轉(zhuǎn)成了gif。第一個gif設置了幀率,所以看起來可能掉幀比較嚴重,但是實際上并不會,因為這里我也注意了1s要繪制60幀的問題。閱讀本文需要一些基本的View知識和會一些基礎Kotlin語法。說實話,就知識點來說,跟Kotlin是沒多大關系的,只要懂基本的語法就可以了。
理清思路
在動手前先要理一下思路,從以下幾個方面來分析一下該采用什么方案來實現(xiàn)這個效果:
- 工作線程:首先要想到的是:這個下雨的效果需要通過不停的繪制來實現(xiàn),如果在主線程做這個操作,很有可能會阻塞主線程,導致ANR或者異??D。所以需要一個能在子線程進行繪制的View,毫無疑問SurfaceView可以滿足這個需求。
- 如何實現(xiàn):分析一下一顆雨滴的實現(xiàn)。首先,簡單的效果其實可以用畫線的方式代替。并不是每個人都有寫輪眼,動態(tài)視力那么好的,一旦動起來誰還知道他是條線還是雨滴……當然了,Canvas繪制的API有很多,并不一定非要用這種方式來實現(xiàn)。所以在在設計類的時候我們將draw的方法設置成可以讓子類復寫就可以了,你不滿意我的實現(xiàn)?沒問題,我給你改的自由~
- 下落的實現(xiàn):讓雨滴動起來,有兩種方式,一種是純按坐標來繪制,另外一種是利用屬性動畫,自己重寫估值器,動態(tài)改變y值。最終我還是采用了前一種方案,后一種屬性動畫的方案我為什么放棄了呢?原因是:這里的繪制的方式是靠外部不斷的觸發(fā)繪制事件來實現(xiàn)動態(tài)繪制的,很顯然第一種方式更加符合這里的情況。
以上就是我初期的一些關于實現(xiàn)的思考了,接下來是代碼實現(xiàn)分析。
代碼實現(xiàn)分析
先放代碼結(jié)構(gòu)圖:

代碼結(jié)構(gòu)
WeatherShape所有天氣的父類,Rain和Snow是兩個具體實現(xiàn)類。
看一下父類的代碼:
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"
/**
* 是否是正在被使用的狀態(tài)
*/
var isInUse = false
/**
* 是否是隨機刷新的Shape
*/
var isRandom = false
/**
* 下落的速度,特指垂直方向,子類可以實現(xiàn)自己水平方向的速度
*/
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
/**
* 根據(jù)自己的規(guī)則計算加速度,如果是勻速直接 return 0
*/
abstract fun getAcceleration(): Float
/**
* 繪制自身,這里在Shape是非使用的時候進行一些初始化操作
*/
open fun draw(canvas: Canvas) {
if (!isInUse) {
lastTime += randomPre()
initStyle()
isInUse = true
} else {
drawWhenInUse(canvas)
}
}
/**
* Shape在使用的時候調(diào)用此方法
*/
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的工作留給之后對應的子類去實現(xiàn)
// 初始化color也留給子類去實現(xiàn)
paint.apply {
alpha = shapeAlpha
strokeWidth = width
isAntiAlias = true
}
// 如果有什么想要做的,剛好可以在追加上完成,就使用這個函數(shù)
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
}
}
說起這個代碼,恩,還是經(jīng)歷過一番重構(gòu)的……周六去找同學玩的路上順便重構(gòu)了一下,將一些可以放到基類中的操作都抽取到了基類中。這樣雖然靈活不足,但是子類可以很方便的通過繼承實現(xiàn)一個需要類似功能的東西,就比如這里的下雨和下雪。順便吐槽一下……我注釋的風格不太好,中英混搭……如果你仔細觀察,可以看到gif中的雨點或者雪花形態(tài)可能都有一些些的不一樣,是的,每一滴雨和雪花,都經(jīng)過了一些隨機的轉(zhuǎn)變。
里面比較重要的兩個屬性是isInUse和isRandom,本來想用一個容器來作為Shape的管理類,統(tǒng)一管理,但是這樣肯定會讓使用和復用的流程更加復雜。最后還是決定用簡單一點的方法,Shape內(nèi)部保存一個使用狀態(tài)和是否是隨機的。isRandoma表示這個Shape是否是隨機的,隨機在目前的代碼中會體現(xiàn)在Shape的x坐標上。如果隨機標識是true,那么x坐標將是0 ~ ScreenWidth中的任意值。那么不是隨機的呢?在我的實現(xiàn)中,同一類Shape將會被分為兩類,一類常量組。會擁有相對固定的x值,但是也會有10~15px的隨機偏移。另一類就是隨機組,x值全屏自己隨機,這樣就盡量讓屏幕各處都有雨滴(雪花)但會有疏密之別。initStyle就是這一隨機的過程,有興趣可以看看實現(xiàn)~
start和end是Shape的左上角點和右下角點,如果你對于Cavans的api有了解,就應該知道通過對start和end的轉(zhuǎn)換和計算,可以繪制出大部分的形狀。
接下來看一下具體實現(xiàn)的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) + 5) / 1000f
}
override fun wtc(random: Random) {
// 設置顏色漸變
val shader = RadialGradient(center.x, center.y, radius,
Color.parseColor("#FFFFFF"), Color.parseColor("#D1D1D1"),
Shader.TileMode.CLAMP)
// 外部設置的起始點其實并不對,先計算出半徑
radius = random.nextInt(10) + 15f
// 根據(jù)半徑計算start end
end.x = start.x + radius
end.y = start.y + radius
// 計算圓心
calcCenter()
paint.apply {
setShader(shader)
}
}
fun clear() {
isInUse = false
lastTime = 0
start.y = -radius * 2
end.y = 0f
center = calcCenter()
}
}
這個類只要理解了圓心的計算和繪制,基本也就沒什么東西了。首先排除干擾項,getAcceleration這玩意在設計之初是用來通過加速度計算路程的,后來發(fā)現(xiàn)……算了,還是勻速吧……于是都return 0f了。這里wtc()函數(shù)和drawWhenInUse可能會看的你一臉懵逼,什么函數(shù)名,drawWhenInUse倒是見名知意,這wtc()是什么玩意?這里wtc是相當于一種追加初始化,完全狀態(tài)的函數(shù)名應該是wantToChange() 。這些個函數(shù)調(diào)用流程是這樣的:

流程圖
其中draw(canvas)是父類的方法,對供外部調(diào)用的方法,在isInUse標識位為false時對Shape進行初始化操作,具體的就是調(diào)用initStyle()方法,而wtc()則會在initStyle()方法的最后調(diào)用。如果你有什么想要追加的初始化,可以通過這個函數(shù)實現(xiàn)。而drawWhenInUse(canvas)方法則是需要實現(xiàn)動態(tài)繪制的函數(shù)了。我這里就是在wtc()函數(shù)中進行了一些初始化操作,并且根據(jù)圓的特性重新計算了start、end和圓心。
接下來,就看看我們到底是怎么把這些充滿個性(口胡)的雪繪制到屏幕上:
package com.xiasuhuei321.gank_kotlin.customview.weather
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.util.AttributeSet
import android.view.SurfaceHolder
import android.view.SurfaceView
import com.xiasuhuei321.gank_kotlin.extension.LogUtil
import java.lang.Exception
/**
* Created by xiasuhuei321 on 2017/9/5.
* author:luo
* e-mail:xiasuhuei321@163.com
*/
class WeatherView(context: Context, attributeSet: AttributeSet?, defaultStyle: Int) :
SurfaceView(context, attributeSet, defaultStyle), SurfaceHolder.Callback {
private val TAG = "WeatherView"
constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)
constructor(context: Context) : this(context, null, 0)
// 低級并發(fā),Kotlin中支持的不是很好,所以用一下黑科技
val lock = Object()
var type = Weather.RAIN
var weatherShapePool = WeatherShapePool()
@Volatile var canRun = false
@Volatile var threadQuit = false
var thread = Thread {
while (!threadQuit) {
if (!canRun) {
synchronized(lock) {
try {
LogUtil.i(TAG, "條件尚不充足,阻塞中...")
lock.wait()
} catch (e: Exception) {
}
}
}
val startTime = System.currentTimeMillis()
try {
// 正式開始表演
val canvas = holder.lockCanvas()
if (canvas != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
draw(canvas, type, startTime)
}
holder.unlockCanvasAndPost(canvas)
val drawTime = System.currentTimeMillis() - startTime
// 平均16ms一幀才能有順暢的感覺
if (drawTime < 16) {
Thread.sleep(16 - drawTime)
}
} catch (e: Exception) {
// e.printStackTrace()
}
}
}.apply { name = "WeatherThread" }
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
// surface發(fā)生了變化
// canRun = true
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
// 在這里釋放資源
canRun = false
LogUtil.i(TAG, "surfaceDestroyed")
}
override fun surfaceCreated(holder: SurfaceHolder?) {
threadQuit = false
canRun = true
try {
// 如果沒有執(zhí)行wait的話,這里notify會拋異常
synchronized(lock) {
lock.notify()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
init {
LogUtil.i(TAG, "init開始")
holder.addCallback(this)
holder.setFormat(PixelFormat.RGBA_8888)
// initData()
setZOrderOnTop(true)
// setZOrderMediaOverlay(true)
thread.start()
}
private fun draw(canvas: Canvas, type: Weather, startTime: Long) {
// type什么的先放一邊,先實現(xiàn)一個
weatherShapePool.drawSnow(canvas)
}
enum class Weather {
RAIN,
SNOW
}
fun onDestroy() {
threadQuit = true
canRun = true
try {
synchronized(lock) {
lock.notify()
}
} catch (e: Exception) {
}
}
}
init{}是kotlin中提供給我們用于初始化的代碼塊,在init進行了一些初始化操作并讓線程start了。看一下線程中執(zhí)行的代碼,首先會判斷一個叫做canRun的標識,這個標識會在surface被創(chuàng)建的時候置為true,否則將會通過一個對象讓這個線程等待。而在surface被創(chuàng)建后,則會調(diào)用notify方法讓線程重新開始工作。之后是進行繪制的工作,繪制前后會有一個計時的動作,計算時間是否小于16ms,如果不足,則讓thread sleep 補足插值。因為16ms一幀的繪制速度就足夠了,不需要繪制太快浪費資源。
這里可以看到我創(chuàng)建了一個Java的Object對象,主要是因為Kotlin本身對于一些并發(fā)原語支持的并不好。Kotlin中任何對象都是繼承與Any,Any并沒有wait、notify等方法,所以這里用了黑科技……創(chuàng)建了Java對象……
代碼中關鍵代碼繪制調(diào)用了WeatherShapePool的drawRain(canvas)方法,最后在看一下這個類:
package com.xiasuhuei321.gank_kotlin.customview.weather
import android.graphics.Canvas
import android.graphics.PointF
import com.xiasuhuei321.gank_kotlin.context
import com.xiasuhuei321.gank_kotlin.extension.getScreenWidth
/**
* Created by xiasuhuei321 on 2017/9/7.
* author:luo
* e-mail:xiasuhuei321@163.com
*/
class WeatherShapePool {
val constantRain = ArrayList<Rain>()
val randomRain = ArrayList<Rain>()
val constantSnow = ArrayList<Snow>()
val randomSnow = ArrayList<Snow>()
init {
// 初始化
initData()
initSnow()
}
private fun initData() {
val space = context.getScreenWidth() / 20
var currentSpace = 0f
// 將其均勻的分布在屏幕x方向上
for (i in 0..19) {
val rain = Rain(PointF(currentSpace, 0f), PointF(currentSpace, 0f))
rain.originLength = 20f
rain.originX = currentSpace
constantRain.add(rain)
currentSpace += space
}
for (j in 0..9) {
val rain = Rain(PointF(0f, 0f), PointF(0f, 0f))
rain.isRandom = true
rain.originLength = 20f
randomRain.add(rain)
}
}
fun drawRain(canvas: Canvas) {
for (r in constantRain) {
r.draw(canvas)
}
for (r in randomRain) {
r.draw(canvas)
}
}
private fun initSnow(){
val space = context.getScreenWidth() / 20
var currentSpace = 0f
// 將其均勻的分布在屏幕x方向上
for (i in 0..19) {
val snow = Snow(PointF(currentSpace, 0f), PointF(currentSpace, 0f))
snow.originX = currentSpace
snow.radius = 20f
constantSnow.add(snow)
currentSpace += space
}
for (j in 0..19) {
val snow = Snow(PointF(0f, 0f), PointF(0f, 0f))
snow.isRandom = true
snow.radius = 20f
randomSnow.add(snow)
}
}
fun drawSnow(canvas: Canvas){
for(r in constantSnow){
r.draw(canvas)
}
for (r in randomSnow){
r.draw(canvas)
}
}
}
這個類還是比較簡單的,只是一個單純的容器,至于叫Pool……因為剛開始自己想的是自己管理回收復用之類的,所以起了個名叫Pool,后來感覺這玩意好像不用實現(xiàn)的這么復雜……
總之,這玩意,會者不難,我的代碼也非盡善盡美,如果我有任何紕漏或者你有什么好的意見,都可以提出,郵件或者是在文章下評論最佳。
項目地址:https://github.com/ForgetAll/GankKotlin
本地下載:http://xiazai.jb51.net/201709/yuanma/GankKotlin(jb51.net).rar
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關文章
Android Room數(shù)據(jù)庫多表查詢的使用實例
這篇文章主要介紹了Android Room數(shù)據(jù)庫多表查詢的使用實例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03
Android使用MediaRecorder類實現(xiàn)視頻和音頻錄制功能
Android提供了MediaRecorder這一個類來實現(xiàn)視頻和音頻的錄制功能,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友參考下吧2018-07-07
Android中將View的內(nèi)容保存為圖像的簡單實例
這篇文章主要介紹了Android中將View的內(nèi)容保存為圖像的簡單實例,有需要的朋友可以參考一下2014-01-01
Android開發(fā)之數(shù)據(jù)的存儲方式詳解
本篇文章主要介紹了Android開發(fā)之數(shù)據(jù)的存儲方式,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧。2016-11-11
android自定義控件ImageView實現(xiàn)圓形圖片
這篇文章主要為大家詳細介紹了android自定義控件ImageView實現(xiàn)圓形圖片,適用于用戶頭像,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12
Android view自定義實現(xiàn)動態(tài)進度條
這篇文章主要介紹了Android view自定義實現(xiàn)動態(tài)進度條的相關資料,這里提供實例代碼及實現(xiàn)效果圖,需要的朋友可以參考下2016-12-12
Android開發(fā)基礎使用ProgressBar加載進度條示例
這篇文章主要介紹了安卓開發(fā)基礎使用ProgressBar加載進度條示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
Android 個人理財工具二:使用SQLite實現(xiàn)啟動時初始化數(shù)據(jù)
本文主要介紹 Android 使用SQLite實現(xiàn)啟動時初始化數(shù)據(jù),這里對SQLite 的數(shù)據(jù)庫進行詳解,附有示例代碼,有興趣的小伙伴可以參考下2016-08-08

