📱

Android12 SplashScreen対応

2022/11/03に公開

初めに

ポートでAndroid開発をしている @shxun6934 です。

今更ながら、弊社のAndroidアプリで独自に作成したSplashScreenをAndroid 12 以降はデフォルトで表示されるSplashScreenに移行しました。
その移行について、お話ししていきたいと思います。

SplashScreenや移行の仕方については、 公式に載っているので、そちらも確認してみてください。

https://developer.android.com/develop/ui/views/launch/splash-screen

https://developer.android.com/develop/ui/views/launch/splash-screen/migrate

https://developer.android.com/reference/kotlin/androidx/core/splashscreen/SplashScreen

SplashScreenとは

アプリが起動した際に表示されるアニメーション画面をSplashScreenと言います。

Android 12 以降からは、アプリ起動時にSplashScreenデフォルトで表示されるようになりました。
splash screen

SplashScreenの役割

SplashScreenは、アプリ起動からホーム画面 (AndroidManifest.xmlLAUNCHERを設定している画面) を表示するまでに、必要最低限のデータの読み込みを行うための画面だと思っています。

コンポーネントやライブラリの読み込みまたは初期化は、極力、必要になった時に行い、ApplicationonCreateをできるだけ軽量にするようにGoogleでは推奨しています。

移行した目的

今までの実装の仕方では、AndroidOSバージョンによる挙動の違いが出てしまいました。
その挙動の差をなくすために移行しようと思ったのが、今回の対応の目的になります。

Android 12以降と以前の挙動の違い

Android 11 以前はSplashScreenが提供されていないので、弊社ではSplashScreenを表示するSplashActivityを作成して、そこで起動時に必要なロジックを書いていました。

このやり方では、Android 12になるとデフォルトのSplashScreenと独自のSplashScreen連続して表示される挙動になってしまいます。
そのため、 ユーザーはホーム画面が表示されるまでの時間が長く感じるので、UXの観点から適切でないと感じました。

移行

では、実施に公式ドキュメントを見ながら移行していきます。

Libraryの導入

compileSdk31以上にして、androidx.core:core-splashscreenを導入します。

build.gradle
android {
  // Android 12
  compileSdkVersion 31 
  ...
}

dependencies {
  implementation 'androidx.core:core-splashscreen:1.0.0'
}

画面

SplashScreenのテーマを作成します。

themes.xml
<!-- Application Theme -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
  <item name="colorPrimary">@color/...</item>
  ...
</style>

<!-- SplashScreen Theme-->
<style name="AppTheme.SplashScreen" parent="Theme.SplashScreen">

   <!-- 表示するアイコンを設定する。(必須)-->
   <!-- アイコン自体をアニメーションさせたい場合は、AnimationDrawableかAnimatedVectorDrawableを使用したdrawableリソースを設定する。 -->
   <item name="windowSplashScreenAnimatedIcon">@drawable/...</item>

   <!-- アイコンの背景色を設定する。-->
   <item name="android:windowSplashScreenIconBackgroundColor">@color/...</item>

   <!-- SplashScreenの背景色を設定する。 -->
   <item name="windowSplashScreenBackground">@color/...</item>

   <!-- アイコンがアニメーションする場合、アニメーションさせる時間を設定する。 -->
   <!-- デフォルトでは、10000ms。推奨は、1000ms以内-->
   <item name="windowSplashScreenAnimationDuration">1000</item>

   <!-- SplashScreenを表示するActivityのThemeを設定する。(必須) -->
   <item name="postSplashScreenTheme">@style/AppTheme</item>
</style>

表示

作成したテーマをSplashScreenを表示したいApplicationまたはActivityのテーマに設定します。
(AndroidManifestで設定しています。)

AndroidManifest.xml
<manifest>
  <application android:theme="@style/AppTheme.SplashScreen">
  <!-- or -->
    <activity android:theme="@style/AppTheme.SplashScreen">
...

ActivityonCreatesuper.onCreate()の前にinstallSplashScreenを呼び出します。

class SplashActivity : Activity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    // SplashScreenの呼び出し
    installSplashScreen()

    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_splash)
    ...

これでSplashScreenをカスタマイズし、表示することができます。

既存のSplashActivityの扱い

AndroidOSのSplashScreenの表示はできましたが、まだ既存のSplashScreenが残っている問題が解決されていません。
なので、以下のどれかの対応を行う必要があります。

  1. SplashActivityを表示されないようにする。(SplashActivityで行うロジックは残す)
  2. SplashActivityを表示させるが、違和感のないようにする
  3. SplashActivityを削除し、SplashActivityで行っていたことを他のActivityに移す

