您好,登錄后才能下訂單哦!
這篇文章主要介紹了利用SurfaceView如何實(shí)現(xiàn)下雨與下雪動(dòng)畫的效果,具有一定借鑒價(jià)值,需要的朋友可以參考下。希望大家閱讀完這篇文章后大有收獲。下面讓小編帶著大家一起了解一下。
最近打算做一波東西鞏固一下自己近期所學(xué)所得。話不多說,先看一下最終完成的效果圖:
下雨.gif
這里比較懶……第二個(gè)圖片中還是降雨……不過這不是關(guān)鍵點(diǎn)……
下雪.gif
錄制的mp4,轉(zhuǎn)成了gif。第一個(gè)gif設(shè)置了幀率,所以看起來可能掉幀比較嚴(yán)重,但是實(shí)際上并不會,因?yàn)檫@里我也注意了1s要繪制60幀的問題。閱讀本文需要一些基本的View知識和會一些基礎(chǔ)Kotlin語法。說實(shí)話,就知識點(diǎn)來說,跟Kotlin是沒多大關(guān)系的,只要懂基本的語法就可以了。
理清思路
在動(dòng)手前先要理一下思路,從以下幾個(gè)方面來分析一下該采用什么方案來實(shí)現(xiàn)這個(gè)效果:
工作線程:首先要想到的是:這個(gè)下雨的效果需要通過不停的繪制來實(shí)現(xiàn),如果在主線程做這個(gè)操作,很有可能會阻塞主線程,導(dǎo)致ANR或者異??D。所以需要一個(gè)能在子線程進(jìn)行繪制的View,毫無疑問SurfaceView可以滿足這個(gè)需求。
如何實(shí)現(xiàn):分析一下一顆雨滴的實(shí)現(xiàn)。首先,簡單的效果其實(shí)可以用畫線的方式代替。并不是每個(gè)人都有寫輪眼,動(dòng)態(tài)視力那么好的,一旦動(dòng)起來誰還知道他是條線還是雨滴……當(dāng)然了,Canvas繪制的API有很多,并不一定非要用這種方式來實(shí)現(xiàn)。所以在在設(shè)計(jì)類的時(shí)候我們將draw的方法設(shè)置成可以讓子類復(fù)寫就可以了,你不滿意我的實(shí)現(xiàn)?沒問題,我給你改的自由~
下落的實(shí)現(xiàn):讓雨滴動(dòng)起來,有兩種方式,一種是純按坐標(biāo)來繪制,另外一種是利用屬性動(dòng)畫,自己重寫估值器,動(dòng)態(tài)改變y值。最終我還是采用了前一種方案,后一種屬性動(dòng)畫的方案我為什么放棄了呢?原因是:這里的繪制的方式是靠外部不斷的觸發(fā)繪制事件來實(shí)現(xiàn)動(dòng)態(tài)繪制的,很顯然第一種方式更加符合這里的情況。
以上就是我初期的一些關(guān)于實(shí)現(xiàn)的思考了,接下來是代碼實(shí)現(xiàn)分析。
代碼實(shí)現(xiàn)分析
先放代碼結(jié)構(gòu)圖:
代碼結(jié)構(gòu)
WeatherShape所有天氣的父類,Rain和Snow是兩個(gè)具體實(shí)現(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 /** * 是否是隨機(jī)刷新的Shape */ var isRandom = false /** * 下落的速度,特指垂直方向,子類可以實(shí)現(xiàn)自己水平方向的速度 */ var speed = 0.05f /** * shape的寬度 */ var width = 5f var shapeAlpha = 100 var paint = Paint().apply { strokeWidth = width isAntiAlias = true alpha = alpha } // 總共下落的時(shí)間 var lastTime = 0L // 原始x坐標(biāo)位置 var originX = 0f /** * 根據(jù)自己的規(guī)則計(jì)算加速度,如果是勻速直接 return 0 */ abstract fun getAcceleration(): Float /** * 繪制自身,這里在Shape是非使用的時(shí)候進(jìn)行一些初始化操作 */ open fun draw(canvas: Canvas) { if (!isInUse) { lastTime += randomPre() initStyle() isInUse = true } else { drawWhenInUse(canvas) } } /** * Shape在使用的時(shí)候調(diào)用此方法 */ abstract fun drawWhenInUse(canvas: Canvas) /** * 初始化Shape風(fēng)格 */ open fun initStyle() { val random = Random() // 獲取隨機(jī)透明度 shapeAlpha = random.nextInt(155) + 50 // 獲得起點(diǎn)x偏移 val translateX = random.nextInt(10).toFloat() + 5 if (!isRandom) { start.x = translateX + originX end.x = translateX + originX } else { // 如果是隨機(jī)Shape,將x坐標(biāo)隨機(jī)范圍擴(kuò)大到整個(gè)屏幕的寬度 val randomWidth = random.nextInt(context.getScreenWidth()) start.x = randomWidth.toFloat() end.x = randomWidth.toFloat() } speed = randomSpeed(random) // 初始化length的工作留給之后對應(yīng)的子類去實(shí)現(xiàn) // 初始化color也留給子類去實(shí)現(xiàn) paint.apply { alpha = shapeAlpha strokeWidth = width isAntiAlias = true } // 如果有什么想要做的,剛好可以在追加上完成,就使用這個(gè)函數(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 /** * 獲取一個(gè)隨機(jī)的提前量,讓shape在豎屏上有一個(gè)初始的偏移 */ open fun randomPre(): Long { val random = Random() val pre = random.nextInt(1000).toLong() return pre } }
說起這個(gè)代碼,恩,還是經(jīng)歷過一番重構(gòu)的……周六去找同學(xué)玩的路上順便重構(gòu)了一下,將一些可以放到基類中的操作都抽取到了基類中。這樣雖然靈活不足,但是子類可以很方便的通過繼承實(shí)現(xiàn)一個(gè)需要類似功能的東西,就比如這里的下雨和下雪。順便吐槽一下……我注釋的風(fēng)格不太好,中英混搭……如果你仔細(xì)觀察,可以看到gif中的雨點(diǎn)或者雪花形態(tài)可能都有一些些的不一樣,是的,每一滴雨和雪花,都經(jīng)過了一些隨機(jī)的轉(zhuǎn)變。
里面比較重要的兩個(gè)屬性是isInUse和isRandom,本來想用一個(gè)容器來作為Shape的管理類,統(tǒng)一管理,但是這樣肯定會讓使用和復(fù)用的流程更加復(fù)雜。最后還是決定用簡單一點(diǎn)的方法,Shape內(nèi)部保存一個(gè)使用狀態(tài)和是否是隨機(jī)的。isRandoma表示這個(gè)Shape是否是隨機(jī)的,隨機(jī)在目前的代碼中會體現(xiàn)在Shape的x坐標(biāo)上。如果隨機(jī)標(biāo)識是true,那么x坐標(biāo)將是0 ~ ScreenWidth中的任意值。那么不是隨機(jī)的呢?在我的實(shí)現(xiàn)中,同一類Shape將會被分為兩類,一類常量組。會擁有相對固定的x值,但是也會有10~15px的隨機(jī)偏移。另一類就是隨機(jī)組,x值全屏自己隨機(jī),這樣就盡量讓屏幕各處都有雨滴(雪花)但會有疏密之別。initStyle就是這一隨機(jī)的過程,有興趣可以看看實(shí)現(xiàn)~
start和end是Shape的左上角點(diǎn)和右下角點(diǎn),如果你對于Cavans的api有了解,就應(yīng)該知道通過對start和end的轉(zhuǎn)換和計(jì)算,可以繪制出大部分的形狀。
接下來看一下具體實(shí)現(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) { /** * 圓心,用戶可以改變這個(gè)值 */ 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 { // 獲取隨機(jī)速度0.005 ~ 0.01 return (random.nextInt(5) + 5) / 1000f } override fun wtc(random: Random) { // 設(shè)置顏色漸變 val shader = RadialGradient(center.x, center.y, radius, Color.parseColor("#FFFFFF"), Color.parseColor("#D1D1D1"), Shader.TileMode.CLAMP) // 外部設(shè)置的起始點(diǎn)其實(shí)并不對,先計(jì)算出半徑 radius = random.nextInt(10) + 15f // 根據(jù)半徑計(jì)算start end end.x = start.x + radius end.y = start.y + radius // 計(jì)算圓心 calcCenter() paint.apply { setShader(shader) } } fun clear() { isInUse = false lastTime = 0 start.y = -radius * 2 end.y = 0f center = calcCenter() } }
這個(gè)類只要理解了圓心的計(jì)算和繪制,基本也就沒什么東西了。首先排除干擾項(xiàng),getAcceleration這玩意在設(shè)計(jì)之初是用來通過加速度計(jì)算路程的,后來發(fā)現(xiàn)……算了,還是勻速吧……于是都return 0f了。這里wtc()
函數(shù)和drawWhenInUse可能會看的你一臉懵逼,什么函數(shù)名,drawWhenInUse倒是見名知意,這wtc()
是什么玩意?這里wtc是相當(dāng)于一種追加初始化,完全狀態(tài)的函數(shù)名應(yīng)該是wantToChange()
。這些個(gè)函數(shù)調(diào)用流程是這樣的:
流程圖
其中draw(canvas)
是父類的方法,對供外部調(diào)用的方法,在isInUse標(biāo)識位為false時(shí)對Shape進(jìn)行初始化操作,具體的就是調(diào)用initStyle()
方法,而wtc()
則會在initStyle()
方法的最后調(diào)用。如果你有什么想要追加的初始化,可以通過這個(gè)函數(shù)實(shí)現(xiàn)。而drawWhenInUse(canvas)
方法則是需要實(shí)現(xiàn)動(dòng)態(tài)繪制的函數(shù)了。我這里就是在wtc()
函數(shù)中進(jìn)行了一些初始化操作,并且根據(jù)圓的特性重新計(jì)算了start、end和圓心。
接下來,就看看我們到底是怎么把這些充滿個(gè)性(口胡)的雪繪制到屏幕上:
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什么的先放一邊,先實(shí)現(xiàn)一個(gè) 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進(jìn)行了一些初始化操作并讓線程start了??匆幌戮€程中執(zhí)行的代碼,首先會判斷一個(gè)叫做canRun的標(biāo)識,這個(gè)標(biāo)識會在surface被創(chuàng)建的時(shí)候置為true,否則將會通過一個(gè)對象讓這個(gè)線程等待。而在surface被創(chuàng)建后,則會調(diào)用notify方法讓線程重新開始工作。之后是進(jìn)行繪制的工作,繪制前后會有一個(gè)計(jì)時(shí)的動(dòng)作,計(jì)算時(shí)間是否小于16ms,如果不足,則讓thread sleep 補(bǔ)足插值。因?yàn)?6ms一幀的繪制速度就足夠了,不需要繪制太快浪費(fèi)資源。
這里可以看到我創(chuàng)建了一個(gè)Java的Object對象,主要是因?yàn)镵otlin本身對于一些并發(fā)原語支持的并不好。Kotlin中任何對象都是繼承與Any,Any并沒有wait、notify等方法,所以這里用了黑科技……創(chuàng)建了Java對象……
代碼中關(guān)鍵代碼繪制調(diào)用了WeatherShapePool的drawRain(canvas)
方法,最后在看一下這個(gè)類:
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) } } }
這個(gè)類還是比較簡單的,只是一個(gè)單純的容器,至于叫Pool……因?yàn)閯傞_始自己想的是自己管理回收復(fù)用之類的,所以起了個(gè)名叫Pool,后來感覺這玩意好像不用實(shí)現(xiàn)的這么復(fù)雜……
感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享利用SurfaceView如何實(shí)現(xiàn)下雨與下雪動(dòng)畫的效果內(nèi)容對大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,遇到問題就找億速云,詳細(xì)的解決方法等著你來學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。