システム壁紙を制御する ～ホームアプリ（ランチャーアプリ）の作り方～

ホームアプリ（ランチャーアプリ）の作り方シリーズ
今回はシステム壁紙の制御の仕方について説明してみたいと思います。

- [超シンプルなホームアプリを作る ～ホームアプリ（ランチャーアプリ）の作り方～](https://qiita.com/ryo_mm2d/items/00326b0d8f088975fa0e)
- [システム壁紙を制御する ～ホームアプリ（ランチャーアプリ）の作り方～](https://qiita.com/ryo_mm2d/items/02bfab5f130b8d17124b) ←イマココ
- [ウィジェット一覧を作る ～ホームアプリ（ランチャーアプリ）の作り方～](https://qiita.com/ryo_mm2d/items/19bd3dce898821920036)
- [ウィジェットをアプリ上に表示する ～ホームアプリ（ランチャーアプリ）の作り方～](https://qiita.com/ryo_mm2d/items/0dfc8a9874ba2dd1ae0c)

※シリーズが続くとは言っていない


いわゆるホームアプリに表示される壁紙は大きく2種類あります。ホームアプリ自体が描画しているものと、システムが描画しているものです。ホームアプリが描画しているものというは要するにActivity上で描画しているだけですので、特に説明不要ですね。システム壁紙は前述の記事で説明していますが、以下のようなスタイルをActivityに設定することでActivityを透過させて、その背景に表示させることができます。

```xml:styles.xml
<style name="AppTheme.Wallpaper" parent="@style/Theme.AppCompat">
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowShowWallpaper">true</item>
</style>
```

ここでは、システム壁紙の制御について説明します。

# 壁紙のオフセットスクロールを行う

ホームアプリのページをスクロールするとそれに併せて背景もスクロールさせているものが多いと思います。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/999c15f1-3fd1-88e5-e152-233f8df10dcc.gif)

これは `WallpaperManager.setWallpaperOffsets()` をコールすることで実現できます。
第一引数にはWindowTokenを渡します、これは適当なViewから取り出すのが楽です。
第二引数はX軸、第三引数はY軸それぞれのスクロールオフセットを指定します。
このオフセット量は0.0fが一番左(上)で、1.0fが一番右(下)になる値ですので、ページ数をそのまま送るのではなく、この範囲に収まるように調整が必要です。

適当にViewPager2を使った5ページ分スクロールできる画面を作って実装してみたものが以下になります。

```kotlin
class LauncherActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityLauncherBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.viewPager.adapter = LauncherPagerAdapter(this)
        binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageScrolled(position: Int, positionOffset: Float, offsetPixels: Int) {
                val offset = (position + positionOffset) / (PAGE_NUM - 1)
                getSystemService<WallpaperManager>()?.setWallpaperOffsets(
                    binding.viewPager.windowToken, offset, offset
                )
            }
        })
    }

    class LauncherPageFragment : Fragment(R.layout.fragment_launcher_page) {
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            FragmentLauncherPageBinding.bind(view).text.text =
                requireArguments().getInt(KEY_NUMBER).toString()
        }

        companion object {
            private const val KEY_NUMBER = "KEY_NUMBER"
            fun newInstance(index: Int): LauncherPageFragment =
                LauncherPageFragment().apply {
                    arguments = Bundle().also { it.putInt(KEY_NUMBER, index + 1) }
                }
        }
    }

    class LauncherPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
        override fun getItemCount(): Int = PAGE_NUM
        override fun createFragment(position: Int): Fragment =
            LauncherPageFragment.newInstance(position)
    }

    companion object {
        private const val PAGE_NUM = 5
    }
}
```

ここではページを横方向にスクロールしているのでX軸方向だけにoffsetを設定しても良いのですが、Y軸方向にも同じように設定しています。
そうすると横向き画面では上下方向にスクロールしてくれるようになります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/10e65093-f98b-9ca3-ac26-36cbf6a770f1.gif)

# システム壁紙を変更する

## 画像の壁紙を設定する

壁紙を変更するには`android.permission.SET_WALLPAPER`パーミッションが必要です。

```xml:AndroidManifest.xml
<uses-permission android:name="android.permission.SET_WALLPAPER" />
```

設定はWallpaperManagerのメソッドを呼び出します。設定する画像としてはRawリソース、InputStream、Bitmapが使えます。

```kotlin
getSystemService<WallpaperManager>()?.let { 
    // リソースIDを渡す
    it.setResource(R.raw.hogehoge)
    // InputStreamを渡す
    it.setStream(resources.assets.open("hogehoge.jpg"))
    // Bitmapを渡す
    it.setBitmap(bitmap)
}
```

ホームアプリ自身がリソースとしていくつか壁紙を持っているというのはよくありますね。
その壁紙がrawにあれば`setResource()`、assetsにあれば`setStream()`で設定できそうですね。`setResource()`に渡すことができるリソースはrawのみで、Drawableは設定することができません。Drawableの場合はBitmapDrawableからBitmapを取り出すなどで`setBitmap()`する必要があります。Bitmapが渡せるなら、動的に作った画像を設定することも可能ですね。

ユーザー環境にある画像を使う場合は、ギャラリーなどから画像を選択してもらって、それを設定します。
なので、以下のようにstartActivityForResultで呼び出して

```kotlin
val intent =
    Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).also {
        it.type = "image/*"
    }
startActivityForResult(intent, REQUEST_CODE_IMAGE)
```

取得したBitmapを取り出して、設定。

```kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE_IMAGE) {
        if (resultCode != RESULT_OK) return
        val uri = data?.data ?: return
        val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            ImageDecoder.decodeBitmap(ImageDecoder.createSource(contentResolver, uri))
        } else {
            MediaStore.Images.Media.getBitmap(contentResolver, uri)
        }
        getSystemService<WallpaperManager>()?.setBitmap(bitmap)
        return
    }
    super.onActivityResult(requestCode, resultCode, data)
}
```

プレビューを挟まず、画像の情報を拾う必要も無いという場合は、InputStreamを渡してしまう方がシンプルかもですね。

```kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_CODE_IMAGE) {
        if (resultCode != RESULT_OK) return
        val uri = data?.data ?: return
        getSystemService<WallpaperManager>()
            ?.setStream(contentResolver.openInputStream(uri))
        return
    }
    super.onActivityResult(requestCode, resultCode, data)
}
```

### 壁紙の設定場所を指定する

API 24以上ですが、`setBitmap()`と`setStream()`の4引数版、`setResources()`の2引数版があり、それらの最後の引数で、ロック画面背景（`WallpaperManager.FLAG_LOCK`）か、システム背景（`WallpaperManager.FLAG_SYSTEM`)か、その両方か（ビット和）を指定することができます。

```kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    getSystemService<WallpaperManager>()?.setBitmap(
        bitmap, null, false, WallpaperManager.FLAG_SYSTEM
    )
}
```

また、第二引数にはRectを渡すことができて、このRectの内側が表示されるようにリサイズされて表示されます。

### 壁紙の色変更通知を受け取る

API 27以降では壁紙の色が変化した際に通知を受け取ることができるようになっています。
設定された壁紙の色に合わせてホームアプリのテーマを変更するなどをしたい場合に利用できます。

```kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
   getSystemService<WallpaperManager>()?.addOnColorsChangedListener({ color, whitch ->
   }, Handler(Looper.getMainLooper()))
}
```

第一引数のcolorは`WallpaperColors`のインスタンスで `primaryColor` `secondaryColor` `tertiaryColor` と `android.graphics.Color` 型の色情報で最大第三次色までを取得可能です。引数のcolor自体も、 `primaryColor` `secondaryColor` `tertiaryColor` それぞれもNullableなので注意しましょう。この色の抽出はPaletteを使っているようです。
whitchにはその壁紙の場所（`WallpaperManager.FLAG_LOCK`、`WallpaperManager.FLAG_SYSTEM`、これらのビット和）が通知されます。

## ライブ壁紙を設定する

ライブ壁紙についてはホームアプリから直接設定するのではなく、設定画面を呼び出します。

```kotlin
startActivity(Intent(WallpaperManager.ACTION_LIVE_WALLPAPER_CHOOSER))
```

|ライブ壁紙設定画面|ライブ壁紙プレビュー画面|
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/39c25499-ed0f-1415-489d-8592b0d141ab.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/4bca3bb0-f8ca-062e-01ce-39e7a07e42fa.png)|

