首頁 >web前端 >H5教程 >利用SurfaceView實現下雨與下雪動畫的效果

利用SurfaceView實現下雨與下雪動畫的效果

不言
不言原創
2018-06-22 15:01:423181瀏覽

這篇文章主要為大家介紹了關於利用SurfaceView實現下雨與下雪動畫效果的相關資料,需要一些基本的View知識和會一些基礎Kotlin語法,文中給出了詳細的示例代碼供大家參考學習,需要的朋友們一起學習學習吧。

前言

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


下雨.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) + 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
  // 根据半径计算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这玩意在设计之初是用来通过加速度计算路程的,后来发现……算了,还是匀速吧……于是都return 0f了。这里wtc()函数和drawWhenInUse可能会看的你一脸懵逼,什么函数名,drawWhenInUse倒是见名知意,这wtc()是什么玩意?这里wtc是相当于一种追加初始化,完全状态的函数名应该是wantToChange() 。这些个函数调用流程是这样的:


流程图

其中draw(canvas)是父类的方法,对供外部调用的方法,在isInUse标识位为false时对Shape进行初始化操作,具体的就是调用initStyle()方法,而wtc()则会在initStyle()方法的最后调用。如果你有什么想要追加的初始化,可以通过这个函数实现。而drawWhenInUse(canvas)方法则是需要实现动态绘制的函数了。我这里就是在wtc()函数中进行了一些初始化操作,并且根据圆的特性重新计算了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)

 // 低级并发,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发生了变化
//  canRun = true

 }

 override fun surfaceDestroyed(holder: SurfaceHolder?) {
  // 在这里释放资源
  canRun = false
  LogUtil.i(TAG, "surfaceDestroyed")
 }

 override fun surfaceCreated(holder: SurfaceHolder?) {
  threadQuit = false
  canRun = true
  try {
   // 如果没有执行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什么的先放一边,先实现一个
  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了。看一下线程中执行的代码,首先会判断一个叫做canRun的标识,这个标识会在surface被创建的时候置为true,否则将会通过一个对象让这个线程等待。而在surface被创建后,则会调用notify方法让线程重新开始工作。之后是进行绘制的工作,绘制前后会有一个计时的动作,计算时间是否小于16ms,如果不足,则让thread sleep 补足插值。因为16ms一帧的绘制速度就足够了,不需要绘制太快浪费资源。

这里可以看到我创建了一个Java的Object对象,主要是因为Kotlin本身对于一些并发原语支持的并不好。Kotlin中任何对象都是继承与Any,Any并没有wait、notify等方法,所以这里用了黑科技……创建了Java对象……

代码中关键代码绘制调用了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,后来感觉这玩意好像不用实现的这么复杂……

总之,这玩意,会者不难,我的代码也非尽善尽美,如果我有任何纰漏或者你有什么好的意见,都可以提出,邮件或者是在文章下评论最佳。

以上就是本文的全部内容,希望对大家的学习有所帮助,更多相关内容请关注PHP中文网!

相关推荐:

如何通过Canvas及File API缩放并上传图片

HTML5 Canvas渐进填充与透明实现图像的Mask效果

以上是利用SurfaceView實現下雨與下雪動畫的效果的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn