Heim > Artikel > Web-Frontend > Verwenden Sie SurfaceView, um Regen- und Schneeanimationseffekte zu erzielen
Dieser Artikel führt Sie hauptsächlich in die relevanten Informationen zur Verwendung von SurfaceView zum Erzielen von Regen- und Schneeanimationseffekten ein. Der Artikel enthält einige grundlegende View-Kenntnisse und einige grundlegende Kotlin-Syntax Wer es braucht, lernt gemeinsam.
Vorwort
Vor kurzem habe ich vor, eine Welle von Dingen zu machen, um das zu festigen, was ich kürzlich gelernt habe. Werfen wir ohne weitere Umschweife einen Blick auf das endgültige Rendering:
Raining.gif
Hier ist es etwas faul... das zweite Bild ist still Es regnet...aber das ist nicht der entscheidende Punkt...
Snow.gif
Aufgenommen mp4, konvertiert in GIF. Im ersten GIF ist die Bildrate festgelegt, daher scheint der Bildabfall schwerwiegend zu sein, ist dies jedoch nicht, da ich hier auch auf das Problem des Zeichnens von 60 Bildern in 1 Sekunde geachtet habe. Das Lesen dieses Artikels erfordert einige grundlegende View-Kenntnisse und einige grundlegende Kotlin-Syntax. Ehrlich gesagt hat es vom Wissen her wenig mit Kotlin zu tun, solange man die grundlegende Syntax versteht.
Klaren Sie Ihre Ideen
Bevor Sie Maßnahmen ergreifen, müssen Sie Ihre Ideen sortieren und analysieren, welche Lösung verwendet werden soll, um sie umzusetzen Folgende Aspekte Dieser Effekt:
Arbeitsthread: Das erste, woran Sie denken sollten, ist: Dieser Regeneffekt muss durch ununterbrochenes Zeichnen erreicht werden, wenn Sie diesen Vorgang im Hauptthread ausführen , ist es sehr wahrscheinlich, dass der Hauptthread blockiert wird, was zu ANR oder abnormaler Verzögerung führt. Sie benötigen also eine Ansicht, die in einem Unterthread gezeichnet werden kann. Es besteht kein Zweifel, dass SurfaceView diese Anforderung erfüllen kann.
So implementieren Sie: Analysieren Sie die Implementierung eines Regentropfens. Erstens können einfache Effekte tatsächlich durch das Zeichnen von Linien ersetzt werden. Nicht jeder hat das Sharingan. Wenn es sich einmal bewegt, wer weiß, ob es eine Linie oder ein Regentropfen ist ... Natürlich gibt es viele APIs für das Canvas-Zeichnen, und es muss nicht unbedingt in gemacht werden auf diese Weise erreichen. Beim Entwerfen der Klasse legen wir die Zeichenmethode einfach so fest, dass sie von Unterklassen überschrieben werden kann. Sind Sie mit meiner Implementierung nicht zufrieden? Kein Problem, ich gebe Ihnen die Freiheit, es zu ändern~
Erkenntnis des Aufenthaltsorts: Es gibt zwei Möglichkeiten, Regentropfen in Bewegung zu setzen: Eine besteht darin, rein nach Koordinaten zu zeichnen, und die andere darin Um die Eigenschaftsanimation zu verwenden, schreiben Sie den Schätzer selbst neu und ändern Sie den y-Wert dynamisch. Am Ende habe ich die erstere Lösung übernommen. Warum habe ich die letztere Eigenschaftsanimationslösung aufgegeben? Der Grund dafür ist, dass die Zeichenmethode hier auf der externen kontinuierlichen Auslösung von Zeichenereignissen beruht, um ein dynamisches Zeichnen zu erreichen. Offensichtlich entspricht die erste Methode eher der Situation hier.
Das Obige sind einige meiner ersten Gedanken zur Implementierung. Als nächstes folgt die Code-Implementierungsanalyse.
Code-Implementierungsanalyse
Fügen Sie zunächst das Codestrukturdiagramm ein:
Code Struktur
WeatherShape ist die übergeordnete Klasse von All Weather. Rain und Snow sind zwei spezifische Implementierungsklassen.
Sehen Sie sich den Code der übergeordneten Klasse an:
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 } }
Apropos Code: Nun, er wurde einer Umgestaltung unterzogen ...Ich habe es auf dem Weg zum Spielen am Samstag mit meinen Klassenkameraden umgestaltet und einige Operationen extrahiert, die in die Basisklasse eingefügt werden können. Obwohl dies nicht flexibel genug ist, können Unterklassen durch Vererbung leicht etwas implementieren, das ähnliche Funktionen erfordert, wie hier Regen und Schnee. Übrigens möchte ich mich beschweren ... Mein Anmerkungsstil ist nicht sehr gut, eine Mischung aus Chinesisch und Englisch ... Wenn Sie genau hinschauen, können Sie erkennen, dass die Form der Regentropfen oder Schneeflocken im GIF möglicherweise leicht abweichen kann Ja, jeder Regentropfen und jede Schneeflocke haben einige zufällige Veränderungen erfahren.
Die beiden wichtigeren Attribute sind isInUse und isRandom. Ursprünglich wollte ich einen Container als Verwaltungsklasse von Shape für eine einheitliche Verwaltung verwenden, aber das wird den Verwendungs- und Wiederverwendungsprozess definitiv komplizierter machen. Letztendlich habe ich mich für eine einfachere Methode entschieden. Shape speichert intern einen Nutzungsstatus und ob dieser zufällig ist. isRandoma gibt an, ob diese Form zufällig ist und sich in der x-Koordinate der Form im aktuellen Code widerspiegelt. Wenn das Zufallsflag wahr ist, ist die x-Koordinate ein beliebiger Wert zwischen 0 und ScreenWidth. Es ist also kein Zufall? In meiner Implementierung wird derselbe Formtyp in zwei Typen unterteilt, einen Typ einer konstanten Gruppe. Es wird einen relativ festen x-Wert haben, aber es wird auch einen zufälligen Versatz von 10 bis 15 Pixel geben. Der andere Typ ist eine Zufallsgruppe, bei der der x-Wert zufällig über den gesamten Bildschirm verteilt wird, sodass überall auf dem Bildschirm Regentropfen (Schneeflocken) zu sehen sind, es jedoch Unterschiede in der Dichte gibt. initStyle ist dieser zufällige Prozess. Wenn Sie die API kennen, können Sie sich die Implementierung ansehen.
Start und Ende sind der obere linke Eckpunkt und der untere rechte Eckpunkt Von Cavans sollten Sie wissen, dass durch die Anfangs- und Endkonvertierung und -berechnung die meisten Formen gezeichnet werden können.
接下来看一下具体实现的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效果
Das obige ist der detaillierte Inhalt vonVerwenden Sie SurfaceView, um Regen- und Schneeanimationseffekte zu erzielen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!