Android のライブ壁紙を作ってみる

2010 年頃でしょうか、 Android 2.1 (Eclair) の端末が普及し始めたのは。 Eclair…

2010 年頃でしょうか、 Android 2.1 (Eclair) の端末が普及し始めたのは。
Eclair からライブ壁紙というものが実装され、待ち受け画面でも背景を動かすことができるようになりました。
それに伴って、嬉々として個人的にライブ壁紙アプリを作ったりしたものです。
当時はサンプルコードが Android SDK に同梱されていたので見よう見まねでどうにかなりましたが、現在ではサンプルにライブ壁紙が無くなっていたので今回はサンプルを作成してみようかと思います。
 

ライブ壁紙の仕組み


ライブ壁紙といっても所詮 Surface でできています。
イメージとしては、待ち受け画面の背景が SurfaceView でできていると思ってもらえれば問題ありません。
SurfaceHolder#lockCanvas で Canvas を取得し、そこに描画していくのがセオリーとなります。
※ SurfaceHolder#unlocakCanvasAndPost は忘れないこと
もちろん、 Surface に OpenGL のコンテキストを割り当てて、 OpenGL 側で処理することも可能です。
 

今回作成するもの


パラパラ漫画の要領でアニメーションライブ壁紙を作成する。
用意するアニメーション用の画像はカメラで動画を撮影し、連番で切り出したものを使用。
 

実装


ライブ壁紙アプリとはいっても、通常のアプリとは異なり Activity を必要としません(設定画面を作る場合はこの限りではない)。
WallpaperService を継承したクラスだけで作成できます。
 
では、まずはコードを見てみましょう。
 
AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    package="jp.co.supersoftware.livewallpapertest">

    <uses-feature android:name="android.software.live_wallpaper" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <service
            android:name=".LiveWallpaperTest"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_WALLPAPER">
            <intent-filter>
                <action android:name="android.service.wallpaper.WallpaperService" />
            </intent-filter>
            <meta-data
                android:name="android.service.wallpaper"
                android:resource="@xml/live_wallpaper_test" />
        </service>

    </application>

</manifest>

 
live_wallpaper_test.xml

<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android" />

 
LiveWallpaperTest.kt

package jp.co.supersoftware.livewallpapertest

import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import android.os.Handler
import android.service.wallpaper.WallpaperService
import android.util.Log
import android.view.SurfaceHolder
import java.io.File

/**
 * ライブ壁紙メインクラスです。
 *
 * Created by Daisuke on 2017/06/20.
 */
class LiveWallpaperTest : WallpaperService() {

    companion object {
        /**
         * 描画間隔
         */
        const val FPS = 30
    }

    private val mHandler = Handler()

    override fun onCreateEngine(): Engine {
        return LiveWallpaperEngine()
    }

    inner class LiveWallpaperEngine : Engine() {

        /**
         * ライブ壁紙起動時の時刻
         */
        private var mFirstTime = 0L
        /**
         * 計測開始時(fps調整用)
         */
        private var mStartTime = 0L
        /**
         * 計測終了時(fps調整用)
         */
        private var mEndTime = 0L
        /**
         * 描画領域(幅)
         */
        private var mWidth = 0
        /**
         * 描画領域(高さ)
         */
        private var mHeight = 0
        /**
         * ライブ壁紙が表示されているか
         */
        private var mVisible = false
        /**
         * 表示する画像番号
         */
        private var mIndex = "0000"

        init {
        }

        override fun onSurfaceCreated(holder: SurfaceHolder?) {
            super.onSurfaceCreated(holder)
        }

        override fun onSurfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
            super.onSurfaceChanged(holder, format, width, height)
            mWidth = width
            mHeight = height
            drawFrame()
        }

        override fun onSurfaceDestroyed(holder: SurfaceHolder?) {
            super.onSurfaceDestroyed(holder)
            mVisible = false
            mHandler.removeCallbacks(mDrawRunnable)
        }

        override fun onVisibilityChanged(visible: Boolean) {
            super.onVisibilityChanged(visible)
            mVisible = visible
            if (visible) {
                drawFrame()
            } else {
                mHandler.removeCallbacks(mDrawRunnable)
            }
        }

        /**
         * 画面に書き込みます。
         */
        private fun drawFrame() {
            if (mFirstTime == 0L) {
                mFirstTime = System.currentTimeMillis()
            }
            mStartTime = System.currentTimeMillis()

            val canvas = surfaceHolder.lockCanvas()
            canvas?.let { canvas ->
                drawPicture(canvas)
                surfaceHolder.unlockCanvasAndPost(canvas)
            }

            mEndTime = System.currentTimeMillis()

            // 次の描画設定
            val delay = 1000 / FPS - (mEndTime - mStartTime)
            mHandler.removeCallbacks(mDrawRunnable)
            if (mVisible) {
                mHandler.postDelayed(mDrawRunnable, delay)
            }
        }

        /**
         * 画像をCanvasに描画します。
         *
         * @param canvas キャンバス
         */
        private fun drawPicture(canvas: Canvas) {
            try {
                // 現在表示すべき画像番号を計算
                val index = (mStartTime - mFirstTime) / (1000 / FPS) % 227
                val indexString = String.format("%04d", index)
                // assets から画像を読み込み、 canvas に描画
                assets.open("animation${File.separator}live_wallpaper_test$indexString.jpg").use { inputStream ->
                    val bitmap = BitmapFactory.decodeStream(inputStream)
                    canvas.drawBitmap(bitmap, null, Rect(0, 0, mWidth, mHeight), null)
                }
            } catch (e: Throwable) {
                Log.e("LiveWallpaperTest", e.message, e)
            }
        }

        /**
         * 画面書き込み用スレッド
         */
        private val mDrawRunnable = Runnable { drawFrame() }

    }

}

 
まずは、マニフェストファイルから。
uses-feature では android.software.live_wallpaper を指定します。これは、アニメーションを含む壁紙を使用または提供することを明示します。
WallpaperService を継承したクラスを application の配下に設定します。
service の android:permission 属性には android.permission.BIND_WALLPAPER を設定します。 WallpaperService を利用する場合必須となります。
intent-filter には、通常設定する android.intent.action.MAIN ではなく、ライブ壁紙アプリであることを宣言する android.service.wallpaper.WallpaperService を設定します。
meta-data にはライブ壁紙設定用の xml ファイルを指定します。今回は設定画面を作成しないので live_wallpaper_test.xml の中身はほぼありません。
 
次にソースコード。
WallpaperService を継承したクラスがサービスとして動作します。
描画関連は更にその配下の Engine を継承したクラスで行われます。
ここで Surface を操作していく形となります。
 
onSurfaceCreated: Surface が作成されたときのイベントハンドラーです。
onSurfaceChanged: Surface が作成されたとき、画面が回転したときのイベントハンドラーです。
onSurfaceDestroyed: Surface が破棄されたときのイベントハンドラーです。
onVisibilityChanged: ライブ壁紙が可視・不可視になったときのイベントハンドラーです。
 
基本的には onSurfaceChanged や onVisibilityChanged で可視状態になったときに描画スレッドを開始して、 onSurfaceDestroyed や onVisibilityChanged で不可視状態になったときに描画スレッドを停止します。
アニメーション用の画像は今回 assets に入れてあるのでそこから読み出しています。あとは fps 調整のディレイを計算して次回描画のタイミングを設定するだけです。
 

実行結果


 
 
 
案外簡単に作成でき、壁紙が動くというインパクトが当時は凄かったように思います。
タッチ操作なども取得できるのでもっと凝ったものが作れたらもっと楽しいかもしれません。