From 586e3667c6ec77c49d3c0298082ed8da3c904e75 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sat, 23 Apr 2022 18:41:18 -0400 Subject: [PATCH] Staggered grid layout + Titleless grid option (#1198) * Most of the work for big toolbar * minor fixes to library * Using accurate offset for library * nav icon fixes * support tabs scrolling properly * fix toolbar snap * Add big icon next to large title * Update MainActivity.kt * Update browse sheet toolbar to be its own * Update recent sheet toolbar to be its own * fix pop from manga details * cleanup of BigAppBarLayout * Use LinearLayoutManagerAccurateOffset for recents * Updates to searchview * fix recents scrolling up on device config change * Move search view up to top on tap * Fix to recent and library searches * Fix multi line big titles * Fix config change resetting recycler scroll * add end padding to big title * More fixes * Fixes to non scrollable appbar controllers * Move swipe refresh circle with view * Fix manga details toolbar * Updates to recents and browse source when config changes * Fixes to global search * Fixes to popping from manga details * clear search when switching controllers + fixes to global and source search * Update search.xml * update backgroundColor * fixes to big appbar background on push * use canShowFloatingToolbar where possible * fix collapse/hide of dl bottom sheet * persist search on config change * Fix library menu on config change * Fix recents toolbar placement while searching * adjust calc of grid layout manager offset * Fixes to range calculation in library * More accurate scrollbar in manga details * use item animator for moving appbar * alpha on main tb * accurate offset for settings * updates to search activity * Fixes to back button and up button * Option to go back to smaller toolbar * Optimization to fast computing offset * moving scrollbar in library as app bar moves * fixes to search title in recents * fix popping into browse after 2 line big titles * Updates to landscape and tablets Tablets now always have a sticky header, just shrinks down Phones in landscape will have a smaller big title * fixes to main toolbar text alpha * Fixes to device config changes * remove updateOnRefresh preference big toolbar now, you can just reach the first refresh button * Fix category hopper * Fix recents in small toolbar mode * clean up and fixes * Fixes to back press + search activity * Fixes to going in and back out of manga details * Optimizations to grid layout calculations * adjustments to library fast scroller margins * Fixes to download queue overflow menu * Fixes to library search when popping * Fixes to d/l sheet on launch * expanding library search * remove includeTabView as parameter * remove sheetIsFullscreen * fixes to bigToolbarHeight * add onDetachedFromWindow to LinearLayoutManagerAccurateOffset * add scrollUpAnyway to moveRecyclerViewUp * snap swipe refresh circle with appbar * Fixes to extension searching * accurate offset use in ExtensionDetailsController * alpha big view adjustments * Fixes to MigrationController * Dont change toolbar menus while scrolling down * staggered grid * titleless grid + plenty of other fixes * fix top scrolls for staggered managers * fix disappearing appbar in some cases * save cover ratios less often * Cleanup GlobalSearchController searchItem * Cleanup SettingsMainController searchItem * Update LibraryController.kt * change onRoot to isControllerVisible and add to controller extensions * lock appbar y when switching tabs * Dont use large toolbar on extremely small devices Basically devices in landscape that cant display nav rails * Fixes to progress view and empty view in landscape new layout for empty view for short devices * keep recycler pos when switching between staggered and regular * move manager offsets to new files * move StaggeredGridLayoutManagerAccurateOffset to new file * Move MangaCoverRatios to another file * setItem -> saveStaggeredState * cleanup * fix indent in manga_grid_item * Use dominant color as a placeholder cover in library * fix for ungrouped library mode * clean up search items in BrowseSourceController * dont show incog icon on main toolbar when search bar is showing * fix browse source having the grid be offset when popping * Fixes to drag and drop for staggered/non * remove mapping for manga not in library * set min height for grid layout when ratio is provided * Fixes to estimated heights of viewholders * rename large toolbar setting to expanded also update setting copy to mention small devices will never show it * BigAppBarLayout -> ExpandedAppBarLayout * save a var for computedRange to ease the calls a bit * fix crash * Fix span being wrong at first on auto fit recycler * Fixes to grid gradient * fixed behind title showing in compact grid * Move color processing to manga fetcher + own scope * Fix staggered not working on compact grid * More fixes to grid gradient * Cleanup BaseToolbar * Refactoring ExpandedAppBarLayout * misc cleanup * use updateAppBarAfterY where needed * Rename ToolbarStates * refactor toolbar height variable names * More refactoring to MainActivity * helper method for setTextColorAlpha * Refactor MangaDetailsController * Refactor Recents * Refactoring settings controllers * Cleanup Browse controller * Remove Float.spToPx its not used * Clean up ControllerExtensions * clean up import in BrowseController * Use keys for the cover preferences * rename methods in MangaFetcher * minor refactor in LibraryController * Add migration method to save covers and ratios of library manga on app update * MangaCoverRatios -> MangaCoverMetadata * Cleanup LibraryItem * Titleless Grid -> Cover-only grid Going to try this as a different key since it seems like weblate still grabs the string even with different kets from upstream * Use constants for library layouts * Use snackbar to show the title of cover only grid when long pressing * move itemanimator to a different method same change was already made in staggered branch * Fixes to browse source toolbar not being expanded * cleanup ControllerExtensions.setAppBarBG * Mainactivity cleanup * Optimizations + fixes to setting menu items in search toolbar * Fixes to first load of library with staggered grid * Fix grid items in migration controller * dismiss about manga title snackbar when selecting again * use measured width for temp span * Fixes to switching between grid/list in browse source with correct offsetting and all * Fix findFirstVisibleItemPosition for offset layoutmanagers * remove unneeded invalidateOptionsMenu calls * More fixes to search toolbar menu in browse source * filter sources controller now uses search toolbar too when using expanded mode sticks back to main toolbar when collapsed (ie like older version of j2k) * Fix compact -> expanded setting change when starting app in compact mode * Fix starting span of autofit recycler * fix how findFirstVisibleItemPosition works in staggered grid * More fixes to BrowseSourceController * Fixes to LibraryController search menu * Filter sources search title is now just "search" because not gonna add a new string for this * Fix statusbar color when popping back to ext sheet * Make adding submenu items recursive * Fix appbar locking up in recents * Fix download/ext sheet in landscape 3 nav devices * Fixing to snapping on compact toolbars * Fix search toolbar menu flashing so much on change * Fix animation time of snapping compact appbar * Fixes to toolbar for tablets * Cleanup * Fix controller navigation when sidebar shows/hides Removing the logic for the sidenav fade out change handling too now In this branch because it made the staggered stuff wonky Fixes to last commit * Reduce decoding fullbitmap for covermetadata when possible * Minor updates to coil setup allow hardware + disable rgb565 on low ram devices * better handling of menu item in search toolbar * minimize use of obtainStyledAttributes of mainActionBarSize * reorder import * some staggered fixes to the appbar in library * use staggered grid pref key directly --- app/src/main/java/eu/kanade/tachiyomi/App.kt | 2 + .../java/eu/kanade/tachiyomi/Migrations.kt | 7 +- .../tachiyomi/data/database/models/Manga.kt | 8 + .../tachiyomi/data/image/coil/CoilSetup.kt | 7 +- .../image/coil/LibraryMangaImageTarget.kt | 13 -- .../tachiyomi/data/image/coil/MangaFetcher.kt | 51 +++++++ .../data/preference/PreferenceKeys.kt | 4 + .../data/preference/PreferencesHelper.kt | 9 +- .../controller/OneWayFadeChangeHandler.kt | 4 +- .../tachiyomi/ui/library/LibraryController.kt | 139 ++++++++++++------ .../tachiyomi/ui/library/LibraryGridHolder.kt | 58 +++++++- .../tachiyomi/ui/library/LibraryHeaderItem.kt | 3 + .../tachiyomi/ui/library/LibraryItem.kt | 56 ++++--- .../tachiyomi/ui/library/LibraryPresenter.kt | 12 ++ .../tachiyomi/ui/library/SearchGlobalItem.kt | 3 + .../ui/library/display/LibraryDisplayView.kt | 8 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 33 ++++- .../manga/process/MigrationProcessHolder.kt | 4 + .../ui/source/browse/BrowseSourceItem.kt | 8 +- .../util/manga/MangaCoverMetadata.kt | 75 ++++++++++ .../util/view/ControllerExtensions.kt | 4 + .../tachiyomi/util/view/ViewExtensions.kt | 13 +- .../tachiyomi/widget/AutofitRecyclerView.kt | 96 +++++++++++- ...taggeredGridLayoutManagerAccurateOffset.kt | 56 +++++++ .../res/layout/library_display_layout.xml | 13 ++ app/src/main/res/layout/manga_grid_item.xml | 64 ++++++-- app/src/main/res/values/strings.xml | 2 + 27 files changed, 632 insertions(+), 120 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/widget/StaggeredGridLayoutManagerAccurateOffset.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index cd44647b92..a872587681 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.ui.library.LibraryPresenter import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.source.SourcePresenter +import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.system.AuthenticatorUtil import eu.kanade.tachiyomi.util.system.notification import kotlinx.coroutines.flow.launchIn @@ -78,6 +79,7 @@ open class App : Application(), DefaultLifecycleObserver { ProcessLifecycleOwner.get().lifecycle.addObserver(this) + MangaCoverMetadata.load() preferences.nightMode() .asImmediateFlow { AppCompatDelegate.setDefaultNightMode(it) } .launchIn(ProcessLifecycleOwner.get().lifecycleScope) diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index c527c8b97c..1624fae0c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -15,7 +15,9 @@ import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE import eu.kanade.tachiyomi.ui.library.LibraryPresenter import eu.kanade.tachiyomi.ui.reader.settings.OrientationType +import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.CoroutineScope import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File @@ -28,7 +30,7 @@ object Migrations { * @param preferences Preferences of the application. * @return true if a migration is performed, false otherwise. */ - fun upgrade(preferences: PreferencesHelper): Boolean { + fun upgrade(preferences: PreferencesHelper, scope: CoroutineScope): Boolean { val context = preferences.context val prefs = PreferenceManager.getDefaultSharedPreferences(context) prefs.edit { @@ -184,6 +186,9 @@ object Migrations { } } if (oldVersion < 88) { + scope.launchIO { + LibraryPresenter.updateRatiosAndColors() + } val oldReaderTap = prefs.getBoolean("reader_tap", false) if (!oldReaderTap) { preferences.navigationModePager().set(5) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 4b0c3e921d..5b98c1104f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.reader.settings.OrientationType import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType +import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import tachiyomi.source.model.MangaInfo import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -275,6 +276,13 @@ interface Manga : SManga { id?.let { vibrantCoverColorMap[it] = value } } + var dominantCoverColors: Pair? + get() = MangaCoverMetadata.getColors(this) + set(value) { + value ?: return + MangaCoverMetadata.addCoverColor(this, value.first, value.second) + } + companion object { // Generic filter that does not filter anything diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/CoilSetup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/CoilSetup.kt index 95d355bffb..d3d2173913 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/CoilSetup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/CoilSetup.kt @@ -1,7 +1,10 @@ package eu.kanade.tachiyomi.data.image.coil +import android.app.ActivityManager import android.content.Context import android.os.Build +import androidx.core.content.ContextCompat.getSystemService +import androidx.core.content.getSystemService import coil.Coil import coil.ImageLoader import coil.decode.GifDecoder @@ -16,8 +19,8 @@ class CoilSetup(context: Context) { val imageLoader = ImageLoader.Builder(context) .availableMemoryPercentage(0.40) .crossfade(true) - .allowRgb565(true) - .allowHardware(false) + .allowRgb565(context.getSystemService()!!.isLowRamDevice) + .allowHardware(true) .componentRegistry { if (Build.VERSION.SDK_INT >= 28) { add(ImageDecoderDecoder(context)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/LibraryMangaImageTarget.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/LibraryMangaImageTarget.kt index b8a5c207f7..6fa7f450c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/LibraryMangaImageTarget.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/LibraryMangaImageTarget.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.image.coil import android.graphics.BitmapFactory -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.widget.ImageView import androidx.palette.graphics.Palette @@ -23,18 +22,6 @@ class LibraryMangaImageTarget( private val coverCache: CoverCache by injectLazy() - override fun onSuccess(result: Drawable) { - super.onSuccess(result) - if (manga.vibrantCoverColor == null) { - val bitmap = (drawable as? BitmapDrawable)?.bitmap ?: return - Palette.from(bitmap).generate { - if (it == null) return@generate - val color = it.getBestColor() ?: return@generate - manga.vibrantCoverColor = color - } - } - } - override fun onError(error: Drawable?) { super.onError(error) if (manga.favorite) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaFetcher.kt index a1ade28011..2fbe3c5fad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/image/coil/MangaFetcher.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.image.coil +import android.graphics.BitmapFactory import android.webkit.MimeTypeMap +import androidx.palette.graphics.Palette import coil.bitmap.BitmapPool import coil.decode.DataSource import coil.decode.Options @@ -14,7 +16,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.storage.DiskUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import okhttp3.CacheControl import okhttp3.Call import okhttp3.Request @@ -40,6 +47,7 @@ class MangaFetcher : Fetcher { private val coverCache: CoverCache by injectLazy() private val sourceManager: SourceManager by injectLazy() private val defaultClient = Injekt.get().client + val fileScope = CoroutineScope(Job() + Dispatchers.IO) override fun key(data: Manga): String? { if (data.thumbnail_url.isNullOrBlank()) return null @@ -65,6 +73,7 @@ class MangaFetcher : Fetcher { if (!shouldFetchRemotely) { val customCoverFile = coverCache.getCustomCoverFile(manga) if (customCoverFile.exists() && options.parameters.value(realCover) != true) { + setRatioAndColorsInScope(manga, customCoverFile) return fileLoader(customCoverFile) } } @@ -73,6 +82,7 @@ class MangaFetcher : Fetcher { if (!manga.favorite) { coverFile.setLastModified(Date().time) } + setRatioAndColorsInScope(manga, coverFile) return fileLoader(coverFile) } val (response, body) = awaitGetCall( @@ -104,9 +114,50 @@ class MangaFetcher : Fetcher { coverCache.deleteCachedCovers() } } + setRatioAndColorsInScope(manga, coverFile, true) return fileLoader(coverFile) } + private fun setRatioAndColorsInScope(manga: Manga, ogFile: File? = null, force: Boolean = false) { + fileScope.launch { + setRatioAndColors(manga, ogFile, force) + } + } + + fun setRatioAndColors(manga: Manga, ogFile: File? = null, force: Boolean = false) { + if (!manga.favorite) { + MangaCoverMetadata.remove(manga) + } + if (manga.vibrantCoverColor != null && !manga.favorite) return + val file = ogFile ?: coverCache.getCustomCoverFile(manga).takeIf { it.exists() } ?: coverCache.getCoverFile(manga) + // if the file exists and the there was still an error then the file is corrupted + if (file.exists()) { + val options = BitmapFactory.Options() + val hasVibrantColor = if (manga.favorite) manga.vibrantCoverColor != null else true + if (manga.dominantCoverColors != null && hasVibrantColor && !force) { + options.inJustDecodeBounds = true + } else { + options.inSampleSize = 4 + } + val bitmap = BitmapFactory.decodeFile(file.path, options) ?: return + if (!options.inJustDecodeBounds) { + Palette.from(bitmap).generate { + if (it == null) return@generate + if (manga.favorite) { + it.dominantSwatch?.let { swatch -> + manga.dominantCoverColors = swatch.rgb to swatch.titleTextColor + } + } + val color = it.getBestColor() ?: return@generate + manga.vibrantCoverColor = color + } + } + if (manga.favorite && !(options.outWidth == -1 || options.outHeight == -1)) { + MangaCoverMetadata.addCoverRatio(manga, options.outWidth / options.outHeight.toFloat()) + } + } + } + private suspend fun awaitGetCall(manga: Manga, onlyCache: Boolean = false, forceNetwork: Boolean): Pair { val call = getCall(manga, onlyCache, forceNetwork) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 95732af01a..614e693550 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -253,6 +253,10 @@ object PreferenceKeys { const val defaultChapterSortByAscendingOrDescending = "default_chapter_sort_by_ascending_or_descending" + const val coverRatios = "cover_ratio" + + const val coverColors = "cover_colors" + const val hideChapterTitles = "hide_chapter_titles" const val chaptersDescAsDefault = "chapters_desc_as_default" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 5371c1151a..4c329ff477 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder +import eu.kanade.tachiyomi.ui.library.LibraryItem import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet import eu.kanade.tachiyomi.ui.reader.settings.OrientationType import eu.kanade.tachiyomi.ui.reader.settings.PageLayout @@ -263,7 +264,7 @@ class PreferencesHelper(val context: Context) { fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0) - fun libraryLayout() = flowPrefs.getInt(Keys.libraryLayout, 2) + fun libraryLayout() = flowPrefs.getInt(Keys.libraryLayout, LibraryItem.LAYOUT_COMFORTABLE_GRID) fun gridSize() = flowPrefs.getFloat(Keys.gridSize, 1f) @@ -451,4 +452,10 @@ class PreferencesHelper(val context: Context) { fun chaptersDescAsDefault() = flowPrefs.getBoolean(Keys.chaptersDescAsDefault, true) fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC) + + fun coverRatios() = flowPrefs.getStringSet(Keys.coverRatios, emptySet()) + + fun coverColors() = flowPrefs.getStringSet(Keys.coverColors, emptySet()) + + fun useStaggeredGrid() = flowPrefs.getBoolean("use_staggered_grid", false) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt index 27a3398c90..17a7cd16c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/OneWayFadeChangeHandler.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.base.controller import android.animation.Animator import android.animation.AnimatorSet import android.animation.ObjectAnimator -import android.content.res.Configuration import android.view.View import android.view.ViewGroup import com.bluelinelabs.conductor.ControllerChangeHandler @@ -35,9 +34,8 @@ class OneWayFadeChangeHandler : FadeChangeHandler { animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f)) } - val hasSideNav = container.context.resources.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE if (from != null && (!isPush || removesFromViewOnPush())) { - if (!hasSideNav && fadeOut) { + if (fadeOut) { animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0f)) } else { container.removeView(from) 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 61c66c456f..67a11805db 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 @@ -9,6 +9,7 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle +import android.os.Parcelable import android.util.TypedValue import android.view.Gravity import android.view.LayoutInflater @@ -19,6 +20,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.ViewPropertyAnimator +import android.view.ViewTreeObserver import android.view.inputmethod.InputMethodManager import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode @@ -37,8 +39,8 @@ import androidx.core.view.updatePadding import androidx.core.view.updatePaddingRelative import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import com.fredporciuncula.flow.preferences.Preference @@ -227,6 +229,8 @@ class LibraryController( override val mainRecycler: RecyclerView get() = binding.libraryGridRecycler.recycler + var staggeredBundle: Parcelable? = null + private var staggeredObserver: ViewTreeObserver.OnGlobalLayoutListener? = null override fun getTitle(): String? { setSubtitle() @@ -318,6 +322,18 @@ class LibraryController( updateHopperPosition() } } + if (newState != RecyclerView.SCROLL_STATE_IDLE) { + removeStaggeredObserver() + } + } + } + + private fun removeStaggeredObserver() { + if (staggeredObserver != null) { + binding.libraryGridRecycler.recycler.viewTreeObserver.removeOnGlobalLayoutListener( + staggeredObserver + ) + staggeredObserver = null } } @@ -517,19 +533,6 @@ class LibraryController( adapter = LibraryCategoryAdapter(this) adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY setRecyclerLayout() - binding.libraryGridRecycler.recycler.manager.spanSizeLookup = ( - object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - if (libraryLayout == 0) return binding.libraryGridRecycler.recycler.manager.spanCount - val item = this@LibraryController.adapter.getItem(position) - return if (item is LibraryHeaderItem || item is SearchGlobalItem || (item is LibraryItem && item.manga.isBlank())) { - binding.libraryGridRecycler.recycler.manager.spanCount - } else { - 1 - } - } - } - ) binding.libraryGridRecycler.recycler.setHasFixedSize(true) binding.libraryGridRecycler.recycler.adapter = adapter @@ -798,8 +801,7 @@ class LibraryController( val category = getVisibleHeader() ?: return if (presenter.showAllCategories) { if (!next) { - val fPosition = - (binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + val fPosition = binding.libraryGridRecycler.recycler.findFirstVisibleItemPosition() if (fPosition > adapter.currentItems.indexOf(category)) { scrollToHeader(category.category.order) return @@ -834,7 +836,7 @@ class LibraryController( private fun getHeader(firstCompletelyVisible: Boolean = false): LibraryHeaderItem? { val position = if (firstCompletelyVisible) { - (binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + binding.libraryGridRecycler.recycler.findFirstCompletelyVisibleItemPosition() } else { -1 } @@ -844,8 +846,7 @@ class LibraryController( is LibraryItem -> return item.header } } else { - val fPosition = - (binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + val fPosition = binding.libraryGridRecycler.recycler.findFirstVisibleItemPosition() when (val item = adapter.getItem(fPosition)) { is LibraryHeaderItem -> return item is LibraryItem -> return item.header @@ -855,8 +856,7 @@ class LibraryController( } private fun getVisibleHeader(): LibraryHeaderItem? { - val fPosition = - (binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + val fPosition = binding.libraryGridRecycler.recycler.findFirstVisibleItemPosition() when (val item = adapter.getItem(fPosition)) { is LibraryHeaderItem -> return item is LibraryItem -> return item.header @@ -897,7 +897,8 @@ class LibraryController( bottom = 50.dpToPx + (activityBinding?.bottomNav?.height ?: 0) ) } - if (libraryLayout == 0) { + useStaggered(preferences) + if (libraryLayout == LibraryItem.LAYOUT_LIST) { spanCount = 1 updatePaddingRelative( start = 0, @@ -910,6 +911,17 @@ class LibraryController( end = 5.dpToPx ) } + (manager as? GridLayoutManager)?.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + if (libraryLayout == LibraryItem.LAYOUT_LIST) return managerSpanCount + val item = this@LibraryController.adapter.getItem(position) + return if (item is LibraryHeaderItem || item is SearchGlobalItem || (item is LibraryItem && item.manga.isBlank())) { + managerSpanCount + } else { + 1 + } + } + } } } @@ -917,7 +929,8 @@ class LibraryController( listOf( preferences.libraryLayout(), preferences.uniformGrid(), - preferences.gridSize() + preferences.gridSize(), + preferences.useStaggeredGrid() ).forEach { it.asFlow() .drop(1) @@ -970,7 +983,12 @@ class LibraryController( } } } + if (binding.libraryGridRecycler.recycler.manager is StaggeredGridLayoutManager && staggeredBundle != null) { + binding.libraryGridRecycler.recycler.manager.onRestoreInstanceState(staggeredBundle) + staggeredBundle = null + } } else { + saveStaggeredState() updateFilterSheetY() closeTip() if (binding.filterBottomSheet.filterBottomSheet.sheetBehavior.isHidden()) { @@ -1003,6 +1021,7 @@ class LibraryController( } displaySheet?.dismiss() displaySheet = null + saveStaggeredState() super.onDestroyView(view) } @@ -1046,6 +1065,29 @@ class LibraryController( } } } + + if (binding.libraryGridRecycler.recycler.manager is StaggeredGridLayoutManager && isControllerVisible) { + staggeredObserver = ViewTreeObserver.OnGlobalLayoutListener { + binding.libraryGridRecycler.recycler.postOnAnimation { + if (!isControllerVisible) return@postOnAnimation + scrollToHeader(activeC, false) + activityBinding?.appBar?.y = 0f + activityBinding?.appBar?.updateAppBarAfterY(binding.libraryGridRecycler.recycler) + if (activeC > 0) { + activityBinding?.appBar?.useSearchToolbarForMenu(true) + } + } + } + binding.libraryGridRecycler.recycler.viewTreeObserver.addOnGlobalLayoutListener(staggeredObserver) + viewScope.launchUI { + delay(500) + removeStaggeredObserver() + if (!isControllerVisible) return@launchUI + if (activeC > 0) { + activityBinding?.appBar?.useSearchToolbarForMenu(true) + } + } + } } if (isControllerVisible) { activityBinding?.appBar?.lockYPos = false @@ -1155,7 +1197,10 @@ class LibraryController( } } - private fun scrollToHeader(pos: Int) { + private fun scrollToHeader(pos: Int, removeObserver: Boolean = true) { + if (removeObserver) { + removeStaggeredObserver() + } if (!presenter.showAllCategories) { presenter.switchSection(pos) activeCategory = pos @@ -1168,7 +1213,7 @@ class LibraryController( val activityBinding = activityBinding ?: return val appbarOffset = if (pos <= 0) 0 else -fullAppBarHeight!! + activityBinding.cardFrame.height val previousHeader = adapter.getItem(adapter.indexOf(pos - 1)) as? LibraryHeaderItem - (binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( + binding.libraryGridRecycler.recycler.scrollToPositionWithOffset( headerPosition, ( when { @@ -1209,14 +1254,9 @@ class LibraryController( private fun reattachAdapter() { libraryLayout = preferences.libraryLayout().get() setRecyclerLayout() - val position = - (binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + val position = binding.libraryGridRecycler.recycler.findFirstVisibleItemPosition() binding.libraryGridRecycler.recycler.adapter = adapter - - (binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - position, - 0 - ) + binding.libraryGridRecycler.recycler.scrollToPositionWithOffset(position, 0) } fun search(query: String?): Boolean { @@ -1333,6 +1373,7 @@ class LibraryController( override fun onItemClick(view: View?, position: Int): Boolean { val item = adapter.getItem(position) as? LibraryItem ?: return false return if (adapter.mode == SelectableAdapter.Mode.MULTI) { + snack?.dismiss() lastClickPosition = position toggleSelection(position) false @@ -1342,11 +1383,15 @@ class LibraryController( } } - private fun openManga(manga: Manga) = router.pushController( - MangaDetailsController( - manga - ).withFadeTransaction() - ) + private fun saveStaggeredState() { + if (binding.libraryGridRecycler.recycler.manager is StaggeredGridLayoutManager) { + staggeredBundle = binding.libraryGridRecycler.recycler.manager.onSaveInstanceState() + } + } + + private fun openManga(manga: Manga) { + router.pushController(MangaDetailsController(manga).withFadeTransaction()) + } /** * Called when a manga is long clicked. @@ -1354,7 +1399,15 @@ class LibraryController( * @param position the position of the element clicked. */ override fun onItemLongClick(position: Int) { - if (adapter.getItem(position) !is LibraryItem) return + val item = adapter.getItem(position) + if (item !is LibraryItem) return + snack?.dismiss() + if (libraryLayout == LibraryItem.LAYOUT_COVER_ONLY_GRID && actionMode == null) { + snack = view?.snack(item.manga.title) { + anchorView = activityBinding?.bottomNav + view.elevation = 15f.dpToPx + } + } createActionModeIfNeeded() when { lastClickPosition == -1 -> setSelection(position) @@ -1409,11 +1462,11 @@ class LibraryController( override fun onItemMove(fromPosition: Int, toPosition: Int) { // Because padding a recycler causes it to scroll up we have to scroll it back down... wild - if (( - adapter.getItem(fromPosition) is LibraryItem && - adapter.getItem(fromPosition) is LibraryItem - ) || - adapter.getItem(fromPosition) == null + val fromItem = adapter.getItem(fromPosition) + val toItem = adapter.getItem(toPosition) + if (binding.libraryGridRecycler.recycler.layoutManager !is StaggeredGridLayoutManager && ( + (fromItem is LibraryItem && toItem is LibraryItem) || fromItem == null + ) ) { binding.libraryGridRecycler.recycler.scrollBy( 0, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt index c204e937f4..388ea5b787 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryGridHolder.kt @@ -3,17 +3,25 @@ package eu.kanade.tachiyomi.ui.library import android.app.Activity import android.view.Gravity import android.view.View +import android.view.ViewGroup import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import coil.clear import coil.size.Precision import coil.size.Scale +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.databinding.MangaGridItemBinding import eu.kanade.tachiyomi.util.lang.highlightText +import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.backgroundColor import eu.kanade.tachiyomi.util.view.setCards +import eu.kanade.tachiyomi.widget.AutofitRecyclerView /** * Class used to hold the displayed data of a manga in the library, like the cover or the title. @@ -27,9 +35,8 @@ import eu.kanade.tachiyomi.util.view.setCards class LibraryGridHolder( private val view: View, adapter: LibraryCategoryAdapter, - var width: Int, compact: Boolean, - private var fixedSize: Boolean + val fixedSize: Boolean ) : LibraryHolder(view, adapter) { private val binding = MangaGridItemBinding.bind(view) @@ -60,6 +67,12 @@ class LibraryGridHolder( setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root) binding.constraintLayout.isVisible = !item.manga.isBlank() binding.title.text = item.manga.title.highlightText(item.filter, color) + binding.behindTitle.text = item.manga.title + val mangaColor = item.manga.dominantCoverColors + binding.coverConstraint.backgroundColor = mangaColor?.first ?: itemView.context.getResourceColor(R.attr.background) + binding.behindTitle.setTextColor( + mangaColor?.second ?: itemView.context.getResourceColor(R.attr.colorOnBackground) + ) val authorArtist = if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) { item.manga.author?.trim() ?: "" } else { @@ -109,13 +122,25 @@ class LibraryGridHolder( private fun setCover(manga: Manga) { if ((adapter.recyclerView.context as? Activity)?.isDestroyed == true) return binding.coverThumbnail.loadManga(manga) { - if (!fixedSize) { + val hasRatio = binding.coverThumbnail.layoutParams.height != ViewGroup.LayoutParams.WRAP_CONTENT + if (!fixedSize && !hasRatio) { precision(Precision.INEXACT) scale(Scale.FIT) } + listener( + onSuccess = { _, _ -> + if (!fixedSize && !hasRatio && MangaCoverMetadata.getRatio(manga) != null) { + setFreeformCoverRatio(manga) + } + } + ) } } + fun setFreeformCoverRatio(manga: Manga, parent: AutofitRecyclerView? = null) { + binding.setFreeformCoverRatio(manga, parent) + } + private fun playButtonClicked() { adapter.libraryListener.startReading(flexibleAdapterPosition) } @@ -134,3 +159,30 @@ class LibraryGridHolder( binding.unreadDownloadBadge.badgeView.isDragged = false } } + +fun MangaGridItemBinding.setFreeformCoverRatio(manga: Manga?, parent: AutofitRecyclerView? = null) { + val ratio = manga?.let { MangaCoverMetadata.getRatio(it) } + val itemWidth = parent?.itemWidth ?: root.width + if (ratio != null) { + coverThumbnail.adjustViewBounds = false + coverThumbnail.maxHeight = Int.MAX_VALUE + coverThumbnail.minimumHeight = 56.dpToPx + constraintLayout.minHeight = 56.dpToPx + } else { + val coverHeight = (itemWidth / 3f * 4f).toInt() + constraintLayout.minHeight = coverHeight / 2 + coverThumbnail.minimumHeight = + (itemWidth / 3f * 3.6f).toInt() + coverThumbnail.maxHeight = (itemWidth / 3f * 6f).toInt() + coverThumbnail.adjustViewBounds = true + } + coverThumbnail.updateLayoutParams { + if (ratio != null) { + height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT + dimensionRatio = "W,1:$ratio" + } else { + height = ViewGroup.LayoutParams.WRAP_CONTENT + dimensionRatio = null + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt index 5fff77c0ba..90c6c2e93b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.library import android.view.View import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.IFlexible @@ -32,6 +33,8 @@ class LibraryHeaderItem( payloads: MutableList? ) { holder.bind(this) + val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams + layoutParams?.isFullSpan = true } val category: Category diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt index 878be5bda5..d2294a698b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryItem.kt @@ -1,14 +1,15 @@ package eu.kanade.tachiyomi.ui.library -import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ImageView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.IFilterable @@ -20,6 +21,7 @@ import eu.kanade.tachiyomi.databinding.MangaGridItemBinding import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.util.system.contextCompatDrawable import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.compatToolTipText import eu.kanade.tachiyomi.widget.AutofitRecyclerView import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -48,7 +50,7 @@ class LibraryItem( get() = preferences.hideStartReadingButton().get() override fun getLayoutRes(): Int { - return if (libraryLayout == 0 || manga.isBlank()) { + return if (libraryLayout == LAYOUT_LIST || manga.isBlank()) { R.layout.manga_list_item } else { R.layout.manga_grid_item @@ -60,22 +62,18 @@ class LibraryItem( return if (parent is AutofitRecyclerView) { val libraryLayout = libraryLayout val isFixedSize = uniformSize - if (libraryLayout == 0 || manga.isBlank()) { + if (libraryLayout == LAYOUT_LIST || manga.isBlank()) { LibraryListHolder(view, adapter as LibraryCategoryAdapter) } else { view.apply { val binding = MangaGridItemBinding.bind(this) - val coverHeight = (parent.itemWidth / 3f * 4f).toInt() - if (libraryLayout == 1) { - binding.gradient.layoutParams = FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - (coverHeight * 0.66f).toInt(), - Gravity.BOTTOM - ) + binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID + if (libraryLayout == LAYOUT_COMPACT_GRID) { binding.card.updateLayoutParams { bottomMargin = 6.dpToPx } - } else if (libraryLayout == 2) { + } else if (libraryLayout >= LAYOUT_COMFORTABLE_GRID) { + binding.textLayout.isVisible = libraryLayout == LAYOUT_COMFORTABLE_GRID binding.constraintLayout.background = context.contextCompatDrawable( R.drawable.library_comfortable_grid_selector ) @@ -99,23 +97,22 @@ class LibraryItem( binding.constraintLayout.minHeight = 0 binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP binding.coverThumbnail.adjustViewBounds = false - binding.coverThumbnail.layoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - (parent.itemWidth / 3f * 3.875f).toInt() - ) - } else { - binding.constraintLayout.minHeight = coverHeight / 2 - binding.coverThumbnail.minimumHeight = (parent.itemWidth / 3f * 3.6f).toInt() - binding.coverThumbnail.maxHeight = (parent.itemWidth / 3f * 6f).toInt() + binding.coverThumbnail.updateLayoutParams { + height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT + dimensionRatio = "15:22" + } } } - LibraryGridHolder( + val gridHolder = LibraryGridHolder( view, adapter as LibraryCategoryAdapter, - parent.itemWidth, - libraryLayout == 1, + libraryLayout == LAYOUT_COMPACT_GRID, isFixedSize ) + if (!isFixedSize) { + gridHolder.setFreeformCoverRatio(manga, parent) + } + gridHolder } } else { LibraryListHolder(view, adapter as LibraryCategoryAdapter) @@ -128,8 +125,16 @@ class LibraryItem( position: Int, payloads: MutableList? ) { + if (holder is LibraryGridHolder && !holder.fixedSize) { + holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView) + } holder.onSetValues(this) (holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position)) + val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams + layoutParams?.isFullSpan = manga.isBlank() + if (libraryLayout == LAYOUT_COVER_ONLY_GRID) { + holder.itemView.compatToolTipText = manga.title + } } /** @@ -196,4 +201,11 @@ class LibraryItem( result = 31 * result + (header?.hashCode() ?: 0) return result } + + companion object { + const val LAYOUT_LIST = 0 + const val LAYOUT_COMPACT_GRID = 1 + const val LAYOUT_COMFORTABLE_GRID = 2 + const val LAYOUT_COVER_ONLY_GRID = 3 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 7b6eeb56cb..75751763c3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.image.coil.MangaFetcher import eu.kanade.tachiyomi.data.preference.DelayedLibrarySuggestionsJob import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.minusAssign @@ -35,6 +36,7 @@ import eu.kanade.tachiyomi.util.chapter.ChapterSort import eu.kanade.tachiyomi.util.lang.capitalizeWords import eu.kanade.tachiyomi.util.lang.chopByWords import eu.kanade.tachiyomi.util.lang.removeArticles +import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.withUIContext @@ -1218,6 +1220,16 @@ class LibraryPresenter( } } + suspend fun updateRatiosAndColors() { + val db: DatabaseHelper = Injekt.get() + val mangaFetcher = MangaFetcher() + val libraryManga = db.getLibraryMangas().executeOnIO() + libraryManga.forEach { manga -> + mangaFetcher.setRatioAndColors(manga) + } + MangaCoverMetadata.savePrefs() + } + fun updateCustoms() { val db: DatabaseHelper = Injekt.get() val cc: CoverCache = Injekt.get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/SearchGlobalItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/SearchGlobalItem.kt index cf1b585212..56585cd162 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/SearchGlobalItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/SearchGlobalItem.kt @@ -5,6 +5,7 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.IFlexible @@ -49,6 +50,8 @@ class SearchGlobalItem : AbstractFlexibleItem() { payloads: MutableList ) { holder.bind(string) + val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams + layoutParams?.isFullSpan = true } override fun equals(other: Any?): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/display/LibraryDisplayView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/display/LibraryDisplayView.kt index e598d0004e..d1a445362c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/display/LibraryDisplayView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/display/LibraryDisplayView.kt @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.databinding.LibraryDisplayLayoutBinding import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet import eu.kanade.tachiyomi.ui.library.filter.ManageFilterItem import eu.kanade.tachiyomi.util.bindToPreference +import eu.kanade.tachiyomi.util.lang.addBetaTag import eu.kanade.tachiyomi.util.lang.withSubtitle import eu.kanade.tachiyomi.util.system.bottomCutoutInset import eu.kanade.tachiyomi.util.system.dpToPx @@ -31,8 +32,13 @@ class LibraryDisplayView @JvmOverloads constructor(context: Context, attrs: Attr override fun inflateBinding() = LibraryDisplayLayoutBinding.bind(this) override fun initGeneralPreferences() { binding.displayGroup.bindToPreference(preferences.libraryLayout()) - binding.uniformGrid.bindToPreference(preferences.uniformGrid()) + binding.uniformGrid.bindToPreference(preferences.uniformGrid()) { + binding.staggeredGrid.isEnabled = !it + } binding.outlineOnCovers.bindToPreference(preferences.outlineOnCovers()) + binding.staggeredGrid.text = context.getString(R.string.use_staggered_grid).addBetaTag(context) + binding.staggeredGrid.isEnabled = !preferences.uniformGrid().get() + binding.staggeredGrid.bindToPreference(preferences.useStaggeredGrid()) binding.gridSeekbar.value = ((preferences.gridSize().get() + .5f) * 2f).roundToInt().toFloat() binding.resetGridSize.setOnClickListener { binding.gridSeekbar.value = 3f 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 2f076bfee0..772d671d8a 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 @@ -80,6 +80,7 @@ import eu.kanade.tachiyomi.ui.setting.SettingsController import eu.kanade.tachiyomi.ui.setting.SettingsMainController import eu.kanade.tachiyomi.ui.source.BrowseController import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController +import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import eu.kanade.tachiyomi.util.manga.MangaShortcutManager import eu.kanade.tachiyomi.util.system.contextCompatDrawable import eu.kanade.tachiyomi.util.system.dpToPx @@ -385,6 +386,7 @@ open class MainActivity : BaseActivity(), DownloadServiceLi } nav.isVisible = !hideBottomNav + updateControllersWithSideNavChanges() binding.bottomView?.visibility = if (hideBottomNav) View.GONE else binding.bottomView?.visibility ?: View.GONE nav.alpha = if (hideBottomNav) 0f else 1f router.addChangeListener( @@ -438,7 +440,7 @@ open class MainActivity : BaseActivity(), DownloadServiceLi preferences.incognitoMode().set(false) // Show changelog if needed - if (Migrations.upgrade(preferences)) { + if (Migrations.upgrade(preferences, lifecycleScope)) { if (!BuildConfig.DEBUG) { content.post { whatsNewSheet().show() @@ -642,7 +644,12 @@ open class MainActivity : BaseActivity(), DownloadServiceLi super.onPause() snackBar?.dismiss() setStartingTab() + saveExtras() + } + + fun saveExtras() { mangaShortcutManager.updateShortcuts() + MangaCoverMetadata.savePrefs() } private fun checkForAppUpdates() { @@ -816,7 +823,7 @@ open class MainActivity : BaseActivity(), DownloadServiceLi setStartingTab() } SecureActivityDelegate.locked = this !is SearchActivity - mangaShortcutManager.updateShortcuts() + saveExtras() super.onBackPressed() } } @@ -1051,6 +1058,7 @@ open class MainActivity : BaseActivity(), DownloadServiceLi nav.visibility = if (!hideBottomNav) View.VISIBLE else nav.visibility if (nav == binding.sideNav) { nav.isVisible = !hideBottomNav + updateControllersWithSideNavChanges(from) nav.alpha = 1f } else { animationSet?.cancel() @@ -1077,6 +1085,27 @@ open class MainActivity : BaseActivity(), DownloadServiceLi } } + private fun updateControllersWithSideNavChanges(extraController: Controller? = null) { + if (!isBindingInitialized || !this::router.isInitialized) return + binding.sideNav?.let { sideNav -> + val controllers = (router.backstack.map { it?.controller } + extraController) + .filterNotNull() + .distinct() + val navWidth = sideNav.width.takeIf { it != 0 } ?: 80.dpToPx + controllers.forEach { controller -> + val isRootController = controller is RootSearchInterface + if (controller.view?.layoutParams !is ViewGroup.MarginLayoutParams) return@forEach + controller.view?.updateLayoutParams { + marginStart = if (sideNav.isVisible) { + if (isRootController) 0 else -navWidth + } else { + if (isRootController) navWidth else 0 + } + } + } + } + } + fun showTabBar(show: Boolean, animate: Boolean = true) { tabAnimation?.cancel() if (animate) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt index 02b277c64e..b0200a0cea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationProcessHolder.kt @@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.ui.library.setFreeformCoverRatio import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.view.setCards @@ -50,6 +51,9 @@ class MigrationProcessHolder( fun bind(item: MigrationProcessItem) { this.item = item launchUI { + binding.migrationMangaCardFrom.setFreeformCoverRatio(item.manga.manga()) + binding.migrationMangaCardTo.setFreeformCoverRatio(null) + val manga = item.manga.manga() val source = item.manga.mangaSource() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt index 8927cf15c5..eb519977d1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/BrowseSourceItem.kt @@ -75,10 +75,10 @@ class BrowseSourceItem( binding.constraintLayout.minHeight = 0 binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP binding.coverThumbnail.adjustViewBounds = false - binding.coverThumbnail.layoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - (parent.itemWidth / 3f * 3.7f).toInt() - ) + binding.coverThumbnail.updateLayoutParams { + height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT + dimensionRatio = "15:22" + } } BrowseSourceGridHolder(view, adapter, listType == 1, outlineOnCovers.get()) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt new file mode 100644 index 0000000000..ae6f840d35 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaCoverMetadata.kt @@ -0,0 +1,75 @@ +package eu.kanade.tachiyomi.util.manga + +import androidx.annotation.ColorInt +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import uy.kohesive.injekt.injectLazy +import java.util.concurrent.ConcurrentHashMap + +/** Object that holds info about a covers size ratio + dominant colors */ +object MangaCoverMetadata { + private var coverRatioMap = ConcurrentHashMap() + private var coverColorMap = ConcurrentHashMap>() + val preferences by injectLazy() + + fun load() { + val ratios = preferences.coverRatios().get() + coverRatioMap = ConcurrentHashMap( + ratios.mapNotNull { + val splits = it.split("|") + val id = splits.firstOrNull()?.toLongOrNull() + val ratio = splits.lastOrNull()?.toFloatOrNull() + if (id != null && ratio != null) { + id to ratio + } else { + null + } + }.toMap() + ) + val colors = preferences.coverColors().get() + coverColorMap = ConcurrentHashMap( + colors.mapNotNull { + val splits = it.split("|") + val id = splits.firstOrNull()?.toLongOrNull() + val color = splits.getOrNull(1)?.toIntOrNull() + val textColor = splits.getOrNull(2)?.toIntOrNull() + if (id != null && color != null) { + id to (color to (textColor ?: 0)) + } else { + null + } + }.toMap() + ) + } + + fun remove(manga: Manga) { + val id = manga.id ?: return + coverRatioMap.remove(id) + coverColorMap.remove(id) + } + + fun addCoverRatio(manga: Manga, ratio: Float) { + val id = manga.id ?: return + coverRatioMap[id] = ratio + } + + fun addCoverColor(manga: Manga, @ColorInt color: Int, @ColorInt textColor: Int) { + val id = manga.id ?: return + coverColorMap[id] = color to textColor + } + + fun getColors(manga: Manga): Pair? { + return coverColorMap[manga.id] + } + + fun getRatio(manga: Manga): Float? { + return coverRatioMap[manga.id] + } + + fun savePrefs() { + val mapCopy = coverRatioMap.toMap() + preferences.coverRatios().set(mapCopy.map { "${it.key}|${it.value}" }.toSet()) + val mapColorCopy = coverColorMap.toMap() + preferences.coverColors().set(mapColorCopy.map { "${it.key}|${it.value.first}|${it.value.second}" }.toSet()) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt index ab981b0dd5..398bc4eaf4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ControllerExtensions.kt @@ -34,6 +34,7 @@ import androidx.core.view.updatePaddingRelative import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.ControllerChangeHandler @@ -579,6 +580,7 @@ fun Controller.moveRecyclerViewUp(allTheWayUp: Boolean = false, scrollUpAnyway: val appBarOffset = activityBinding.appBar.toolbarDistanceToTop if (allTheWayUp && recycler.computeVerticalScrollOffset() - recycler.paddingTop <= fullAppBarHeight ?: activityBinding.appBar.preLayoutHeight) { (recycler.layoutManager as? LinearLayoutManager)?.scrollToPosition(0) + (recycler.layoutManager as? StaggeredGridLayoutManager)?.scrollToPosition(0) recycler.post { activityBinding.appBar.updateAppBarAfterY(recycler) activityBinding.appBar.useSearchToolbarForMenu(false) @@ -588,6 +590,8 @@ fun Controller.moveRecyclerViewUp(allTheWayUp: Boolean = false, scrollUpAnyway: if (scrollUpAnyway || recycler.computeVerticalScrollOffset() - recycler.paddingTop <= 0 - appBarOffset) { (recycler.layoutManager as? LinearLayoutManager) ?.scrollToPositionWithOffset(0, activityBinding.appBar.yNeededForSmallToolbar) + (recycler.layoutManager as? StaggeredGridLayoutManager) + ?.scrollToPositionWithOffset(0, activityBinding.appBar.yNeededForSmallToolbar) recycler.post { activityBinding.appBar.updateAppBarAfterY(recycler) activityBinding.appBar.useSearchToolbarForMenu(recycler.computeVerticalScrollOffset() != 0) 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 a86974ef48..0f94becbfa 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 @@ -77,6 +77,7 @@ import eu.kanade.tachiyomi.util.system.powerManager import eu.kanade.tachiyomi.util.system.pxToDp import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat import eu.kanade.tachiyomi.widget.AutofitRecyclerView +import eu.kanade.tachiyomi.widget.StaggeredGridLayoutManagerAccurateOffset import kotlin.math.max import kotlin.math.min import kotlin.math.pow @@ -310,21 +311,25 @@ fun NavigationBarView.getItemView(@IdRes id: Int): NavigationBarItemView? { fun RecyclerView.smoothScrollToTop() { val linearLayoutManager = layoutManager as? LinearLayoutManager - if (linearLayoutManager != null) { + val staggeredLayoutManager = layoutManager as? StaggeredGridLayoutManagerAccurateOffset + if (linearLayoutManager != null || staggeredLayoutManager != null) { val smoothScroller: SmoothScroller = object : LinearSmoothScroller(context) { override fun getVerticalSnapPreference(): Int { return SNAP_TO_START } } smoothScroller.targetPosition = 0 - val firstItemPos = linearLayoutManager.findFirstVisibleItemPosition() + val firstItemPos = linearLayoutManager?.findFirstVisibleItemPosition() + ?: staggeredLayoutManager?.findFirstVisibleItemPosition() ?: 0 if (firstItemPos > 15) { scrollToPosition(15) post { - linearLayoutManager.startSmoothScroll(smoothScroller) + linearLayoutManager?.startSmoothScroll(smoothScroller) + staggeredLayoutManager?.startSmoothScroll(smoothScroller) } } else { - linearLayoutManager.startSmoothScroll(smoothScroller) + linearLayoutManager?.startSmoothScroll(smoothScroller) + staggeredLayoutManager?.startSmoothScroll(smoothScroller) } } else { scrollToPosition(0) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt index da487a5f33..d15089a5e8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt @@ -3,10 +3,19 @@ package eu.kanade.tachiyomi.widget import android.content.Context import android.util.AttributeSet import androidx.core.content.edit +import androidx.core.view.WindowInsetsCompat.Type.systemBars +import androidx.core.view.doOnNextLayout import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.library.LibraryItem +import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.pxToDp +import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat +import kotlin.math.abs import kotlin.math.max import kotlin.math.pow import kotlin.math.roundToInt @@ -14,7 +23,7 @@ import kotlin.math.roundToInt class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : androidx.recyclerview.widget.RecyclerView(context, attrs) { - val manager = GridLayoutManagerAccurateOffset(context, 1) + var manager: LayoutManager = GridLayoutManagerAccurateOffset(context, 1) var lastMeasuredWidth = 0 var columnWidth = -1f @@ -29,20 +38,29 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att set(value) { field = value if (value > 0) { - manager.spanCount = value + managerSpanCount = value } } val itemWidth: Int get() { return if (spanCount == 0) measuredWidth / getTempSpan() - else measuredWidth / manager.spanCount + else measuredWidth / managerSpanCount } init { layoutManager = manager } + var managerSpanCount: Int + get() { + return (manager as? GridLayoutManager)?.spanCount ?: (manager as StaggeredGridLayoutManager).spanCount + } + set(value) { + (manager as? GridLayoutManager)?.spanCount = value + (manager as? StaggeredGridLayoutManager)?.spanCount = value + } + private fun getTempSpan(): Int { if (spanCount == 0 && columnWidth > 0) { val dpWidth = (measuredWidth.pxToDp / 100f).roundToInt() @@ -54,7 +72,67 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att override fun onMeasure(widthSpec: Int, heightSpec: Int) { super.onMeasure(widthSpec, heightSpec) setSpan() - lastMeasuredWidth = measuredWidth + if (width == 0) { + spanCount = getTempSpan() + } + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, l, t, r, b) + setSpan() + lastMeasuredWidth = width + } + + fun useStaggered(preferences: PreferencesHelper) { + useStaggered( + preferences.useStaggeredGrid().get() && + !preferences.uniformGrid().get() && + preferences.libraryLayout().get() != LibraryItem.LAYOUT_LIST + ) + } + + private fun useStaggered(use: Boolean) { + if (use && manager !is StaggeredGridLayoutManagerAccurateOffset) { + manager = StaggeredGridLayoutManagerAccurateOffset( + context, + null, + 1, + StaggeredGridLayoutManager.VERTICAL + ) + setNewManager() + } else if (!use && manager !is GridLayoutManagerAccurateOffset) { + manager = GridLayoutManagerAccurateOffset(context, 1) + setNewManager() + } + } + + private fun setNewManager() { + val firstPos = findFirstVisibleItemPosition().takeIf { it != NO_POSITION } + layoutManager = manager + if (firstPos != null) { + val insetsTop = rootWindowInsetsCompat?.getInsets(systemBars())?.top ?: 0 + doOnNextLayout { + scrollToPositionWithOffset(firstPos, -paddingTop + 56.dpToPx + insetsTop) + } + } + } + + fun scrollToPositionWithOffset(position: Int, offset: Int) { + layoutManager ?: return + return (layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(position, offset) + ?: (layoutManager as StaggeredGridLayoutManagerAccurateOffset).scrollToPositionWithOffset(position, offset) + } + + fun findFirstVisibleItemPosition(): Int { + layoutManager ?: return 0 + return (layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition() + ?: (layoutManager as StaggeredGridLayoutManagerAccurateOffset).findFirstVisibleItemPosition() + } + + fun findFirstCompletelyVisibleItemPosition(): Int { + layoutManager ?: return 0 + return (layoutManager as? LinearLayoutManager)?.findFirstCompletelyVisibleItemPosition() + ?: (layoutManager as StaggeredGridLayoutManagerAccurateOffset).findFirstCompletelyVisibleItemPosition() } fun setGridSize(preferences: PreferencesHelper) { @@ -85,8 +163,14 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att } private fun setSpan(force: Boolean = false) { - if ((spanCount == 0 || force || measuredHeight != lastMeasuredWidth) && columnWidth > 0) { - val dpWidth = (measuredWidth.pxToDp / 100f).roundToInt() + if (( + spanCount == 0 || force || + // Add 100dp check to make sure we dont update span for sidenav changes + (width != lastMeasuredWidth && abs(width - lastMeasuredWidth) > 100.dpToPx) + ) && + columnWidth > 0 + ) { + val dpWidth = (width.pxToDp / 100f).roundToInt() val count = max(1, (dpWidth / columnWidth).roundToInt()) spanCount = count } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/StaggeredGridLayoutManagerAccurateOffset.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/StaggeredGridLayoutManagerAccurateOffset.kt new file mode 100644 index 0000000000..2c4b277fe5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/StaggeredGridLayoutManagerAccurateOffset.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.widget + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import eu.kanade.tachiyomi.R + +class StaggeredGridLayoutManagerAccurateOffset(context: Context?, attr: AttributeSet?, spanCount: Int, orientation: Int) : + StaggeredGridLayoutManager(context, attr, spanCount, orientation) { + + var rView: RecyclerView? = null + + private val toolbarHeight by lazy { + val attrsArray = intArrayOf(R.attr.mainActionBarSize) + val array = (context ?: rView?.context)?.obtainStyledAttributes(attrsArray) + val height = array?.getDimensionPixelSize(0, 0) ?: 0 + array?.recycle() + height + } + + override fun onAttachedToWindow(view: RecyclerView?) { + super.onAttachedToWindow(view) + rView = view + } + + override fun onDetachedFromWindow(view: RecyclerView?, recycler: RecyclerView.Recycler?) { + super.onDetachedFromWindow(view, recycler) + rView = null + } + + override fun computeVerticalScrollOffset(state: RecyclerView.State): Int { + if (childCount == 0) { + return 0 + } + rView ?: return super.computeVerticalScrollOffset(state) + val firstChild = (0 until childCount) + .mapNotNull { getChildAt(it) } + .mapNotNull { pos -> (pos to getPosition(pos)).takeIf { it.second != RecyclerView.NO_POSITION } } + .minByOrNull { it.second } ?: return 0 + val scrolledY: Int = -firstChild.first.y.toInt() + return if (firstChild.second == 0) { + scrolledY + paddingTop + } else { + super.computeVerticalScrollOffset(state) + } + } + + fun findFirstVisibleItemPosition(): Int { + return getFirstPos(rView, toolbarHeight) + } + + fun findFirstCompletelyVisibleItemPosition(): Int { + return getFirstCompletePos(rView, toolbarHeight) + } +} diff --git a/app/src/main/res/layout/library_display_layout.xml b/app/src/main/res/layout/library_display_layout.xml index fbcff97b9b..b950145ee8 100644 --- a/app/src/main/res/layout/library_display_layout.xml +++ b/app/src/main/res/layout/library_display_layout.xml @@ -39,6 +39,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/comfortable_grid" /> + + + + - + android:layout_height="wrap_content"> + + + + + + + + - - Can also be found by expanding library filters List Comfortable Grid + Cover-only grid Compact Grid Download badges Language badges Hide start reading button Badges Uniform grid covers + Use staggered grid Show outline around covers Grid size %d per row