ホームアプリ(ランチャーアプリ)の作り方シリーズ
今回はシステム壁紙の制御の仕方について説明してみたいと思います。
- 超シンプルなホームアプリを作る ~ホームアプリ(ランチャーアプリ)の作り方~
- システム壁紙を制御する ~ホームアプリ(ランチャーアプリ)の作り方~ ←イマココ
- ウィジェット一覧を作る ~ホームアプリ(ランチャーアプリ)の作り方~
- ウィジェットをアプリ上に表示する ~ホームアプリ(ランチャーアプリ)の作り方~
※シリーズが続くとは言っていない
いわゆるホームアプリに表示される壁紙は大きく2種類あります。ホームアプリ自体が描画しているものと、システムが描画しているものです。ホームアプリが描画しているものというは要するにActivity上で描画しているだけですので、特に説明不要ですね。システム壁紙は前述の記事で説明していますが、以下のようなスタイルをActivityに設定することでActivityを透過させて、その背景に表示させることができます。
<style name="AppTheme.Wallpaper" parent="@style/Theme.AppCompat">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowShowWallpaper">true</item>
</style>
ここでは、システム壁紙の制御について説明します。
壁紙のオフセットスクロールを行う
ホームアプリのページをスクロールするとそれに併せて背景もスクロールさせているものが多いと思います。
これは WallpaperManager.setWallpaperOffsets()
をコールすることで実現できます。
第一引数にはWindowTokenを渡します、これは適当なViewから取り出すのが楽です。
第二引数はX軸、第三引数はY軸それぞれのスクロールオフセットを指定します。
このオフセット量は0.0fが一番左(上)で、1.0fが一番右(下)になる値ですので、ページ数をそのまま送るのではなく、この範囲に収まるように調整が必要です。
適当にViewPager2を使った5ページ分スクロールできる画面を作って実装してみたものが以下になります。
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軸方向にも同じように設定しています。
そうすると横向き画面では上下方向にスクロールしてくれるようになります。
システム壁紙を変更する
画像の壁紙を設定する
壁紙を変更するにはandroid.permission.SET_WALLPAPER
パーミッションが必要です。
<uses-permission android:name="android.permission.SET_WALLPAPER" />
設定はWallpaperManagerのメソッドを呼び出します。設定する画像としてはRawリソース、InputStream、Bitmapが使えます。
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で呼び出して
val intent =
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).also {
it.type = "image/*"
}
startActivityForResult(intent, REQUEST_CODE_IMAGE)
取得したBitmapを取り出して、設定。
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を渡してしまう方がシンプルかもですね。
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
)か、その両方か(ビット和)を指定することができます。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
getSystemService<WallpaperManager>()?.setBitmap(
bitmap, null, false, WallpaperManager.FLAG_SYSTEM
)
}
また、第二引数にはRectを渡すことができて、このRectの内側が表示されるようにリサイズされて表示されます。
壁紙の色変更通知を受け取る
API 27以降では壁紙の色が変化した際に通知を受け取ることができるようになっています。
設定された壁紙の色に合わせてホームアプリのテーマを変更するなどをしたい場合に利用できます。
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
、これらのビット和)が通知されます。
ライブ壁紙を設定する
ライブ壁紙についてはホームアプリから直接設定するのではなく、設定画面を呼び出します。
startActivity(Intent(WallpaperManager.ACTION_LIVE_WALLPAPER_CHOOSER))
ライブ壁紙設定画面 | ライブ壁紙プレビュー画面 |
---|---|
ライブ壁紙のComponentNameが分かっている場合は、以下のようにすることでライブ壁紙を指定して設定させることができます。コールするといきなり反映されるわけではなく、プレビュー画面が表示されます。
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
で定義されています。
val liveWallpapers =
packageManager.queryIntentServices(Intent(WallpaperService.SERVICE_INTERFACE), 0)
.map { ComponentName(it.serviceInfo.packageName, it.serviceInfo.name) }
なので、ライブ壁紙のセレクターも自前実装することは可能ではありますが、実装するメリットはあまりなさそうですね。WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER
は自分自身がライブ壁紙を提供している場合に、自分を設定してもらうために使うぐらいかなと思います。
以上、システム壁紙の制御について説明しました。
目新しい機能ではなく、昔からある機能ですが、通常のアプリ開発ではあまり触れない部分かな、と思います。
また、テーマ設定やモダンOSに合わせた機能が追加されていたり、地味ながらちゃんと進化している機能でもあります。