Fix systemBar flickering on API 31
On API 31, if an OnExitAnimationListener is set, the Window layout params are only
applied only when the [android.window.SplashScreenView] is removed. This lead to some
flickers.
To fix this, we apply these attributes to the window while the
[android.window.SplashScreenView] is still visible.
Bug: 196273921
Test: SplashscreenParametrizedTest#endStateStableWithAndWithoutListener
Change-Id: I7e0593caba49563ccf06892df7f294e43ead7a5d
diff --git a/core/core-splashscreen/samples/src/main/res/drawable/vertical_line.xml b/core/core-splashscreen/samples/src/main/res/drawable/vertical_line.xml
new file mode 100644
index 0000000..01f4921
--- /dev/null
+++ b/core/core-splashscreen/samples/src/main/res/drawable/vertical_line.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<!-- Debug drawable to identify the top and bottom of the content view without coloring
+the whole background -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:width="40dp"
+ android:left="40dp">
+ <color android:color="#0000FF" />
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/samples/src/main/res/layout/main_activity.xml b/core/core-splashscreen/samples/src/main/res/layout/main_activity.xml
index e170fbe..a157632 100644
--- a/core/core-splashscreen/samples/src/main/res/layout/main_activity.xml
+++ b/core/core-splashscreen/samples/src/main/res/layout/main_activity.xml
@@ -19,6 +19,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
+ android:background="@drawable/vertical_line"
android:orientation="vertical">
<LinearLayout
diff --git a/core/core-splashscreen/src/androidTest/AndroidManifest.xml b/core/core-splashscreen/src/androidTest/AndroidManifest.xml
index e1fabc9..8414ca5 100644
--- a/core/core-splashscreen/src/androidTest/AndroidManifest.xml
+++ b/core/core-splashscreen/src/androidTest/AndroidManifest.xml
@@ -30,7 +30,7 @@
<activity
android:name=".SplashScreenAppCompatTestActivity"
android:exported="true"
- android:theme="@style/Theme.Test.Starting.AppCompat">
+ android:theme="@style/Theme.Test.AppCompat.Starting">
</activity>
<activity
@@ -39,5 +39,17 @@
android:theme="@style/Theme.Test.Starting.IconBackground">
</activity>
+ <activity
+ android:name=".SplashScreenStability1"
+ android:exported="true"
+ android:theme="@style/Theme.Test.Stability1.Starting">
+ </activity>
+
+ <activity
+ android:name=".SplashScreenStability2"
+ android:exported="true"
+ android:theme="@style/Theme.Test.Stability2.Starting">
+ </activity>
+
</application>
</manifest>
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt
index adf7e72..0173146 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestActivities.kt
@@ -18,6 +18,7 @@
import android.app.Activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
open class SplashScreenTestActivity : Activity(), SplashScreenTestControllerHolder {
@@ -43,3 +44,17 @@
override lateinit var controller: SplashScreenTestController
}
+
+class SplashScreenStability1 : SplashScreenTestActivity() {
+ override fun onStart() {
+ super.onStart()
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ }
+}
+
+class SplashScreenStability2 : SplashScreenTestActivity() {
+ override fun onStart() {
+ super.onStart()
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ }
+}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
index 3418529..3fe8b23 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
@@ -25,6 +25,8 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.screenshot.ScreenCapture
+import androidx.test.runner.screenshot.Screenshot
import androidx.test.screenshot.matchers.MSSIMMatcher
import androidx.test.uiautomator.UiDevice
import org.junit.Assert.assertEquals
@@ -160,6 +162,75 @@
}
/**
+ * Checks that activity and especially the system bars, have the same appearance whether we set
+ * an OnExitAnimationListener or not. This allows us to check that the system ui is stable
+ * before and after the removal of the SplashScreenView.
+ */
+ @Test
+ fun endStateStableWithAndWithoutListener() {
+ // Take a screenshot of the activity when no OnExitAnimationListener is set.
+ // This is our reference.
+ var controller = startActivityWithSplashScreen(SplashScreenStability1::class, device) {
+ // Clear out any previous instances
+ it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ it.putExtra(EXTRA_ANIMATION_LISTENER, false)
+ }
+ assertTrue(controller.drawnLatch.await(2, TimeUnit.SECONDS))
+ Thread.sleep(500)
+ val withoutListener = Screenshot.capture()
+
+ // Take a screenshot of the container view while the splash screen view is invisible but
+ // not removed
+ controller = startActivityWithSplashScreen(SplashScreenStability1::class, device) {
+ // Clear out any previous instances
+ it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ it.putExtra(EXTRA_ANIMATION_LISTENER, true)
+ it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
+ }
+ val withListener = screenshotContainerInExitListener(controller)
+
+ compareBitmaps(withoutListener.bitmap, withListener.bitmap, 0.999)
+
+ // Execute the same steps as above but with another set of theme attributes to check.
+ controller = startActivityWithSplashScreen(SplashScreenStability2::class, device) {
+ // Clear out any previous instances
+ it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ it.putExtra(EXTRA_ANIMATION_LISTENER, false)
+ }
+ controller.waitForActivityDrawn()
+ Thread.sleep(500)
+ val withoutListener2 = Screenshot.capture()
+
+ controller = startActivityWithSplashScreen(SplashScreenStability2::class, device) {
+ // Clear out any previous instances
+ it.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ it.putExtra(EXTRA_ANIMATION_LISTENER, true)
+ it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
+ }
+ val withListener2 = screenshotContainerInExitListener(controller)
+ compareBitmaps(withListener2.bitmap, withoutListener2.bitmap)
+ }
+
+ private fun screenshotContainerInExitListener(
+ controller: SplashScreenTestController
+ ): ScreenCapture {
+ lateinit var contentViewInListener: ScreenCapture
+ controller.doOnExitAnimation {
+ it.view.visibility = View.INVISIBLE
+ it.view.postDelayed({
+ contentViewInListener = Screenshot.capture()
+ it.remove()
+ controller.exitAnimationListenerLatch.countDown()
+ }, 100)
+ true
+ }
+ controller.waitBarrier.set(false)
+ controller.waitSplashScreenViewRemoved()
+ controller.activity.finishAndRemoveTask()
+ return contentViewInListener
+ }
+
+ /**
* The splash screen is drawn full screen. On Android 12, this is achieved using
* [Window.setDecorFitsSystemWindow(false)].
*/
@@ -203,7 +274,8 @@
private fun compareBitmaps(
beforeScreenshot: Bitmap,
- afterScreenshot: Bitmap
+ afterScreenshot: Bitmap,
+ threshold: Double = 0.99
) {
val beforeBuffer = IntArray(beforeScreenshot.width * beforeScreenshot.height)
beforeScreenshot.getPixels(
@@ -217,7 +289,7 @@
afterScreenshot.width, afterScreenshot.height
)
- val matcher = MSSIMMatcher(0.99).compareBitmaps(
+ val matcher = MSSIMMatcher(threshold).compareBitmaps(
beforeBuffer, afterBuffer, afterScreenshot.width,
afterScreenshot.height
)
@@ -285,4 +357,18 @@
activityClass, device, intentModifier
)
}
+
+ private fun SplashScreenTestController.waitForActivityDrawn() {
+ assertTrue(
+ "Activity was never drawn",
+ drawnLatch.await(2, TimeUnit.SECONDS)
+ )
+ }
+
+ private fun SplashScreenTestController.waitSplashScreenViewRemoved() {
+ assertTrue(
+ "Exit animation listener was not called",
+ exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
+ )
+ }
}
\ No newline at end of file
diff --git a/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml b/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml
index 6570777..b5010c1 100644
--- a/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml
+++ b/core/core-splashscreen/src/androidTest/res/layout/main_activity.xml
@@ -19,6 +19,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- android:background="#00FF00">
+ android:background="#FEDEDE">
</androidx.core.splashscreen.test.TestView>
diff --git a/core/core-splashscreen/src/androidTest/res/values/styles.xml b/core/core-splashscreen/src/androidTest/res/values/styles.xml
index 4a9e72b..7fabf0a 100644
--- a/core/core-splashscreen/src/androidTest/res/values/styles.xml
+++ b/core/core-splashscreen/src/androidTest/res/values/styles.xml
@@ -16,11 +16,28 @@
<resources>
- <style name="Theme.Test" parent="android:Theme.Material.Light.NoActionBar">
+ <!--
+ Two important point that we care about in the tests:
+ 1. That the SplashScreen View, used for the OnExitAnimationListener is the same as the system
+ provided splash screen window.
+ 2. That for the system bars, the transition between the system's theme and the app themes is
+ smooth and happens when the app is drawn.
+
+ Because handling of system bars colors differs between API, tests related to the SplashScreen
+ View and doing screenshot comparison are using themes with fixed system bar style, hence the
+ presence of the windowLight*Bar attribute.
+
+ The Stability* tests and associated themes are testing the expected behavior when the system
+ bars have different values on the splash screen theme and the app theme.
+ -->
+
+ <style name="Theme.Test" parent="android:Theme.DeviceDefault.Light.NoActionBar">
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:fitsSystemWindows">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ <item name="android:windowLightStatusBar">true</item>
</style>
<style name="Theme.Test.Starting" parent="Theme.SplashScreen">
@@ -28,6 +45,8 @@
<item name="windowSplashScreenAnimatedIcon">@drawable/android</item>
<item name="windowSplashScreenAnimationDuration">1234</item>
<item name="postSplashScreenTheme">@style/Theme.Test</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ <item name="android:windowLightStatusBar">true</item>
</style>
<style name="Theme.Test.Starting.IconBackground" parent="Theme.SplashScreen.IconBackground">
@@ -36,6 +55,8 @@
<item name="windowSplashScreenAnimationDuration">1234</item>
<item name="windowSplashScreenIconBackgroundColor">@color/icon_bg</item>
<item name="postSplashScreenTheme">@style/Theme.Test</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ <item name="android:windowLightStatusBar">true</item>
</style>
<!-- Themes for AppCompat Tests -->
@@ -44,13 +65,47 @@
<item name="android:fitsSystemWindows">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ <item name="android:windowLightStatusBar">true</item>
</style>
- <style name="Theme.Test.Starting.AppCompat" parent="Theme.SplashScreen">
+ <style name="Theme.Test.AppCompat.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/bg_launcher</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/android</item>
<item name="windowSplashScreenAnimationDuration">1234</item>
<item name="postSplashScreenTheme">@style/Theme.Test.AppCompat</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ <item name="android:windowLightStatusBar">true</item>
+ </style>
+
+ <!-- Themes for system ui stability -->
+ <style name="Theme.Test.Stability1.Starting" parent="Theme.SplashScreen">
+ <item name="postSplashScreenTheme">@style/Theme.Test.Stability1</item>
+ </style>
+
+ <style name="Theme.Test.Stability2.Starting" parent="Theme.SplashScreen">
+ <item name="postSplashScreenTheme">@style/Theme.Test.Stability2</item>
+ </style>
+
+ <style name="Theme.Test.Stability1" parent="android:Theme.Material.Light.NoActionBar">
+ <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+ <item name="android:fitsSystemWindows">false</item>
+ <item name="android:statusBarColor">#AAFF0000</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:enforceNavigationBarContrast">true</item>
+ <item name="android:enforceStatusBarContrast">true</item>
+ <item name="android:windowLightStatusBar">true</item>
+ <item name="android:windowLightNavigationBar">true</item>
+ </style>
+
+ <style name="Theme.Test.Stability2" parent="android:Theme.Material.Light.NoActionBar">
+ <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+ <item name="android:fitsSystemWindows">false</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">#AAFF0000</item>
+ <item name="android:enforceStatusBarContrast">true</item>
+ <item name="android:windowLightStatusBar">false</item>
+ <item name="android:windowLightNavigationBar">false</item>
</style>
</resources>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
index 1fc3ebe..ac95d97 100644
--- a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreen.kt
@@ -16,6 +16,7 @@
package androidx.core.splashscreen
+import android.R.attr
import android.annotation.SuppressLint
import android.app.Activity
import android.content.res.Resources
@@ -29,10 +30,12 @@
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnPreDrawListener
import android.view.WindowInsets
+import android.view.WindowManager.LayoutParams
import android.widget.ImageView
import android.window.SplashScreenView
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.splashscreen.SplashScreen.KeepOnScreenCondition
/**
@@ -456,11 +459,51 @@
}
}
+ /**
+ * Apply the system ui related theme attribute defined in the application to override the
+ * ones set on the [SplashScreenView]
+ *
+ * On API 31, if an OnExitAnimationListener is set, the Window layout params are only
+ * applied only when the [SplashScreenView] is removed. This lead to some
+ * flickers.
+ *
+ * To fix this, we apply these attributes as soon as the [SplashScreenView]
+ * is visible.
+ */
private fun applyAppSystemUiTheme() {
+ val tv = TypedValue()
+ val theme = activity.theme
val window = activity.window
+ if (theme.resolveAttribute(attr.statusBarColor, tv, true)) {
+ window.statusBarColor = tv.data
+ }
+
+ if (theme.resolveAttribute(attr.navigationBarColor, tv, true)) {
+ window.navigationBarColor = tv.data
+ }
+
+ if (theme.resolveAttribute(attr.windowDrawsSystemBarBackgrounds, tv, true)) {
+ if (tv.data != 0) {
+ window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ } else {
+ window.clearFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ }
+ }
+
+ if (theme.resolveAttribute(attr.enforceNavigationBarContrast, tv, true)) {
+ window.isNavigationBarContrastEnforced = tv.data != 0
+ }
+
+ if (theme.resolveAttribute(attr.enforceStatusBarContrast, tv, true)) {
+ window.isStatusBarContrastEnforced = tv.data != 0
+ }
+
+ val decorView = window.decorView as ViewGroup
+ ThemeUtils.Api31.applyThemesSystemBarAppearance(theme, decorView, tv)
+
// Fix setDecorFitsSystemWindows being overridden by the SplashScreenView
- (activity.window.decorView as ViewGroup).setOnHierarchyChangeListener(null)
+ decorView.setOnHierarchyChangeListener(null)
window.setDecorFitsSystemWindows(mDecorFitWindowInsets)
}
}
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
index a0c4e5b..1eae14c 100644
--- a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/SplashScreenViewProvider.kt
@@ -132,6 +132,12 @@
override val iconAnimationDurationMillis: Long
get() = platformView.iconAnimationDuration?.toMillis() ?: 0
- override fun remove() = platformView.remove()
+ override fun remove() {
+ platformView.remove()
+ ThemeUtils.Api31.applyThemesSystemBarAppearance(
+ activity.theme,
+ activity.window.decorView
+ )
+ }
}
}
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/java/androidx/core/splashscreen/ThemeUtils.kt b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/ThemeUtils.kt
new file mode 100644
index 0000000..66d08c6
--- /dev/null
+++ b/core/core-splashscreen/src/main/java/androidx/core/splashscreen/ThemeUtils.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.splashscreen
+
+import android.content.res.Resources
+import android.util.TypedValue
+import android.view.View
+import android.view.WindowInsetsController
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+
+/**
+ * Utility function for applying themes fixes to the system bar when the [SplashScreenViewProvider]
+ * is added and removed.
+ */
+@RequiresApi(31)
+internal object ThemeUtils {
+ object Api31 {
+
+ /**
+ * Apply the theme's values for the system bar appearance to the decorView.
+ *
+ * This needs to be called when the [SplashScreenViewProvider] is added and after it's been
+ * removed.
+ */
+ @JvmStatic
+ @JvmOverloads
+ @DoNotInline
+ fun applyThemesSystemBarAppearance(
+ theme: Resources.Theme,
+ decor: View,
+ tv: TypedValue = TypedValue()
+ ) {
+ var appearance = 0
+ val mask =
+ WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or
+ WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
+ if (theme.resolveAttribute(android.R.attr.windowLightStatusBar, tv, true)) {
+ if (tv.data != 0) {
+ appearance = appearance or WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+ }
+ }
+ if (theme.resolveAttribute(android.R.attr.windowLightNavigationBar, tv, true)) {
+ if (tv.data != 0) {
+ appearance =
+ appearance or WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
+ }
+ }
+ decor.windowInsetsController!!.setSystemBarsAppearance(appearance, mask)
+ }
+ }
+}
\ No newline at end of file