Maison > Article > interface Web > Utilisez SurfaceView pour obtenir des effets d'animation de pluie et de neige
Cet article vous présente principalement les informations pertinentes sur l'utilisation de SurfaceView pour obtenir des effets d'animation de pluie et de neige. Il nécessite des connaissances de base sur View et un exemple de code détaillé de Kotlin dans l'article pour votre référence. qui en ont besoin, étudiez ensemble.
Préface
Récemment j'envisage de faire une vague de choses pour consolider ce que j'ai appris récemment. Sans plus attendre, regardons le rendu final :
Raining.gif
C'est un peu paresseux ici... la deuxième photo est toujours il pleut... mais ce n'est pas le point clé...
Snow.gif
MP4 enregistré, converti en gif. Le premier gif a une fréquence d'images définie, il semble donc que la chute d'images puisse être grave, mais en fait ce n'est pas le cas, car ici j'ai également prêté attention au problème de dessiner 60 images en 1 seconde. La lecture de cet article nécessite des connaissances de base sur View et une syntaxe de base Kotlin. Pour être honnête, en termes de connaissances, cela n’a pas grand-chose à voir avec Kotlin, du moment que vous comprenez la syntaxe de base.
Clarifiez vos idées
Avant de passer à l'action, vous devez faire le tri dans vos idées et analyser quelle solution utiliser pour la mettre en œuvre dès le aspects suivants Cet effet :
Fil de travail : La première chose à penser est : cet effet de pluie doit être obtenu grâce à un dessin non-stop si vous effectuez cette opération sur le fil principal. , il est très probable que cela bloque le thread principal, provoquant un ANR ou un décalage anormal. Vous avez donc besoin d'une vue pouvant être dessinée dans un sous-thread. Il ne fait aucun doute que SurfaceView peut répondre à cette demande.
Comment mettre en œuvre : Analyser la mise en œuvre d'une goutte de pluie. Tout d’abord, des effets simples peuvent être remplacés par des lignes tracées. Tout le monde n'a pas le Sharingan. La vision dynamique est si bonne. Une fois qu'il bouge, qui sait si c'est une ligne ou une goutte de pluie... Bien sûr, il existe de nombreuses API pour dessiner sur Canvas, et ce n'est pas nécessairement le cas. manière. Ainsi, lors de la conception de la classe, nous définissons simplement la méthode draw afin qu'elle puisse être remplacée par les sous-classes. N'êtes-vous pas satisfait de mon implémentation ? Pas de problème, je vous donne la liberté de le changer ~
Réalisation de la localisation : Il existe deux façons de faire bouger les gouttes de pluie, l'une consiste à dessiner uniquement en fonction des coordonnées, et l'autre est pour utiliser l'animation de propriété, réécrivez vous-même l'estimateur et modifiez dynamiquement la valeur y. Au final, j'ai adopté la première solution. Pourquoi ai-je abandonné la seconde solution d'animation immobilière ? La raison en est que la méthode de dessin repose ici sur le déclenchement externe continu d'événements de dessin pour obtenir un dessin dynamique. Évidemment, la première méthode est plus conforme à la situation ici.
Ce qui précède sont quelques-unes de mes premières réflexions sur la mise en œuvre. Vient ensuite l'analyse de la mise en œuvre du code.
Analyse de l'implémentation du code
Mettez d'abord le diagramme de structure du code :
Code structure
WeatherShape est la classe parent de tous les temps Rain et Snow sont deux classes d'implémentation spécifiques.
Regardez le code de la classe parent :
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 } }
En parlant de ce code, eh bien, je suis quand même passé par beaucoup de problèmes Construit... Je l'ai refactorisé en allant jouer avec mes camarades de classe samedi, et j'ai extrait quelques opérations qui peuvent être mises dans la classe de base. Bien que cela ne soit pas assez flexible, les sous-classes peuvent facilement implémenter quelque chose qui nécessite des fonctions similaires via l'héritage, comme la pluie et la neige ici. Au fait, je veux me plaindre... Mon style d'annotation n'est pas très bon, un mélange de chinois et d'anglais... Si vous regardez bien, vous pouvez voir que la forme des gouttes de pluie ou des flocons de neige dans le gif peut être légèrement différent. Oui, chaque goutte de pluie et chaque flocon de neige ont subi des transformations aléatoires.
Les deux attributs les plus importants sont isInUse et isRandom. Au départ, je voulais utiliser un conteneur comme classe de gestion de Shape pour une gestion unifiée, mais cela rendra certainement le processus d'utilisation et de réutilisation plus compliqué. En fin de compte, j'ai décidé d'utiliser une méthode plus simple. Shape enregistre en interne un statut d'utilisation et s'il est aléatoire. isRandoma indique si cette forme est aléatoire. Le caractère aléatoire sera reflété dans la coordonnée x de la forme dans le code actuel. Si l'indicateur aléatoire est vrai, alors la coordonnée x sera n'importe quelle valeur comprise entre 0 et ScreenWidth. Ce n'est donc pas aléatoire ? Dans mon implémentation, le même type de Shape sera divisé en deux types, un type de groupe constant. Il aura une valeur x relativement fixe, mais il y aura également un décalage aléatoire de 10 ~ 15 px. L'autre type est un groupe aléatoire, où la valeur x est randomisée sur tout l'écran, de sorte qu'il y ait des gouttes de pluie (flocons de neige) partout sur l'écran mais il y aura des différences de densité. initStyle est ce processus aléatoire. Si vous êtes intéressé, vous pouvez jeter un œil à l'implémentation ~
le début et la fin sont le point du coin supérieur gauche et le point du coin inférieur droit de la forme si vous connaissez les Cavans. API, vous devez savoir que grâce à la conversion et au calcul de début et de fin, la plupart des formes peuvent être dessinées.
接下来看一下具体实现的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中文网!
相关推荐:
HTML5 Canvas渐进填充与透明实现图像的Mask效果
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!