GIFアニメーションをImageViewで表示する

# はじめに
需要があるかはわからないですが、Gifアニメをこの度自プロジェクトでAndroidアプリ上で動かしたいという要望があったので調べました。

# 調べてみた
まずは、「Gif animation android imageview」などでググってみました。
すると・・・glideというライブラリを使うといいよ！
という記事がたくさん出てきました。

https://qiita.com/maehara08/items/eef63804c52b2e0bb404

それ以外だと、Movieクラスを使ってCanvasに一枚ずつ書き出して・・・みたいなゴリ押しパターンがありました。
https://tomorrowkey-2.hatenadiary.org/entry/20100419/1271678022

弊社では、ライブラリを使うにはちょっと時間がかかるのと、外部の信用できる会社のものでないと結構たいへんなので、glideライブラリを使うというのは避けたかったので、ゴリ押しパターンを実装してみようということになりました。

# 準備
## Gifアニメのダウンロード
とりあえず、フリー素材のGifアニメをダウンロード。
下記のサイトからもらいました。ありがとう。
https://sozai-good.com/illust/gifanimation/

## assetsフォルダの作成
とりあえず表示するだけなので、assetsにダウンロードしたGifアニメを置くことに。

projectツリーのappあたりで右クリックして
[New]->[Folder]->[Assets Folder]をすると簡単につくれます。
<img width="300" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151973/8f46beb0-bc09-066a-9da2-26dc7d0e900e.png">

assetsフォルダがapp->src->mainの下にできていたら、ダウンロードしたGifアニメを入れます。
Gifアニメのファイル名は適当に。
<img width="300" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151973/9e2607e2-e894-d5f7-d647-e5bbdc9a6d05.png">

## レイアウトファイル
今回は、Activityの画面にImageViewを配置するだけにします。

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <ImageView
            android:id="@+id/image_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

# Gifアニメを表示させる
Gifアニメを表示する場合、`Movie`がversion28から非推奨になっているため、
コードをOSごとに分ける必要があります。

## Android P以上の場合
リファレンス通りに、`Movie`はdeprecatedなので`AnimatedImageDrawable`
[Movieのリファレンス](https://developer.android.com/reference/android/graphics/Movie)
[AnimatedImageDrawableのリファレンス](https://developer.android.com/reference/android/graphics/drawable/AnimatedImageDrawable)

AnimatedImageDrawableを、assetsからGifアニメを取得して表示します。

```kotlin
@RequiresApi(Build.VERSION_CODES.P)
    private fun getGifAnimationDrawable(): AnimatedImageDrawable {
        val source = ImageDecoder.createSource(assets, "gif_anim_toy_poodle.gif")
        return ImageDecoder.decodeDrawable(source) as? AnimatedImageDrawable
            ?: throw ClassCastException()
    }
```

手順は簡単です。
1. `ImageDecoder.createSource`で、第一引数にassetManager、第二引数にassetsに入っているファイル名を指定してください。
ちなみに、この`ImageDecoder.createSource`は他にも`File`を指定したり、`res & resID`を指定したり、`ContentResolver & URI`など、複数のファイル取得に対応していそうです。
2. 1で取得した`ImageDecoder.Source`インスタンスを`ImageDecoder.decodeDrawable()`に渡してDrawableを取得します。
3. `AnimatedImageDrawable`にキャストします。

取得ができたら、ImageViewに設定します。

```kotlin
// AnimatedImageDrawableを取得する
val drawable = getGifAnimationDrawable() 

// アニメーションをセットする
binding.imageView.setImageDrawable(drawable)

// アニメーションを開始する
drawable.start()
```
## Android P未満の場合
Android P未満の場合だと、`ImageDecoder`と`AnimatedImageDrawable`が利用できません。
なので、従来の通りMovieクラスを使った方法を一応書いておきます。

参考にしたサイトはこちらで、基本的にこちらの写経です。
https://end0tknr.hateblo.jp/entry/20120714/1342247240

### カスタムのDrawableを用意する
MovieクラスでのGifアニメーション起動の方法は、onDrawでMovieを動かす方法になるので、
カスタムクラスを作る必要があります。
参考にしたサイトではViewを継承していましたが、今回はImageViewに設定したかったので、Drawableを継承しました。

```CustomAnimatedDrawable.kt
class CustomAnimatedDrawable(
    private val inputStream: InputStream
): Drawable() {
    private val _movie by lazy {
        Movie.decodeStream(inputStream)
    }
    
    private var _movieStart = 0
    private var _loop = true
    private var _stop = false
    private var relativeMillisecond = 0

    override fun draw(canvas: Canvas) {
        canvas.apply {
            drawColor(Color.TRANSPARENT)
            scale(width / _movie.width().toFloat(),
                height / _movie.height().toFloat() )
        }
        val now = SystemClock.uptimeMillis()

        if (_movieStart == 0) {
            _movieStart = now.toInt()
        }

        relativeMillisecond = when {
            _stop -> {
                _movieStart = 0
                relativeMillisecond
            }
            _loop -> ((now - _movieStart) % _movie.duration()).toInt()
            else -> (now - _movieStart).toInt()
        }

        _movie.apply {
            setTime(relativeMillisecond)
            draw(canvas, 0f, 0f)
        }
        
        invalidateSelf()
    }

    override fun setAlpha(alpha: Int) {
    }

    override fun getOpacity(): Int = PixelFormat.UNKNOWN

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    fun stop() {
        _stop = true
    }
    
    fun start() {
        _stop = false
        _movieStart = 0
    }
    
    fun isRunning() = !_stop
}
```

### assetsからファイルをopenする
あとはassetsからファイルを取得して、カスタムクラスにinputStreamを渡してあげればOKです。

```kotlin
    private fun getGifAnimationDrawableLessThanP(): CustomAnimatedDrawable {
        val inputStream = assets.open(ANIMATION_GIF_FILE_NAME)
        return CustomAnimatedDrawable(inputStream)
    }
```

これでOKです。

# キャプチャ
recordで動画を撮ったんですが、いつからかわからないですが、.webm方式での保存になってしまい、かつadb shell screen record..みたいなのもうまく動かないので、泣く泣くキャプチャを取るしかできませんでした。
<img width="300" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151973/6846bc78-4b49-f317-c84a-8749e7851700.png">
この子がペケペケ動きます。

# コード
サンプルのコードはここにおいておきました。
なんかpushの仕方間違えちゃったので一階層無駄があります。
https://github.com/keikyukyun/gif_animation_sample 

# 最後に
久しぶりにgitにもアップしたけど、WebViewなら一撃という噂も。