弊社では、2の対応を行いました。

SplashActivityでは、ログインチェック・計測サービスにユーザー情報を送る・DynamicLinkによる開始Activityのチェックなどを行っていて、他Activityに移すとしても他Activityが肥大化してしまいます。
また、通信できなかった場合は、再通信させるためのダイアログを表示しているので、親Viewが必要になります。

以上のことから、ロジックを処理するためにActivityを残し、Viewの描画は通信に失敗した時のみ描画する方針にしました。

(Googleは削除を推奨しているので、近日対応します。)

SplashScreenの描画時間を伸ばす

アプリの起動に必要なデータが揃うまでは、SplashScreenを表示しておかないといけません。
SplashScreenを表示し続けるためには、ViewTreeObserver.OnPreDrawListener
を使用して、アプリを一時停止しておきます。

SplashActivity.kt
class SplashActivity : Activity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // rootでOnPreDrawListenerを監視する。
    val content: View = findViewById(android.R.id.content)
    content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {

      override fun onPreDraw(): Boolean {
        // 準備ができた場合は、trueで描画開始。
        // 準備中の場合は、falseで一時停止。
        if (isReady) {
          // rootのOnPreDrawListenerを削除する。
          content.viewTreeObserver.removeOnPreDrawListener(this)
          true
        } else {
          false
        }    
      }
    })
  }
}

SplashActivityのViewを表示しない

Viewを描画しないようにするには、SplashScreen#setKeepOnScreenConditionを使用します。

class SplashActivity : Activity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    // Splash Screenの呼び出し
    val splashScreen = installSplashScreen()
    super.onCreate(savedInstanceState)
    
    // SplashScreenを表示し続ける
    // trueの場合は、SplashScreenを表示し続ける
    // falseの場合は、ActivityのViewを表示する
    splashScreen.setKeepOnScreenCondition { true }
  }
}

SplashActivityのViewをSplashScreenに寄せる

SplashScreenSplashActivityViewを同じデザインにすることで、画面が切り替わっているように見えないようにすることもできます。

SplashScreenに表示されるアイコンのサイズは、背景があるアイコンの場合、240×240dpで直径160dpの円、背景がないアイコンの場合、288×288dpで直径192dpの円になります。

弊社では

ViewModelでデータとデータが準備できたかどうかをステータスとして持っておきます。
準備ができたら、SplashScreenの描画を再開、データの状態によって、SplashScreenを表示するか、SplashActivityViewを表示するかを決めます。

通信エラーをダイアログで表示するためには、親Viewが必要です。
そのため、通信に成功した場合のみ、splashScreen.setKeepOnScreenCondition { true }を宣言して、Viewを描画しないようにしています。

class SplashActivity : Activity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    val splashScreen = installSplashScreen()

    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_splash)
    
    val content: View = findViewById(android.R.id.content)
      content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
        override fun onPreDraw(): Boolean {
          // viewModelのデータが揃ったら描画開始する。
          return if (viewModel.result.value is SplashViewModel.Result.Success) {
            content.viewTreeObserver.removeOnPreDrawListener(this)
            true
          } else {
            false
          }
        }
      })
    
    lifecycleScope.launch {
      viewModel.result.collect {
        when (it) {
          is SplashViewModel.Result.Success -> {
            // SplashScreenして、Activityに遷移させる
            splashScreen.setKeepOnScreenCondition { true }
            startNextActivity()
            finish()
          }
          is SplashViewModel.Result.Error -> {
            // SplashActivityのViewを表示して、ダイアログを表示する。
            showConnectErrorDialog()
          }
        }
      }
    }
  }
}

これで、SplashScreenの移行ができました。(完全な移行ではない。)

終わりに

SplashScreenの実装方法と移行の仕方について見ていきました。

SplashScreen自体の実装はそれほど難しくないですが、既存のSplashScreenとの折り合いをどうするかが一番迷いました。

完全な移行を行うためには、SplashActivityのロジックを各ActivityやViewModelに移行する必要があるので、
影響範囲を調べながら最適な移行をした方がいいと思いました。

弊社でも完全な移行に向けて色々調査・実装していきたいと思います。

また、SplashScreenにアニメーションを使用している場合は、AnimationDrawableでアプリアイコンを作成し、アニメーションの挙動も設定しなくてはいけないです。
ここら辺を触ることができれば、イケてるアプリに近づけそうです。

Discussion