Androidでナイトライダー風アニメーション

# できたもの

URL読み取りアプリのローディング表示に使ってみました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130234/b14dd937-c4d4-86d9-f2cd-fcd54f0ac22d.gif" width="400">

# コード
```kotlin
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を使えば自由にアニメーションが使えて楽しいですね！
是非ナイトライダー風アニメーション使ってみてください。