Prechádzať zdrojové kódy

Fix splash screen icon on Android 12 (#5565)

* Use Core Splashscreen for splashscreen stuff

* Keep splash screen until activity ready

Ready as in the data inside starting screen is finished showing

* Use custom splash screen exit animation on older android version

* Add splash screen minimum duration to prevent exit jank

* Fix broken AMOLED theme

* Improvements
Ivan Iskandar 3 rokov pred
rodič
commit
05e7b0dc22

+ 1 - 0
app/build.gradle.kts

@@ -147,6 +147,7 @@ dependencies {
     implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02")
     implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
     implementation("androidx.core:core-ktx:1.7.0-alpha01")
+    implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
     implementation("androidx.multidex:multidex:2.0.1")
     implementation("androidx.preference:preference-ktx:1.1.1")
     implementation("androidx.recyclerview:recyclerview:1.2.1")

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -37,7 +37,7 @@
         <activity
             android:name=".ui.main.MainActivity"
             android:launchMode="singleTop"
-            android:theme="@style/Theme.Splash">
+            android:theme="@style/Theme.Tachiyomi.SplashScreen">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt

@@ -31,6 +31,8 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController
 import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
 import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.util.view.onAnimationsFinished
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
@@ -81,6 +83,9 @@ class SourceController :
         // Create recycler and set adapter.
         binding.recycler.layoutManager = LinearLayoutManager(view.context)
         binding.recycler.adapter = adapter
+        binding.recycler.onAnimationsFinished {
+            (activity as? MainActivity)?.ready = true
+        }
         adapter?.fastScroller = binding.fastScroller
 
         requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)

+ 6 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt

@@ -14,9 +14,11 @@ import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.library.LibraryUpdateService
 import eu.kanade.tachiyomi.data.preference.PreferencesHelper
 import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
+import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.lang.plusAssign
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.view.inflate
+import eu.kanade.tachiyomi.util.view.onAnimationsFinished
 import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.cancel
@@ -106,6 +108,10 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
             }
             .launchIn(scope)
 
+        recycler.onAnimationsFinished {
+            (controller.activity as? MainActivity)?.ready = true
+        }
+
         // Double the distance required to trigger sync
         binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
         binding.swipeRefresh.refreshes()

+ 1 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt

@@ -277,6 +277,7 @@ class LibraryController(
             binding.emptyView.hide()
         } else {
             binding.emptyView.show(R.string.information_empty_library)
+            (activity as? MainActivity)?.ready = true
         }
 
         // Get the current active category.

+ 100 - 6
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -1,18 +1,27 @@
 package eu.kanade.tachiyomi.ui.main
 
+import android.animation.ValueAnimator
 import android.app.SearchManager
 import android.content.Intent
+import android.graphics.Color
+import android.os.Build
 import android.os.Bundle
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
 import android.widget.Toast
 import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.animation.doOnEnd
+import androidx.core.splashscreen.SplashScreen
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator
+import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
 import androidx.lifecycle.lifecycleScope
 import androidx.preference.PreferenceDialogController
 import com.bluelinelabs.conductor.Conductor
@@ -49,6 +58,7 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
 import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchUI
+import eu.kanade.tachiyomi.util.system.dpToPx
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
 import kotlinx.coroutines.delay
@@ -80,7 +90,13 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
 
     private var fixedViewsToBottom = mutableMapOf<View, AppBarLayout.OnOffsetChangedListener>()
 
+    // To be checked by splash screen. If true then splash screen will be removed.
+    var ready = false
+
     override fun onCreate(savedInstanceState: Bundle?) {
+        // Prevent splash screen showing up on configuration changes
+        val splashScreen = if (savedInstanceState == null) installSplashScreen() else null
+
         super.onCreate(savedInstanceState)
 
         val didMigration = if (savedInstanceState == null) Migrations.upgrade(preferences) else false
@@ -114,13 +130,12 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
             }
         }
 
-        // Make sure navigation bar is on bottom before we modify it
-        ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
-            if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
-                window.setNavigationBarTransparentCompat(this)
-            }
-            insets
+        val startTime = System.currentTimeMillis()
+        splashScreen?.setKeepVisibleCondition {
+            val elapsed = System.currentTimeMillis() - startTime
+            elapsed <= SPLASH_MIN_DURATION || (!ready && elapsed <= SPLASH_MAX_DURATION)
         }
+        setSplashScreenExitAnimation(splashScreen)
 
         tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
 
@@ -255,6 +270,79 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
             .launchIn(lifecycleScope)
     }
 
