Android13から使えるようになるAGSLを試してみた

Android13から使えるようになるAGSL(Android Graphics Shader Language)を試してみました。
Shaderの導入を検討している方、導入してみたいけど何をすればいいか悩んでいる方の助けになれば幸いです。

# 開発環境
- Macbook Pro OS Catalina 10.15.7
- Android Studio Electric Eel | 2022.1.1 Canary 8
- Kotlin
- Pixel6 Android13 β（エミュレータ） 

# 参考元
[公式](https://developer.android.com/guide/topics/graphics/agsl/using-agsl#kotlin)を参考に試しました。

# STEP1
いつもどおり新規にプロジェクトを作成します。
今回はJetpack Composeではなくxmlで作成しています。

# STEP2
カスタムViewを作成してactivity_main.xmlにサンプルとして320dp x 240dpの作成したカスタムViewを配置します。
公式ではonDrawForegroundメソッドの中でcanvasを使用して描画していたので用意しています。

```.kotlin
class ShaderView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
): View(context, attrs, defStyleAttr, defStyleRes) {
....
}
```

# STEP3
shaderで描画する際に必要な処理を追記します。

```.kotlin
    companion object {
        private const val COLOR_SHADER_SRC =
            """half4 main(float2 fragCoord) {
                return half4(1,0,0,1);
            }"""
    }

    private val fixedColorShader = RuntimeShader(COLOR_SHADER_SRC)
    private val paint = Paint().apply {
        shader = fixedColorShader
    }

    override fun onDrawForeground(canvas: Canvas?) {
        canvas?.let {
            canvas.drawPaint(paint) // fill the Canvas with the shader
        }
    }
```

この時点で実行すれば以下のように画面に真っ赤な四角が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287537/722933a9-60ea-5d34-92ed-2a83486955a8.png" width="320">

# STEP4
uniformを使って色のパラメータを渡す形で緑色を表示してみます。

```.kotlin
    companion object {
        private const val COLOR_SHADER_SRC =
            """layout(color) uniform half4 iColor;
               half4 main(float2 fragCoord) {
                    return iColor;
            }"""
    }

    private val fixedColorShader = RuntimeShader(COLOR_SHADER_SRC)
    private val paint = Paint().apply {
        fixedColorShader.setColorUniform("iColor", Color.GREEN )
        shader = fixedColorShader
    }

    override fun onDrawForeground(canvas: Canvas?) {
        canvas?.let {
            canvas.drawPaint(paint) // fill the Canvas with the shader
        }
    }
```

上記に書き換えたあとに実行すれば以下のように緑色の四角が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287537/dff60501-9c16-433a-b2d0-0e18061fae1a.png" width="320">

# STEP5
今度はグラデーションの表示を試します。

```.kotlin
    companion object {
        private const val COLOR_SHADER_SRC =
            """uniform float2 iResolution;
               half4 main(float2 fragCoord) {
                   float2 scaled = fragCoord/iResolution.xy;
                   return half4(scaled, 0, 1);
               }"""
    }

    private val fixedColorShader = RuntimeShader(COLOR_SHADER_SRC)
    private val paint = Paint().apply {
        shader = fixedColorShader
    }

    override fun onDrawForeground(canvas: Canvas?) {
        canvas?.let {
            fixedColorShader.setFloatUniform("iResolution", width.toFloat(), height.toFloat())
            canvas.drawPaint(paint)
        }
    }
```

実行すると以下のようにグラデーションで赤と緑が表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287537/b77c86f7-9a90-567c-c051-5b36eda1defc.png" width="320">

# STEP6
アニメーションをさせてみましょう。

```.kotlin
    companion object {
        private const val DURATION = 4000f
        private const val COLOR_SHADER_SRC = """
            uniform float2 iResolution;
            uniform float iTime;
            uniform float iDuration;
            half4 main(in float2 fragCoord) {
                float2 scaled = abs(1.0-mod(fragCoord/iResolution.xy+iTime/(iDuration/2.0),2.0));
                return half4(scaled, 0, 1.0);
            }
        """
    }

    private val animatedShader = RuntimeShader(COLOR_SHADER_SRC)
    private val paint = Paint().apply {
        shader = animatedShader
    }

    // declare the ValueAnimator
    private val shaderAnimator = ValueAnimator.ofFloat(0f, DURATION)

    init {
        // use it to animate the time uniform
        shaderAnimator.duration = DURATION.toLong()
        shaderAnimator.repeatCount = ValueAnimator.INFINITE
        shaderAnimator.repeatMode = ValueAnimator.RESTART
        shaderAnimator.interpolator = LinearInterpolator()

        animatedShader.setFloatUniform("iDuration", DURATION )
        shaderAnimator.addUpdateListener { animation ->
            animatedShader.setFloatUniform("iTime", animation.animatedValue as Float )
            invalidate()

        }
        shaderAnimator.start()
    }

    override fun onDrawForeground(canvas: Canvas?) {
        canvas?.let {
            animatedShader.setFloatUniform("iResolution", width.toFloat(), height.toFloat())
            canvas.drawPaint(paint)
        }
    }
```

実行すると以下のようにアニメーション表示されます。
### 公式のコードをそのまま記述するのでは実行時にアニメーションがされなかったため、addUpdateListener内でinvalidate()を呼んで強制的にViewを更新するようにしています。（正しい方法を御存知の方は教えていただけますと幸いです）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287537/172110be-171d-62df-f2a1-9591225fe432.gif" width="320">

# STEP7
文字を表示して色をアニメーションさせてみましょう。
STEP6との変更点は以下になります。
- Paintの生成時にtextSizeの指定を追加
- canvas.drawPaintをcanvas.drawTextへ
- xmlに定義しているカスタムViewのwidthとheightを画面いっぱいになるように変更

```.kotlin
    companion object {
        private const val DURATION = 4000f
        private const val COLOR_SHADER_SRC = """
            uniform float2 iResolution;
            uniform float iTime;
            uniform float iDuration;
            half4 main(in float2 fragCoord) {
                float2 scaled = abs(1.0-mod(fragCoord/iResolution.xy+iTime/(iDuration/2.0),2.0));
                return half4(scaled, 0, 1.0);
            }
        """
    }

    private val animatedShader = RuntimeShader(COLOR_SHADER_SRC)
    private val paint = Paint().apply {
        textSize = 260f
        shader = animatedShader
    }

    // declare the ValueAnimator
    private val shaderAnimator = ValueAnimator.ofFloat(0f, DURATION)

    init {
        // use it to animate the time uniform
        shaderAnimator.duration = DURATION.toLong()
        shaderAnimator.repeatCount = ValueAnimator.INFINITE
        shaderAnimator.repeatMode = ValueAnimator.RESTART
        shaderAnimator.interpolator = LinearInterpolator()

        animatedShader.setFloatUniform("iDuration", DURATION )
        shaderAnimator.addUpdateListener { animation ->
            animatedShader.setFloatUniform("iTime", animation.animatedValue as Float )
            invalidate()
        }
        shaderAnimator.start()
    }

    override fun onDrawForeground(canvas: Canvas?) {
        canvas?.let {
            animatedShader.setFloatUniform("iResolution", width.toFloat(), height.toFloat())
            canvas.drawText("Color", x / 2, y / 2, paint)
        }
    }
```

実行すると以下のようにアニメーションされます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287537/65863ea3-0f57-8ab4-2cde-31c3ce39d349.gif" width="320">

# STEP8
文字に回転アニメーションをさせてみましょう。
### 公式のコードをそのまま記述すると回転速度が速すぎたのでaddUpdateListener内で計算しているcamera.rotateの2番目の引数に / 100fを追加しています。

```.kotlin
    companion object {
        private const val ANIMATED_TEXT = "Color"
        private const val DURATION = 4000f
        private const val COLOR_SHADER_SRC = """
            uniform float2 iResolution;
            uniform float iTime;
            uniform float iDuration;
            half4 main(in float2 fragCoord) {
                float2 scaled = abs(1.0-mod(fragCoord/iResolution.xy+iTime/(iDuration/2.0),2.0));
                return half4(scaled, 0, 1.0);
            }
        """
    }

    private val animatedShader = RuntimeShader(COLOR_SHADER_SRC)
    private val paint = Paint().apply {
        textSize = 260f
        shader = animatedShader
    }
    private val camera = Camera()
    private val rotationMatrix = Matrix()
    private val bounds = Rect()

    // declare the ValueAnimator
    private val shaderAnimator = ValueAnimator.ofFloat(0f, DURATION)

    init {
        // use it to animate the time uniform
        shaderAnimator.duration = DURATION.toLong()
        shaderAnimator.repeatCount = ValueAnimator.INFINITE
        shaderAnimator.repeatMode = ValueAnimator.RESTART
        shaderAnimator.interpolator = LinearInterpolator()

        animatedShader.setFloatUniform("iDuration", DURATION )
        shaderAnimator.addUpdateListener { animation ->
            animatedShader.setFloatUniform("iTime", animation.animatedValue as Float )
            camera.rotate(0.0f, animation.animatedValue as Float / DURATION * 360f / 100f, 0.0f)
            invalidate()
        }
        shaderAnimator.start()
    }

    override fun onDrawForeground(canvas: Canvas?) {
        canvas?.let {
            animatedShader.setFloatUniform("iResolution", width.toFloat(), height.toFloat())

            camera.getMatrix(rotationMatrix)
            paint.getTextBounds(ANIMATED_TEXT, 0, ANIMATED_TEXT.length, bounds)

            val centerX = (bounds.width().toFloat()) / 2
            val centerY = (bounds.height().toFloat()) / 2

            rotationMatrix.preTranslate(-centerX, -centerY)
            rotationMatrix.postTranslate(centerX, centerY)

            canvas.save()
            canvas.concat(rotationMatrix)
            canvas.drawText(ANIMATED_TEXT, 0f, 0f + bounds.height(), paint)
            canvas.restore()
        }
    }
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287537/deeead6f-3e0d-ec0b-0a1e-c10ec40ef4b1.gif" width="320">

# AGSLを使ってみて
Shaderを使いこなすことができれば、ピンポイントで目立たせたいViewをよりリッチに表示することができるようになると思いますので色々と試していきたいと思います。

Shader久しぶりに触ってみてアニメーションさせるの楽し〜〜〜😄