이 글에서는 주로 SurfaceView를 사용하여 비와 눈 애니메이션 효과를 얻는 방법에 대해 소개합니다. 이 글에서는 몇 가지 기본 View 지식과 몇 가지 기본 Kotlin 구문이 필요하므로 참조하고 학습할 수 있도록 자세한 샘플 코드를 제공합니다. 같이 공부해주세요.
머리말
저는 최근에 배운 내용을 통합하기 위해 많은 일을 할 계획을 세웠습니다. 더 이상 고민하지 말고 최종 렌더링을 살펴보겠습니다.
Raining.gif
여기는 좀 게으릅니다... 두 번째 사진에는 아직 비가 내리고 있습니다... 하지만 이것이 핵심은 아닙니다. ..
Snowing.gif
mp4로 녹음하고 gif로 변환했습니다. 첫 번째 gif에는 프레임 레이트가 설정되어 있어서 프레임 드랍이 심각할 것 같지만 실제로는 그렇지 않습니다. 여기서도 1초에 60프레임을 그리는 문제에 주목했기 때문입니다. 이 문서를 읽으려면 몇 가지 기본 View 지식과 몇 가지 기본 Kotlin 구문이 필요합니다. 솔직히 말해서, 기본 구문을 이해하는 한 지식 측면에서는 Kotlin과 거의 관련이 없습니다.
생각을 정리하세요
행동을 취하기 전에 다음 측면에서 생각을 정리하고 이 효과를 달성하기 위해 어떤 솔루션을 사용해야 하는지 분석해야 합니다.
작업 스레드: 가장 먼저 생각해야 할 것 of is: 이 비 효과는 지속적인 그리기를 통해 달성되어야 합니다. 이 작업이 메인 스레드에서 수행되면 메인 스레드가 차단되어 ANR 또는 비정상적인 지연이 발생할 가능성이 있습니다. 따라서 하위 스레드에 그릴 수 있는 뷰가 필요합니다. SurfaceView가 이러한 요구를 충족할 수 있다는 것은 의심의 여지가 없습니다.
구현 방법: 빗방울 구현을 분석합니다. 우선 간단한 효과를 실제로 선을 그리는 것으로 대체할 수 있습니다. 모든 사람이 사륜안을 갖고 있는 것은 아닙니다. 일단 움직이면 그것이 선인지 빗방울인지 누가 알겠습니까? 물론 캔버스 그리기를 위한 많은 API가 있으며 반드시 이렇게 할 필요는 없습니다. 방법. 그래서 클래스를 디자인할 때 서브클래스에 의해 재정의될 수 있도록 그리기 메서드를 설정했습니다. 제 구현이 만족스럽지 않습니까? 문제 없습니다. 자유롭게 변경할 수 있습니다~
위치 구현: 빗방울을 움직이게 하는 두 가지 방법이 있습니다. 하나는 좌표에 따라 순수하게 그리는 것이고, 다른 하나는 속성 애니메이션을 사용하여 평가를 다시 작성하는 것입니다. y 값을 동적으로 변경하는 장치입니다. 결국 전자의 솔루션을 채택하게 되었습니다. 후자의 속성 애니메이션 솔루션을 포기한 이유는 무엇입니까? 그 이유는 여기의 그리기 방법이 동적 그리기를 달성하기 위해 그리기 이벤트의 외부 연속 트리거에 의존한다는 것입니다. 분명히 여기의 상황에 더 부합합니다.
위는 구현에 대한 초기 생각 중 일부입니다. 다음은 코드 구현 분석입니다.
코드 구현 분석
먼저 코드 구조 다이어그램을 넣습니다.
코드 구조
WeatherShape는 모든 날씨의 상위 클래스이고, 비와 눈은 두 가지 특정 구현 클래스입니다.
부모 클래스의 코드를 보세요:
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를 알고 있다면 알아야 합니다. 시작과 끝, 계산을 변환하면 대부분의 모양을 그릴 수 있습니다.
接下来看一下具体实现的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效果
위 내용은 SurfaceView를 사용하여 비와 눈 애니메이션 효과 얻기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!