みんからきりまで

きりみんです。

Kotlinの練習にオセロ作って遊んだ

AndroidのリハビリとKotlinでのリスト操作の練習のために雑なオセロゲームを作った。

https://i.gyazo.com/9d889a7725433631c384fe93e6fffb4e.gif

アルゴリズムは適当だけどKotlinの練習なので出来るだけfor文やvarを使わず関数型プログラミングっぽい感じで書いてみた。Kotlinだとリスト操作がめっちゃやりやすくてたのしい。
そこそこきれいに書けた気がするけど、特に結果をキャッシュして使いまわしたりせず同じ計算処理を何度も行ったりしてるのでパフォーマンス的には微妙そう。

class OseroGame() {

    val BOARD_SIZE = 8
    // 初期配置石
    private val CENTER_LEFT_UP = Place(BOARD_SIZE / 2 - 1, BOARD_SIZE / 2 - 1, Stone.BLACK)
    private val CENTER_LEFT_UNDER = Place(BOARD_SIZE / 2 - 1, BOARD_SIZE / 2, Stone.WHITE)
    private val CENTER_RIGHT_UP = Place(BOARD_SIZE / 2, BOARD_SIZE / 2 - 1, Stone.WHITE)
    private val CENTER_RIGHT_UNDER = Place(BOARD_SIZE / 2, BOARD_SIZE / 2, Stone.BLACK)

    /** 盤の状態を2次元配列で保持 */
    val boardStatus = arrayOfNulls<List<Place>>(BOARD_SIZE).mapIndexed { x, list -> arrayOfNulls<Place>(BOARD_SIZE).mapIndexed { y, place -> Place(x, y, Stone.NONE) } }

    fun getInitialPlaces() = listOf(CENTER_LEFT_UP, CENTER_LEFT_UNDER, CENTER_RIGHT_UP, CENTER_RIGHT_UNDER)

    /** 指定された場所に石を置けるか */
    fun canPut(place: Place) = boardStatus[place.x][place.y].stone == Stone.NONE && getCanChangePlaces(place).isNotEmpty()

    /** 石を置ける全ての場所 */
    fun getAllCanPutPlaces(color: Stone) = boardStatus.flatMap { it }.filter { canPut(Place(it.x, it.y, color)) }

    /** 次のターンで置ける場所がまだ存在するか */
    fun canNext(color: Stone): Boolean = getAllCanPutPlaces(color).isNotEmpty()

    /** 石の数を数える */
    fun countStones(color: Stone) = boardStatus.flatMap { it }.count { it.stone == color }

    /** ゲームが終了しているか */
    fun isGameOver() = !canNext(Stone.BLACK) && !canNext(Stone.WHITE)

    /** ひっくり返せる石のリストを取得 */
    fun getCanChangePlaces(target: Place): List<Place> {
        return searchChangePlacesRight(target)
                .plus(searchChangePlacesLeft(target))
                .plus(searchChangePlacesUp(target))
                .plus(searchChangePlacesUnder(target))
                .plus(searchChangePlacesUpperLeft(target))
                .plus(searchChangePlacesDownRight(target))
                .plus(searchChangePlacesUpperRight(target))
                .plus(searchChangePlacesDownLeft(target))
    }

