国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統(tǒng) > Android > 正文

利用SurfaceView實現(xiàn)下雨與下雪動畫效果詳解(Kotlin語法)

2019-12-12 02:02:15
字體:
供稿:網(wǎng)友

前言

最近打算做一波東西鞏固一下自己近期所學所得。話不多說,先看一下最終完成的效果圖:


下雨.gif

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

下雪.gif

錄制的mp4,轉(zhuǎn)成了gif。第一個gif設置了幀率,所以看起來可能掉幀比較嚴重,但是實際上并不會,因為這里我也注意了1s要繪制60幀的問題。閱讀本文需要一些基本的View知識和會一些基礎Kotlin語法。說實話,就知識點來說,跟Kotlin是沒多大關系的,只要懂基本的語法就可以了。

理清思路

在動手前先要理一下思路,從以下幾個方面來分析一下該采用什么方案來實現(xiàn)這個效果:

  • 工作線程:首先要想到的是:這個下雨的效果需要通過不停的繪制來實現(xiàn),如果在主線程做這個操作,很有可能會阻塞主線程,導致ANR或者異常卡頓。所以需要一個能在子線程進行繪制的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.weatherimport android.graphics.Canvasimport android.graphics.Paintimport android.graphics.PointFimport com.xiasuhuei321.gank_kotlin.contextimport com.xiasuhuei321.gank_kotlin.extension.getScreenWidthimport 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.weatherimport android.graphics.*import com.xiasuhuei321.gank_kotlin.contextimport com.xiasuhuei321.gank_kotlin.extension.getScreenHeightimport 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.weatherimport android.content.Contextimport android.graphics.Canvasimport android.graphics.Colorimport android.graphics.PixelFormatimport android.graphics.PorterDuffimport android.util.AttributeSetimport android.view.SurfaceHolderimport android.view.SurfaceViewimport com.xiasuhuei321.gank_kotlin.extension.LogUtilimport 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.weatherimport android.graphics.Canvasimport android.graphics.PointFimport com.xiasuhuei321.gank_kotlin.contextimport 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.VeVB.COm/201709/yuanma/GankKotlin(VeVB.COm).rar

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網(wǎng)的支持。

發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 徐水县| 美姑县| 左权县| 都匀市| 金秀| 白水县| 临湘市| 潼关县| 呼伦贝尔市| 宜章县| 平谷区| 广东省| 安塞县| 安阳县| 濉溪县| 金阳县| 汉源县| 平山县| 克拉玛依市| 大关县| 察隅县| 原阳县| 朝阳市| 同德县| 仙桃市| 新竹市| 新化县| 木兰县| 云安县| 手机| 哈尔滨市| 上犹县| 清水河县| 博罗县| 海丰县| 福清市| 泽州县| 灵武市| 常宁市| 侯马市| 安图县|