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


はじめに

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

調べてみた

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

それ以外だと、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]をすると簡単につくれます。

assetsフォルダがapp->src->mainの下にできていたら、ダウンロードしたGifアニメを入れます。
Gifアニメのファイル名は適当に。

レイアウトファイル

今回は、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のリファレンス
AnimatedImageDrawableのリファレンス

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

@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に設定します。

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

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

// アニメーションを開始する
drawable.start()

Android P未満の場合

Android P未満の場合だと、ImageDecoderAnimatedImageDrawableが利用できません。
なので、従来の通り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です。

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

これでOKです。

キャプチャ

recordで動画を撮ったんですが、いつからかわからないですが、.webm方式での保存になってしまい、かつadb shell screen record..みたいなのもうまく動かないので、泣く泣くキャプチャを取るしかできませんでした。

この子がペケペケ動きます。

コード

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

最後に

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