    /** 置いた石から右方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesRight(target: Place): List<Place> {
        if (target.x + 1 > BOARD_SIZE - 1) return emptyList()

        val rightPlaces = boardStatus.drop(target.x + 1) // targetより右の列だけ抽出
                .map { it[target.y] } // targetの行だけ取り出す
        return getInsidePlaces(target, rightPlaces)
    }

    /** 置いた石から左方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesLeft(target: Place): List<Place> {
        if (target.x == 0) return emptyList()

        val leftPlaces = boardStatus
                .take(target.x) // targetより左の列だけを抽出
                .map { it[target.y] } // targetの行だけ取り出す
                .reversed() // 左方向だと辿りにくいのでリスト反転
        return getInsidePlaces(target, leftPlaces)
    }

    /** 置いた石から下方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesUnder(target: Place): List<Place> {
        if (target.y + 1 > BOARD_SIZE - 1) return emptyList()

        val underPlaces = boardStatus[target.x].drop(target.y + 1) // targetより下の行だけ抽出
        return getInsidePlaces(target, underPlaces)
    }

    /** 置いた石から上方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesUp(target: Place): List<Place> {
        if (target.y == 0) return emptyList()

        val upPlaces = boardStatus[target.x]
                .take(target.y) // targetより上の行だけ抽出
                .reversed() // 上方向だと辿りにくいのでリスト反転
        return getInsidePlaces(target, upPlaces)
    }

    /** 置いた石から左上方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesUpperLeft(target: Place): List<Place> {
        if (target.x == 0 || target.y == 0) return emptyList()

        val upperLeftPlaces = boardStatus.flatMap { it }
                .filter { it.x < target.x && it.y < target.y } // targetより左上だけ抽出
                .filter { it.x - it.y == target.x - target.y } // 斜めのライン上だけ抽出
                .reversed()
        return getInsidePlaces(target, upperLeftPlaces)
    }

    /** 置いた石から右下方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesDownRight(target: Place): List<Place> {
        if (target.x + 1 > BOARD_SIZE - 1 || target.y + 1 > BOARD_SIZE - 1) return emptyList()

        val downRightPlaces = boardStatus.flatMap { it }
                .filter { it.x > target.x && it.y > target.y }
                .filter { it.x - it.y == target.x - target.y } // 斜めのライン上だけ抽出
        return getInsidePlaces(target, downRightPlaces)
    }

    /** 置いた石から右上方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesUpperRight(target: Place): List<Place> {
        if (target.x + 1 > BOARD_SIZE || target.y == 0) return emptyList()

        val upperRightPlaces = boardStatus.flatMap { it }
                .filter { it.x > target.x && it.y < target.y }
                .filter { it.x + it.y == target.x + target.y }
        return getInsidePlaces(target, upperRightPlaces)
    }

    /** 置いた石から左下方向にひっくり返せる石のリストを返す */
    private fun searchChangePlacesDownLeft(target: Place): List<Place> {
        if (target.x == 0 || target.y + 1 > BOARD_SIZE - 1) return emptyList()

        val downLeftPlaces = boardStatus.flatMap { it }
                .filter { it.x < target.x && it.y > target.y }
                .filter { it.x + it.y == target.x + target.y }
                .reversed()
        return getInsidePlaces(target, downLeftPlaces)
    }

    /** targetを始点として挟めている範囲を判定して返す */
    private fun getInsidePlaces(target: Place, places: List<Place>): List<Place> {
        // 最初に見つかった自分の石
        val endPoint = places.indexOfFirst { it.stone == target.stone }
        // 挟めていなければ終わり
        if (endPoint == -1) return emptyList()
        //挟んでいる範囲を抽出
        val insidePlaces = places.take(endPoint)
        // 挟んでいる範囲が全て相手の石なら反転範囲として返す
        if (insidePlaces.all { it.stone == target.stone.other() }) {
            return insidePlaces.map { Place(it.x, it.y, target.stone) }
        }
        return emptyList()
    }
}

一応簡単なAIも以下のページを参考に実装してみた。

オセロ(リバーシ)の作り方(アルゴリズム) ~石の位置による評価~

盤面重み付けして計算するだけだけど、それなりにちゃんと戦ってくれて面白い。

class AIStrong : OseroAI {

    val boardRatings = arrayOf(
            arrayOf(30, -12, 0, -1, -1, 0, -12, 30),
            arrayOf(-12, -15, -3, -3, -3, -3, -15, -12),
            arrayOf(0, -3, 0, -1, -1, 0, -3, -1),
            arrayOf(-1, -3, -1, -1, -1, -1, -3, -1),
            arrayOf(-1, -3, -1, -1, -1, -1, -3, -1),
            arrayOf(0, -3, 0, -1, -1, 0, -3, -1),
            arrayOf(-12, -15, -3, -3, -3, -3, -15, -12),
            arrayOf(30, -12, 0, -1, -1, 0, -12, 30)
    )

    override fun computeNext(game: OseroGame, color: Stone): Place {
        return game.boardStatus.flatMap { it }
                .filter { game.canPut(Place(it.x, it.y, color)) }
                .maxBy { checkScore(it) + game.getCanChangePlaces(it).map { checkScore(it) }.sum() }!!
    }