+    /**
+     * Sets custom splash screen exit animation on devices prior to Android 12.
+     *
+     * When custom animation is used, status and navigation bar color will be set to transparent and will be restored
+     * after the animation is finished.
+     */
+    private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) {
+        val setNavbarScrim = {
+            // Make sure navigation bar is on bottom before we modify it
+            ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
+                if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
+                    window.setNavigationBarTransparentCompat(this@MainActivity)
+                }
+                insets
+            }
+            ViewCompat.requestApplyInsets(binding.root)
+        }
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+            val oldStatusColor = window.statusBarColor
+            val oldNavigationColor = window.navigationBarColor
+            window.statusBarColor = Color.TRANSPARENT
+            window.navigationBarColor = Color.TRANSPARENT
+
+            val wicc = WindowInsetsControllerCompat(window, window.decorView)
+            val isLightStatusBars = wicc.isAppearanceLightStatusBars
+            val isLightNavigationBars = wicc.isAppearanceLightNavigationBars
+            wicc.isAppearanceLightStatusBars = false
+            wicc.isAppearanceLightNavigationBars = false
+
+            splashScreen?.setOnExitAnimationListener { splashProvider ->
+                // For some reason the SplashScreen applies (incorrect) Y translation to the iconView
+                splashProvider.iconView.translationY = 0F
+
+                val activityAnim = ValueAnimator.ofFloat(1F, 0F).apply {
+                    interpolator = LinearOutSlowInInterpolator()
+                    duration = SPLASH_EXIT_ANIM_DURATION
+                    addUpdateListener { va ->
+                        val value = va.animatedValue as Float
+                        binding.root.translationY = value * 16.dpToPx
+                    }
+                }
+
+                var barColorRestored = false
+                val splashAnim = ValueAnimator.ofFloat(1F, 0F).apply {
+                    interpolator = FastOutSlowInInterpolator()
+                    duration = SPLASH_EXIT_ANIM_DURATION
+                    addUpdateListener { va ->
+                        val value = va.animatedValue as Float
+                        splashProvider.view.alpha = value
+
+                        if (!barColorRestored && value <= 0.5F) {
+                            barColorRestored = true
+                            wicc.isAppearanceLightStatusBars = isLightStatusBars
+                            wicc.isAppearanceLightNavigationBars = isLightNavigationBars
+                        }
+                    }
+                    doOnEnd {
+                        splashProvider.remove()
+                        window.statusBarColor = oldStatusColor
+                        window.navigationBarColor = oldNavigationColor
+                        setNavbarScrim()
+                    }
+                }
+
+                activityAnim.start()
+                splashAnim.start()
+            }
+        } else {
+            setNavbarScrim()
+        }
+    }
+
     override fun onNewIntent(intent: Intent) {
         if (!handleIntentAction(intent)) {
             super.onNewIntent(intent)
@@ -355,6 +443,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
             }
         }
 
+        ready = true
         isHandlingShortcut = false
         return true
     }
@@ -526,6 +615,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
         get() = binding.bottomNav ?: binding.sideNav!!
 
     companion object {
+        // Splash screen
+        private const val SPLASH_MIN_DURATION = 500 // ms
+        private const val SPLASH_MAX_DURATION = 5000 // ms
+        private const val SPLASH_EXIT_ANIM_DURATION = 400L // ms
+
         // Shortcut actions
         const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
         const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"

+ 5 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt

@@ -23,9 +23,11 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.ui.base.controller.RootController
 import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem
+import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.util.view.onAnimationsFinished
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -110,6 +112,9 @@ class HistoryController :
         } else {
             adapter?.onLoadMoreComplete(mangaHistory)
         }
+        binding.recycler.onAnimationsFinished {
+            (activity as? MainActivity)?.ready = true
+        }
     }
 
     /**

+ 4 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt

@@ -27,6 +27,7 @@ import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.system.notificationManager
 import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.util.view.onAnimationsFinished
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import reactivecircus.flowbinding.recyclerview.scrollStateChanges
@@ -224,6 +225,9 @@ class UpdatesController :
     fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
         destroyActionModeIfNeeded()
         adapter?.updateDataSet(chapters)
+        binding.recycler.onAnimationsFinished {
+            (activity as? MainActivity)?.ready = true
+        }
     }
 
     override fun onUpdateEmptyView(size: Int) {

+ 17 - 0
app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt

@@ -197,3 +197,20 @@ inline fun TextView.setMaxLinesAndEllipsize(_ellipsize: TextUtils.TruncateAt = T
     maxLines = (measuredHeight - paddingTop - paddingBottom) / lineHeight
     ellipsize = _ellipsize
 }
+
+/**
+ * Callback will be run immediately when no animation running
+ */
+fun RecyclerView.onAnimationsFinished(callback: (RecyclerView) -> Unit) = post(
+    object : Runnable {
+        override fun run() {
+            if (isAnimating) {
+                itemAnimator?.isRunning {
+                    post(this)
+                }
+            } else {
+                callback(this@onAnimationsFinished)
+            }
+        }
+    }
+)

+ 0 - 4
app/src/main/res/drawable/splash_background.xml → app/src/main/res/drawable/ic_tachi_splash.xml

@@ -1,12 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item android:drawable="@color/splash" />
-
     <item
         android:width="72dp"
         android:height="72dp"
         android:drawable="@drawable/ic_tachi"
         android:gravity="center" />
-
 </layer-list>

+ 4 - 2
app/src/main/res/values/themes.xml

@@ -178,8 +178,10 @@
     <!--===============-->
 
     <!--== Splash Theme ==-->
-    <style name="Theme.Splash" parent="Theme.Tachiyomi">
-        <item name="android:windowBackground">@drawable/splash_background</item>
+    <style name="Theme.Tachiyomi.SplashScreen" parent="Theme.SplashScreen">
+        <item name="windowSplashScreenAnimatedIcon">@drawable/ic_tachi_splash</item>
+        <item name="windowSplashScreenBackground">@color/splash</item>
+        <item name="postSplashScreenTheme">@style/Theme.Tachiyomi</item>
         <item name="android:statusBarColor">@android:color/transparent</item>
         <item name="android:navigationBarColor">@android:color/transparent</item>
         <item name="android:windowLightStatusBar">false</item>