できたもの
コード
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import java.util.*
import kotlin.math.max
class KnightRiderView(context: Context, attrs: AttributeSet) : View(context, attrs) {
enum class Pattern {
OneWay,
TwoWay
}
companion object {
private val PATTERN = Pattern.TwoWay
private const val COLOR = Color.GREEN
private const val BLOCK_NUM = 100 // ブロック数
private const val MOVE_SPEED = 1f // 移動速度(左端から右端に何秒かけて移動するか)
private const val ATTENUATION_TIME = 0.5f // 減衰時間(光った後に何秒かけて消灯するか)
}
private val paint = Paint()
private val blocks = Array(BLOCK_NUM) { 0f }
private var brightIndex = 0 // 現在光っているブロックのインデックス
private var moveDir = 1 // 光の移動方向(1で右、-1で左)
private var lastDrawTime : Long? = null // 前回描画時間
private val timer = Timer()
init {
paint.color = COLOR
paint.style = Paint.Style.FILL
// 指定秒ごとに光を移動させる
timer.schedule(object : TimerTask() {
override fun run() {
move()
}
}, 0L, (1000 * MOVE_SPEED / BLOCK_NUM).toLong())
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
timer.cancel()
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val deltaTime = if (lastDrawTime != null) (System.currentTimeMillis() - lastDrawTime!!).toFloat() / 1000f else 0.01f
for (i in blocks.indices) {
// 指定秒で光を減衰させる
blocks[i] = max(0f, blocks[i] - (deltaTime / ATTENUATION_TIME))
// ブロックを描画する
paint.alpha = (blocks[i] * 255).toInt()
canvas?.drawRect(getBlockRect(i), paint)
}
lastDrawTime = System.currentTimeMillis()
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
when (MeasureSpec.getMode(heightMeasureSpec)) {
MeasureSpec.EXACTLY -> setMeasuredDimension(width, measuredHeight)
else -> setMeasuredDimension(width, width / BLOCK_NUM)
}
}
private fun getBlockRect(index: Int): RectF {
val blockWidth = width.toFloat() / BLOCK_NUM
return RectF(index * blockWidth, 0f, (index + 1) * blockWidth, height.toFloat())
}
private fun move() {
when (PATTERN) {
Pattern.OneWay -> moveOneWay()
Pattern.TwoWay -> moveTwoWay()
}
}
private fun moveOneWay() {
// 端まで到達したら折り返す
if ((moveDir == 1 && brightIndex == blocks.size - 1) || (moveDir == -1 && brightIndex == 0))
moveDir *= -1
// 光をひとつ横のブロックに移動させる
brightIndex += moveDir
blocks[brightIndex] = 1f
}
private fun moveTwoWay() {
// 左端⇔中心に到達したら折り返す
if ((moveDir == 1 && brightIndex == (blocks.size / 2) - 1) || (moveDir == -1 && brightIndex == 0))
moveDir *= -1
// 光をひとつ横のブロックに移動させる
brightIndex += moveDir
// 中心から対称の位置にある2つのブロックを光らせる
blocks[brightIndex] = 1f
blocks[(blocks.size - 1) - brightIndex] = 1f
}
}
レイアウトxmlにこのビューを置けばそのままそれだけで使えます。
layout_heightはwrap_contentにすると1ブロックが正方形になるように大きさ設定されます。dp指定で任意の高さに設定してもOKです。
冒頭のGifはPatternでTwoWayを指定した時のアニメーションで、OneWayを選択すると1つの光が左右に動くアニメーションになります。
BROCK_NUMを変更して粗さを変えたり、MOVE_SPEEDで動きの速度を変更できます。
ATTENUATION_TIMEを長くすると、光の尾が長くなります。
おわりに
Canvasを使えば自由にアニメーションが使えて楽しいですね!
是非ナイトライダー風アニメーション使ってみてください。