    private fun checkScore(place: Place) = boardRatings[place.x][place.y]
}

アルゴリズムとかゲームの計算処理とか全然知識がないので勉強してみたい。
作ったアプリは今更雑なオセロゲームなんか出しても需要が全くないと思うのでリリースはしないですが、コード全体はGitHubにあげているのでご確認ください。

github.com

桜が満開の京都ほかに一人旅してきた

kirimin.hatenablog.com

の続きです。
休暇に入ったので早速念願の一人旅に出掛けてきた。

5泊6日で京都・大阪・尾道・博多を巡ってきました。修学旅行も行かなかったので、関東より西側に行くのは人生初。
丁度桜が満開の一番いい時期に重なったのでとにかく最高だった。
ただし、休暇が確定した1ヶ月前の時点ではもう京都のホテルが全く取れない状態だったので大阪のカプセルホテルに泊まって京都に通うという面白い感じになった。

以下はInstagramに上げた写真の抜粋です。

きりみん (@kirimin) • Instagram photos and videos

1日目

適当に新幹線に乗り昼過ぎに京都へ到着。
鴨川沿いにふらふらと下鴨神社まで歩いて戻ってきた。

ビッグサイトっぽい

きりみんさん(@kirimin)が投稿した写真 -

京都にありがちなやつ!

きりみんさん(@kirimin)が投稿した写真 -

デルタ!!!!!!!!!

きりみんさん(@kirimin)が投稿した写真 -

はるばる来たぜ下鴨〜

きりみんさん(@kirimin)が投稿した写真 -

下鴨

きりみんさん(@kirimin)が投稿した写真 -

夜は京都線で大阪のカプセルホテルへ。
1日目からガッツリ歩いたせいで足がめっちゃ痛くなった。

2日目

2日目は銀閣寺から哲学の道を歩き、その後は繁華街でまったりして清水寺を鑑賞した。 足の痛みが寝ても治らなかったのでなかなかしんどかった。

満開!!!

きりみんさん(@kirimin)が投稿した写真 -

鳥!!!!!

きりみんさん(@kirimin)が投稿した写真 -

北白川たまこ

きりみんさん(@kirimin)が投稿した写真 -

きりみんさん(@kirimin)が投稿した写真 -

北白川あんこ

きりみんさん(@kirimin)が投稿した写真 -

今日はいい天気

きりみんさん(@kirimin)が投稿した写真 -

ミーハーなので舞台撮った

きりみんさん(@kirimin)が投稿した写真 -

3日目

3日目は嵐山を歩いた。とにかく足が痛くて5分歩いて5分休むみたいな感じだった。

にぎやか

きりみんさん(@kirimin)が投稿した写真 -

しゅごいいいいい

きりみんさん(@kirimin)が投稿した写真 -

中に入ったら静かだった(人は多い)

きりみんさん(@kirimin)が投稿した写真 -

展望台

きりみんさん(@kirimin)が投稿した写真 -

例のやつ2

きりみんさん(@kirimin)が投稿した写真 -

建物

きりみんさん(@kirimin)が投稿した写真 -

きりみんさん(@kirimin)が投稿した写真 -

4日目

4日目は大阪まわる予定だったけど台風みたいな天気だった上に足の痛みが限界で一歩も歩けないみたいな感じだったので、ネットカフェに半日くらい居たあと梅田スカイビルに登ったりした。

かぜつよい、しぬ

きりみんさん(@kirimin)が投稿した写真 -

5日目

5日目は午前中に大阪城に行って、午後は尾道の山に登った。

ムムッ

きりみんさん(@kirimin)が投稿した写真 -

うむうむ

きりみんさん(@kirimin)が投稿した写真 -

尾道展望

きりみんさん(@kirimin)が投稿した写真 -

尾道展望3

きりみんさん(@kirimin)が投稿した写真 -

険しい

きりみんさん(@kirimin)が投稿した写真 -

6日目

最終日は午前中にしまなみ海道をレンタサイクルで走り、夕方に博多で福岡タワーなどに行き夜の飛行機で帰ってきた。

連絡船!

きりみんさん(@kirimin)が投稿した写真 -

