首页 > web前端 > H5教程 > 利用SurfaceView实现下雨与下雪动画的效果

利用SurfaceView实现下雨与下雪动画的效果

不言
发布: 2018-06-22 15:01:42
原创
3337 人浏览过

这篇文章主要给大家介绍了关于利用SurfaceView实现下雨与下雪动画效果的相关资料,需要一些基本的View知识和会一些基础Kotlin语法,文中给出了详细的示例代码供大家参考学习,需要的朋友们一起学习学习吧。

前言

最近打算做一波东西巩固一下自己近期所学所得。话不多说,先看一下最终完成的效果图:


下雨.gif

这里比较懒……第二个图片中还是降雨……不过这不是关键点……

下雪.gif

录制的mp4,转成了gif。第一个gif设置了帧率,所以看起来可能掉帧比较严重,但是实际上并不会,因为这里我也注意了1s要绘制60帧的问题。阅读本文需要一些基本的View知识和会一些基础Kotlin语法。说实话,就知识点来说,跟Kotlin是没多大关系的,只要懂基本的语法就可以了。

理清思路

在动手前先要理一下思路,从以下几个方面来分析一下该采用什么方案来实现这个效果:

  • 工作线程:首先要想到的是:这个下雨的效果需要通过不停的绘制来实现,如果在主线程做这个操作,很有可能会阻塞主线程,导致ANR或者异常卡顿。所以需要一个能在子线程进行绘制的View,毫无疑问SurfaceView可以满足这个需求。

  • 如何实现:分析一下一颗雨滴的实现。首先,简单的效果其实可以用画线的方式代替。并不是每个人都有写轮眼,动态视力那么好的,一旦动起来谁还知道他是条线还是雨滴……当然了,Canvas绘制的API有很多,并不一定非要用这种方式来实现。所以在在设计类的时候我们将draw的方法设置成可以让子类复写就可以了,你不满意我的实现?没问题,我给你改的自由~

  • 下落的实现:让雨滴动起来,有两种方式,一种是纯按坐标来绘制,另外一种是利用属性动画,自己重写估值器,动态改变y值。最终我还是采用了前一种方案,后一种属性动画的方案我为什么放弃了呢?原因是:这里的绘制的方式是靠外部不断的触发绘制事件来实现动态绘制的,很显然第一种方式更加符合这里的情况。

以上就是我初期的一些关于实现的思考了,接下来是代码实现分析。

代码实现分析

先放代码结构图:

代码结构

WeatherShape所有天气的父类,Rain和Snow是两个具体实现类。

看一下父类的代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

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类:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

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和圆心。

接下来,就看看我们到底是怎么把这些充满个性(口胡)的雪绘制到屏幕上:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

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)方法,最后在看一下这个类:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

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
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板