From 05e7b0dc22dbd9d180e3410fdfa8c3fa1759e57d Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Sat, 17 Jul 2021 23:06:15 +0700 Subject: [PATCH] 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 --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 2 +- .../ui/browse/source/SourceController.kt | 5 + .../ui/library/LibraryCategoryView.kt | 6 + .../tachiyomi/ui/library/LibraryController.kt | 1 + .../kanade/tachiyomi/ui/main/MainActivity.kt | 106 +++++++++++++++++- .../ui/recent/history/HistoryController.kt | 5 + .../ui/recent/updates/UpdatesController.kt | 4 + .../tachiyomi/util/view/ViewExtensions.kt | 17 +++ ...ash_background.xml => ic_tachi_splash.xml} | 4 - app/src/main/res/values/themes.xml | 6 +- 11 files changed, 144 insertions(+), 13 deletions(-) rename app/src/main/res/drawable/{splash_background.xml => ic_tachi_splash.xml} (84%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index acb2f95d7..c8360823b 100644 --- a/app/build.gradle.kts +++ b/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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 852dabecf..2cb28ef48 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,7 +37,7 @@ + android:theme="@style/Theme.Tachiyomi.SplashScreen"> diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt index 02383e9fd..bc990cf73 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt +++ b/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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt index 2c419c462..c6ad42f58 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index ed8398189..312437477 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 66a02b5b5..031d89fc2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/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() { private var fixedViewsToBottom = mutableMapOf() + // 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() { } } - // 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() { .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() { } } + ready = true isHandlingShortcut = false return true } @@ -526,6 +615,11 @@ class MainActivity : BaseViewBindingActivity() { 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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt index a4dcd26e5..14347c9eb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt +++ b/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 + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt index edf04ede6..2a917c0b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt +++ b/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>) { destroyActionModeIfNeeded() adapter?.updateDataSet(chapters) + binding.recycler.onAnimationsFinished { + (activity as? MainActivity)?.ready = true + } } override fun onUpdateEmptyView(size: Int) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index cfd6a6d3d..d455d7f60 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/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) + } + } + } +) diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/ic_tachi_splash.xml similarity index 84% rename from app/src/main/res/drawable/splash_background.xml rename to app/src/main/res/drawable/ic_tachi_splash.xml index 761fbac44..3fdf1b6f5 100644 --- a/app/src/main/res/drawable/splash_background.xml +++ b/app/src/main/res/drawable/ic_tachi_splash.xml @@ -1,12 +1,8 @@ - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b784ce3d4..e1ae64e4b 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -178,8 +178,10 @@ -