海の上!

きりみんさん(@kirimin)が投稿した写真 -

しまなみ海道!

きりみんさん(@kirimin)が投稿した写真 -

めちゃ景色よい

きりみんさん(@kirimin)が投稿した写真 -

めっちゃかっこよくない?

きりみんさん(@kirimin)が投稿した写真 -

すごい、海の向こうが見える

きりみんさん(@kirimin)が投稿した写真 -

日本海だ!!!!

きりみんさん(@kirimin)が投稿した写真 -

屋台通り

きりみんさん(@kirimin)が投稿した写真 -

飛行機の景色、すごかった。タワーとか展望台とはレベルが違う(あたりまえ)

きりみんさん(@kirimin)が投稿した写真 -

感想など

ずっと行きたかった京都に桜が満開の一番良い時期に行けてとにかく最高だった。
平日でも外国人観光客の数が凄まじかった。京都のバスは満員電車状態で最悪なので出来るだけ歩くか地下鉄などを利用するのが良いと思った。

大阪のカプセルホテルはホテルというより日雇い労働者の寮みたいな感じだったけどそれはそれで面白かった。2連泊くらいまでならいいけどさすがに4連カプセルホテルはパーソナルスペースの無さや布団のかたさなどで消耗してくるので良くないと思った。

飛行機に乗った事が無かったので帰りは飛行機にしたけど、移動手段として考えると飛行機はちょっと面倒くさいなと思った。新幹線は本当に楽。
ただ、飛行機からの景色は本当にすごくて、アトラクションとしては乗って損はないなと思った。

フリーランスなので長期休暇を取るぞ!

フリーランスになって最初に請け負ったお仕事が3月末で無事に契約満了となったので、せっかくなので3ヶ月くらい休暇を取る事にした。
フリーだと長期休暇が取りやすいので最高。

お仕事の思い出振り返り

某情報サービス会社さんで2年半近くお世話になっていました。
フリーランスと言ってもエージェント経由での常駐業務なので、フリーランスというか派遣みたいなものです。

最初の2年弱くらいはWebサービスのクライアントアプリ開発チームに入って保守やエンハンス開発を行っていました。
入った時は数人のチームでの小規模な開発でしたが、だんだんチームが大きくなりアプリも要件もどんどん複雑化していき、最終的にアプリのエンジニアだけで15人以上いて自分が一番古株みたいな感じだったので、技術的にもチームワーク的にもどう変化に対応しスケールして行くかみたいな事を嫌でも考えさせられた。
僕の業務内容も実装から次第に見積もりや調査、設計、レビュー、ドキュメント整備などなどが多くなりなかなかコーディングが出来なくなったりした。
それでも思い返すとGitなどの各種ツールやライブラリの導入推進や、大規模改修案件での設計など随分面白い事をやらせて貰えたなぁという気がします。
あと、非常に大きなサービスでの高い品質を求められる開発体制や、某社のスピード感と妥協のなさを両立させた仕事の進め方などに触れ大変勉強になった。

後半の半年くらいはアプリ開発部署内の技術基盤的なチームに移り、Androidの新しい機能やライブラリの調査、開発環境の整備、標準ルールの策定なんかをやっていました。
職場ではQiita:Team大好きおじさんみたいな感じでひたすら情報共有やポエムを書きなぐっていた。
これもなかなか仕事でやらせて貰える事じゃないので貴重な経験でした。

職場で友人もたくさん出来てとても居心地が良かったのですが、長期休暇が取りたかったのと、せっかくフリーランスになったので、もっと色々な環境でガリガリ実装するような仕事を経験してみたかったのもあって3月末で離れる事になりました。

しんどくなった時に辞めやすいようにという理由もあってフリーランスになったのに、今までの人生で一番長く同じ場所に毎朝通った2年半でした。(学校もロクに行ってなかったし会社も1年で辞めたし)
最後の日には寄せ書きを貰ったりもしてとても有り難かった。

今後の予定

3ヶ月くらいは仕事をせずに、趣味のコードを書いたり本を読んだりして平和に過ごす予定です。
働いている間にすっかりコーディングから離れてしまったので、休暇中にガッツリ勘を取り戻したいですね。あとAndroid以外も色々と勉強して出来るようになりたい。
とは言えあんまり休んでいるとお金が無くなってしまうので、その後はまたフリーランスとしてお仕事を探してアプリの開発をしたいと思っています。