ライブ壁紙のComponentNameが分かっている場合は、以下のようにすることでライブ壁紙を指定して設定させることができます。コールするといきなり反映されるわけではなく、プレビュー画面が表示されます。

```kotlin
startActivity(Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER).also {
    it.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, componentName)
})
```

ライブ壁紙アプリは `android.service.wallpaper.WallpaperService` というActionのintent-filterを持つサービスとして実装しますので、以下のようにライブ壁紙のコンポーネント名の一覧を取得することはできます。
`android.service.wallpaper.WallpaperService` というAction名は `WallpaperService.SERVICE_INTERFACE` で定義されています。

```kotlin
val liveWallpapers = 
    packageManager.queryIntentServices(Intent(WallpaperService.SERVICE_INTERFACE), 0)
        .map { ComponentName(it.serviceInfo.packageName, it.serviceInfo.name) }
```

なので、ライブ壁紙のセレクターも自前実装することは可能ではありますが、実装するメリットはあまりなさそうですね。`WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER`は自分自身がライブ壁紙を提供している場合に、自分を設定してもらうために使うぐらいかなと思います。

----

以上、システム壁紙の制御について説明しました。
目新しい機能ではなく、昔からある機能ですが、通常のアプリ開発ではあまり触れない部分かな、と思います。
また、テーマ設定やモダンOSに合わせた機能が追加されていたり、地味ながらちゃんと進化している機能でもあります。