做了一个星空背景的动态 Drawable – StarrySky
新项目需要做一个星空背景,顺便说下怎么做一个动态 Drawable
先看最终效果图:
我们的目标是一个叫 StarrySky
的 动态 Drawable
, 用法像这样:
imageView.setImageDrawable(starrySky) // or imageView.background = starrySky starrySky.start()
所以基础结构就是
class StarrySky: Drawable(), Animatable { /// xxx override fun draw(canvas: Canvas) override fun start() override fun stop() override fun isRunning() }
分析一下效果图,就是在 随机位置 加了很多点,然后这些点以 随机速度 往 随机方向 做匀速直线运动。
那我们所有需要的要素都在这里了:
- 随机位置
- 随机速度
- 随机方向
那我就定义一个类 保存这些要素 就好,
class Star( var x: Float, var y: Float, var speed: Int, // pixels per second var direction: Int // degree (0-360) )
因为是星星都动态的,所以要可以 计算下一帧 的位置,加一个 move
方法来计算。
class Star( var x: Float, var y: Float, var speed: Int, // pixels per second var direction: Int // degree (0-360) ) { fun move(delta: Int) { x += speed * delta / 1000f * cos(direction.toFloat()) y += speed * delta / 1000f * sin(direction.toFloat()) } }
然后给 StarrySky
加一个列表来 保存这些星星 , 为了避免 concurrent
异常,加上粗暴的同步锁
val stars = HashSet() private val LOCK = Any() fun addStar(star: Star) { synchronized(LOCK) { stars.add(star) } } fun removeStar(star: Star) { synchronized(LOCK) { stars.remove(star) } } fun copyStar(): HashSet { synchronized(LOCK) { val set = HashSet() set.addAll(stars) return set } }
画出来:
fun draw(canvas: Canvas) { canvas.drawColor(backgroundColor) val currentStars = copyStar() for (star in currentStars) { canvas.drawCircle(star.x, star.y, 2f, starPaint) } }
怎么让他们动起来呢?
方法很多, Timer
ValueAnimator
甚至手动 delay
都可以。我们的目标就是每过 16ms(每秒60帧) 能更新一下我们的位置。然后告诉 drawable,我位置更新了,你可以重新画一遍了。
我这里用了 Timer
fun start() { /// xxx timer.schedule(object : TimerTask() { override fun run() { val currentTime = System.currentTimeMillis() update((currentTime - lastTime).toInt()) lastTime = currentTime } }, 0, 16) } fun update(delta: Int) { // xxx // 这里要注意处理同步问题, 我就简写 for star in stars: star.move(delta) }
ok,新位置计算结束
告诉 drawable 重新绘制:
fun update(delta: Int) { // 计算新位置后 invalidateSelf() }
这样就能让星星在夜空中动起来了。
不过,我们想想,星空中不只有星星。还有月亮、太阳和超人。我们可以优化优化让它变得更通用。
做成通用型 Drawable
我们回去看看 星星模型 :
class Star( var x: Float, var y: Float, var speed: Int, // pixels per second var direction: Int // degree (0-360) ) { fun move(delta: Int) { x += speed * delta / 1000f * cos(direction.toFloat()) y += speed * delta / 1000f * sin(direction.toFloat()) } }
我们先给他重新命个名,叫 Model
吧
月亮、太阳、超人等等,这每类模型和星星一样的点在于,他们必然都有坐标,但是移动模式可能不一样。
所以我们可以把速度和方向提出来做抽象得到:
abstract class Model( var position: Point ) { abstract fun move(delta: Int) }
星星我们知道怎么画,就画个小圆就行了。可是如果你想要画月亮画太阳,或者画个大星星,那肯定就不能还是那样了。
所以 绘制 的部分也要抽象出来。
abstract class Model( var position: Point ) { abstract fun move(delta: Int) abstract fun draw(canvas: Canvas) }
星星就变成了
class Star(position: Point, val speed: Int, val direction: Int, paint: Paint): Model(position) { fun move(delta: Int) { position.x += speed * delta / 1000f * cos(direction.toFloat()) position.y += speed * delta / 1000f * sin(direction.toFloat()) } fun draw(canvas: Canvas) { canvas.drawCircle(position.x, position.y, 2f, paint) } }
现在整个星空 StarrySky
看起来像这样了:
class StarrySky: Drawable(), Animatable { /// xxx val models = HashSet() // 开始动画 override fun start() { // 每 16s 更新 timer.schedule(object : TimerTask() { override fun run() { val currentTime = System.currentTimeMillis() update((currentTime - lastTime).toInt()) lastTime = currentTime // 重绘 invalidateSelf() } }, 0, 16) } override fun stop() override fun isRunning() // 计算新位置 fun update(delta: Int) { models.forEach { it.move(delta) } } // 绘制模型 override fun draw(canvas: Canvas) { models.forEach { it.draw(canvas) } } }
当然,这里都是伪代码。你实际写代码还要注意更多的细节,比如 Set
的同步问题,物体移动出范围后如何处理的问题。
当这些问题你都处理好了,美丽的星空就从你的手中诞生了。