もし、Androidのお仕事(特にKotlin案件や週三勤務、リモートなど)があるよ!という方がいらっしゃったらTwitter(@kirimin)か何かで連絡して頂きお話聞かせていただけたら嬉しいです。

自動化ゲーのFactorioにハマりすぎて生活がヤバイ

最近Factorioというゲームを狂ったようにやってる。

Factorio on Steam

どういうゲームかというと、工場のラインをひたすら構築してアイテムを自動で大量生産してゆく、というシミュレーションゲームです。
宇宙船が墜落して未開の惑星に放り出された主人公はロケットを作るために資源を採取して部品を大量生産する必要があるのです。

トレーラーを見るとだいたいどんな感じか分かると思う。

www.youtube.com

www.youtube.com

一見シムシティのような感じですが、自キャラを操作するのでどちらかというとMinecraftに近くて、ざっくり言えばMinecraft釜戸とホッパーを繋ぐような作業をひたすらやり続けるゲーム。
もしくはリアルタイムストラテジーの内政をひたすらやるゲーム。定期的に敵が攻めてくるのでタレットを配置して防衛したりする必要もあります。

このゲーム、安かったので軽い気持ちで買ったのですが、予想を遥かに超える中毒性があり、購入から1週間半で既に42時間もプレイしてます。そろそろFallout4のプレイ時間を超えそうです。
一度やり始めると何時間でも無心でやってしまい生活が破綻し始めているので、そろそろFactorio禁した方がいい気がしてる。
常に生産ラインが動き続けているので眺めているだけでも楽しいのと、ラインはいくらでも改善できてどこかを効率化すると別のどこかがボトルネックになって…というのを無限に繰り返して止まらなくなります。

(最後の方は死んだ目で)30時間ほどプレイしてようやくクリアしたのですが、「もっと効率よくラインを構築すれば半分以下の時間でクリア出来たはずだ…」という思いが頭から離れず、2週目を始めてしまいました。
このゲームの面白さってプログラミングの面白さに似ているなぁと感じていて、手動で操作しても出来る事を一度の手間を惜しまず自動化する事で飛躍的に作業効率が上がる喜びと、より綺麗な設計を考えて何度もリファクタリングしてしまう奥の深さがあります。
というか、プログラマになったら一日中このゲームみたいな作業を黙々としていられるんだろうなぁと思っていたのに、実際の仕事では集中して自由にコーディング出来るような事は全然なくて会議やらExcelやら調査やらばっかりで複雑な気持ちになりますね。

Factorioをやってると普段の生活や仕事も全部ベルトコンベアとアームで自動化出来たらなぁ…と妄想してしまう。
オススメのゲームです。
あと、全然関係ないけどオデッセイの原作の「火星の人」を読みました。Factorioとテーマがちょっと似てる。

https://i.gyazo.com/1e17a8236c75a04ff6dbbf27de273d8b.png 現在プレイ中のライン

Live2Dに入門してAndroidで動かしたりして遊んだ

Live2Dという技術があります。 http://www.live2d.com/

二次元のイラストを3D化せずにそのまま動かせるという夢の技術。
最近だとFaceRigと組み合わせたこんなのが話題になったりした。

www.4gamer.net

3年くらい前にYoutubeで紹介動画を見た時からずっと応援している技術なんだけど、このLive2Dでキャラを動かすのを自分でも試してみた。

Cubism EditorでLive2Dモデルを作ってみる

Cubism Editor | Live2D

どうやらこのCubism EditorというソフトウェアでLive2Dモデルが作成出来るようなので、インストールした。
とりあえず公式のチュートリアルページを見ながら見よう見まねでやってみる。

入門チュートリアル - Live2D Cubism 2 マニュアル

元の絵はこんな感じです。

Live2Dの仕組みをざっくり説明すると、イラストのパーツ(目とか髪とか)を福笑いのように別テクスチャとして配置して、それをずらしたり歪ませたりする動きを設定する…という感じ。
なので、まずはパーツの切り出しを行う必要があります。
行いました。

実際には作り始めて見ないとどんな風に切り分ければいいのかよく分からなかったので5回くらい作業やり直してる。
(フリーモードだとパーツのサイズとかに色々制限があったりもする)

切り出したパーツをテクスチャとして取り込み、頑張って歪ませたりして動きのパターンを設定していく。

Cubism Animatorでアニメーションを作ってみる

上で作成したモデルを付属のCubism Animatorというソフトウェアに読み込ませると、モデルを動かしたアニメーションが作成出来る。

こんな感じ。
動画のコマごとにモデルのパラメータを設定するだけで簡単にアニメーションが作れます。

そして実際に完成したアニメーションGifがこちらになります。

アニメーションや3Dの知識が全くなくても半日くらいでなんかそれっぽい動く絵が出来たぞ!
やったね!

Androidで動かしてみる

と、アニメーションGifを作っただけで終わってしまったらAndroidエンジニアとしては面白くないので、Androidアプリとして動かしてみる。

AndroidSDKがあったのでこれを使おう。

Android - Live2D Cubism 2 マニュアル

なんかサンプルがEclipseプロジェクトだったり、説明の殆どがEclipseの使い方でAPIの使い方についての説明が全然なかったりして不安になるが…。
とりあえずモデルが表示されるだけだったサンプルコードをちょっと書き換えてタッチイベントに反応するようにしてみた。

public class Live2dSurfaceView extends GLSurfaceView {
    private SampleGLRenderer renderer;

    public Live2dSurfaceView(Context context) {
        super(context);

        renderer = new SampleGLRenderer();
        setRenderer(renderer);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        renderer.updateAngle(event.getX());
        return true;
    }

    class SampleGLRenderer implements Renderer {
        private Live2DModelAndroid live2DModel;
        private final String MODEL_PATH = "live2d/model.moc";
        private final String TEXTURE_PATHS[] = {"live2d/model.2048/texture_00.png"};

        @Override
        public void onDrawFrame(GL10 gl) {
            gl.glMatrixMode(GL10.GL_MODELVIEW);
            gl.glLoadIdentity();
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
            gl.glEnable(GL10.GL_BLEND);
            gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA);
            gl.glDisable(GL10.GL_DEPTH_TEST);
            gl.glDisable(GL10.GL_CULL_FACE);

            live2DModel.setGL(gl);
            live2DModel.update();
            live2DModel.draw();
        }


        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            gl.glViewport(0, 0, width, height);

            gl.glMatrixMode(GL10.GL_PROJECTION);
            gl.glLoadIdentity();

            float modelWidth = live2DModel.getCanvasWidth();
            float visibleWidth = modelWidth * (3.0f / 4.0f);
            float margin = 0.5f * (modelWidth / 4.0f);

            gl.glOrthof(margin, margin + visibleWidth, visibleWidth * height / width, 0, 0.5f, -0.5f);
        }


        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            try {
                InputStream in = getContext().getAssets().open(MODEL_PATH);
                live2DModel = Live2DModelAndroid.loadModel(in);
                in.close();

                for (int i = 0; i < TEXTURE_PATHS.length; i++) {
                    InputStream tin = getContext().getAssets().open(TEXTURE_PATHS[i]);
                    int texNo = UtOpenGL.loadTexture(gl, tin, true);
                    live2DModel.setTexture(i, texNo);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void updateAngle(float param){
            Log.d("test", param / 30 + "");
            live2DModel.setParamFloat("PARAM_ANGLE_Z", param / 30);
        }
    }
}

こんな感じ

一応なんとなく動かせたけど、多分ほとんどの人はUnity用SDKを使ってるんだろうなぁ。

おまけ

決まった動きするキャラを表示させたいだけだったら、↓のライブラリとかでgifをそのまま画像として表示させた方が楽な気がした。

https://github.com/koral--/android-gif-drawable

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="me.kirimin.live2dsample.MainActivity">

    <pl.droidsonroids.gif.GifImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/girl"
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="true"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Live2Dだよ!!!!!!!"
        android:minLines="4"
        android:background="#CCCCCCCC"
        android:padding="8dp"
        android:layout_alignBottom="@+id/image"/>
</RelativeLayout>

こんな感じで。