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
This commit is contained in:
Jays2Kings 2022-04-23 18:41:18 -04:00 committed by GitHub
parent d8e944f396
commit 586e3667c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 632 additions and 120 deletions

View file

@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.ui.recents.RecentsPresenter import eu.kanade.tachiyomi.ui.recents.RecentsPresenter
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.source.SourcePresenter 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.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -78,6 +79,7 @@ open class App : Application(), DefaultLifecycleObserver {
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
MangaCoverMetadata.load()
preferences.nightMode() preferences.nightMode()
.asImmediateFlow { AppCompatDelegate.setDefaultNightMode(it) } .asImmediateFlow { AppCompatDelegate.setDefaultNightMode(it) }
.launchIn(ProcessLifecycleOwner.get().lifecycleScope) .launchIn(ProcessLifecycleOwner.get().lifecycleScope)

View file

@ -15,7 +15,9 @@ import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibraryPresenter import eu.kanade.tachiyomi.ui.library.LibraryPresenter
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@ -28,7 +30,7 @@ object Migrations {
* @param preferences Preferences of the application. * @param preferences Preferences of the application.
* @return true if a migration is performed, false otherwise. * @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 context = preferences.context
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit { prefs.edit {
@ -184,6 +186,9 @@ object Migrations {
} }
} }
if (oldVersion < 88) { if (oldVersion < 88) {
scope.launchIO {
LibraryPresenter.updateRatiosAndColors()
}
val oldReaderTap = prefs.getBoolean("reader_tap", false) val oldReaderTap = prefs.getBoolean("reader_tap", false)
if (!oldReaderTap) { if (!oldReaderTap) {
preferences.navigationModePager().set(5) preferences.navigationModePager().set(5)

View file

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -275,6 +276,13 @@ interface Manga : SManga {
id?.let { vibrantCoverColorMap[it] = value } id?.let { vibrantCoverColorMap[it] = value }
} }
var dominantCoverColors: Pair<Int, Int>?
get() = MangaCoverMetadata.getColors(this)
set(value) {
value ?: return
MangaCoverMetadata.addCoverColor(this, value.first, value.second)
}
companion object { companion object {
// Generic filter that does not filter anything // Generic filter that does not filter anything

View file

@ -1,7 +1,10 @@
package eu.kanade.tachiyomi.data.image.coil package eu.kanade.tachiyomi.data.image.coil
import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.getSystemService
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import coil.decode.GifDecoder import coil.decode.GifDecoder
@ -16,8 +19,8 @@ class CoilSetup(context: Context) {
val imageLoader = ImageLoader.Builder(context) val imageLoader = ImageLoader.Builder(context)
.availableMemoryPercentage(0.40) .availableMemoryPercentage(0.40)
.crossfade(true) .crossfade(true)
.allowRgb565(true) .allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice)
.allowHardware(false) .allowHardware(true)
.componentRegistry { .componentRegistry {
if (Build.VERSION.SDK_INT >= 28) { if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder(context)) add(ImageDecoderDecoder(context))

View file

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.image.coil package eu.kanade.tachiyomi.data.image.coil
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.palette.graphics.Palette import androidx.palette.graphics.Palette
@ -23,18 +22,6 @@ class LibraryMangaImageTarget(
private val coverCache: CoverCache by injectLazy() 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?) { override fun onError(error: Drawable?) {
super.onError(error) super.onError(error)
if (manga.favorite) { if (manga.favorite) {

View file

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.image.coil package eu.kanade.tachiyomi.data.image.coil
import android.graphics.BitmapFactory
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.palette.graphics.Palette
import coil.bitmap.BitmapPool import coil.bitmap.BitmapPool
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.Options import coil.decode.Options
@ -14,7 +16,12 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import eu.kanade.tachiyomi.util.storage.DiskUtil 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.CacheControl
import okhttp3.Call import okhttp3.Call
import okhttp3.Request import okhttp3.Request
@ -40,6 +47,7 @@ class MangaFetcher : Fetcher<Manga> {
private val coverCache: CoverCache by injectLazy() private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private val defaultClient = Injekt.get<NetworkHelper>().client private val defaultClient = Injekt.get<NetworkHelper>().client
val fileScope = CoroutineScope(Job() + Dispatchers.IO)
override fun key(data: Manga): String? { override fun key(data: Manga): String? {
if (data.thumbnail_url.isNullOrBlank()) return null if (data.thumbnail_url.isNullOrBlank()) return null
@ -65,6 +73,7 @@ class MangaFetcher : Fetcher<Manga> {
if (!shouldFetchRemotely) { if (!shouldFetchRemotely) {
val customCoverFile = coverCache.getCustomCoverFile(manga) val customCoverFile = coverCache.getCustomCoverFile(manga)
if (customCoverFile.exists() && options.parameters.value(realCover) != true) { if (customCoverFile.exists() && options.parameters.value(realCover) != true) {
setRatioAndColorsInScope(manga, customCoverFile)
return fileLoader(customCoverFile) return fileLoader(customCoverFile)
} }
} }
@ -73,6 +82,7 @@ class MangaFetcher : Fetcher<Manga> {
if (!manga.favorite) { if (!manga.favorite) {
coverFile.setLastModified(Date().time) coverFile.setLastModified(Date().time)
} }
setRatioAndColorsInScope(manga, coverFile)
return fileLoader(coverFile) return fileLoader(coverFile)
} }
val (response, body) = awaitGetCall( val (response, body) = awaitGetCall(
@ -104,9 +114,50 @@ class MangaFetcher : Fetcher<Manga> {
coverCache.deleteCachedCovers() coverCache.deleteCachedCovers()
} }
} }
setRatioAndColorsInScope(manga, coverFile, true)
return fileLoader(coverFile) 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<Response, private suspend fun awaitGetCall(manga: Manga, onlyCache: Boolean = false, forceNetwork: Boolean): Pair<Response,
ResponseBody> { ResponseBody> {
val call = getCall(manga, onlyCache, forceNetwork) val call = getCall(manga, onlyCache, forceNetwork)

View file

@ -253,6 +253,10 @@ object PreferenceKeys {
const val defaultChapterSortByAscendingOrDescending = "default_chapter_sort_by_ascending_or_descending" 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 hideChapterTitles = "hide_chapter_titles"
const val chaptersDescAsDefault = "chapters_desc_as_default" const val chaptersDescAsDefault = "chapters_desc_as_default"

View file

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob import eu.kanade.tachiyomi.data.updater.AutoAppUpdaterJob
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder 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.library.filter.FilterBottomSheet
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
import eu.kanade.tachiyomi.ui.reader.settings.PageLayout 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 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) fun gridSize() = flowPrefs.getFloat(Keys.gridSize, 1f)
@ -451,4 +452,10 @@ class PreferencesHelper(val context: Context) {
fun chaptersDescAsDefault() = flowPrefs.getBoolean(Keys.chaptersDescAsDefault, true) fun chaptersDescAsDefault() = flowPrefs.getBoolean(Keys.chaptersDescAsDefault, true)
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC) 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)
} }

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.base.controller
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorSet import android.animation.AnimatorSet
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.content.res.Configuration
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
@ -35,9 +34,8 @@ class OneWayFadeChangeHandler : FadeChangeHandler {
animator.play(ObjectAnimator.ofFloat(to, View.ALPHA, start, 1f)) 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 (from != null && (!isPush || removesFromViewOnPush())) {
if (!hasSideNav && fadeOut) { if (fadeOut) {
animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0f)) animator.play(ObjectAnimator.ofFloat(from, View.ALPHA, 0f))
} else { } else {
container.removeView(from) container.removeView(from)

View file

@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
@ -19,6 +20,7 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewPropertyAnimator import android.view.ViewPropertyAnimator
import android.view.ViewTreeObserver
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
@ -37,8 +39,8 @@ import androidx.core.view.updatePadding
import androidx.core.view.updatePaddingRelative import androidx.core.view.updatePaddingRelative
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
import com.fredporciuncula.flow.preferences.Preference import com.fredporciuncula.flow.preferences.Preference
@ -227,6 +229,8 @@ class LibraryController(
override val mainRecycler: RecyclerView override val mainRecycler: RecyclerView
get() = binding.libraryGridRecycler.recycler get() = binding.libraryGridRecycler.recycler
var staggeredBundle: Parcelable? = null
private var staggeredObserver: ViewTreeObserver.OnGlobalLayoutListener? = null
override fun getTitle(): String? { override fun getTitle(): String? {
setSubtitle() setSubtitle()
@ -318,6 +322,18 @@ class LibraryController(
updateHopperPosition() 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 = LibraryCategoryAdapter(this)
adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY adapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
setRecyclerLayout() 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.setHasFixedSize(true)
binding.libraryGridRecycler.recycler.adapter = adapter binding.libraryGridRecycler.recycler.adapter = adapter
@ -798,8 +801,7 @@ class LibraryController(
val category = getVisibleHeader() ?: return val category = getVisibleHeader() ?: return
if (presenter.showAllCategories) { if (presenter.showAllCategories) {
if (!next) { if (!next) {
val fPosition = val fPosition = binding.libraryGridRecycler.recycler.findFirstVisibleItemPosition()
(binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
if (fPosition > adapter.currentItems.indexOf(category)) { if (fPosition > adapter.currentItems.indexOf(category)) {
scrollToHeader(category.category.order) scrollToHeader(category.category.order)
return return
@ -834,7 +836,7 @@ class LibraryController(
private fun getHeader(firstCompletelyVisible: Boolean = false): LibraryHeaderItem? { private fun getHeader(firstCompletelyVisible: Boolean = false): LibraryHeaderItem? {
val position = if (firstCompletelyVisible) { val position = if (firstCompletelyVisible) {
(binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() binding.libraryGridRecycler.recycler.findFirstCompletelyVisibleItemPosition()
} else { } else {
-1 -1
} }
@ -844,8 +846,7 @@ class LibraryController(
is LibraryItem -> return item.header is LibraryItem -> return item.header
} }
} else { } else {
val fPosition = val fPosition = binding.libraryGridRecycler.recycler.findFirstVisibleItemPosition()
(binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
when (val item = adapter.getItem(fPosition)) { when (val item = adapter.getItem(fPosition)) {
is LibraryHeaderItem -> return item is LibraryHeaderItem -> return item
is LibraryItem -> return item.header is LibraryItem -> return item.header
@ -855,8 +856,7 @@ class LibraryController(
} }
private fun getVisibleHeader(): LibraryHeaderItem? { private fun getVisibleHeader(): LibraryHeaderItem? {
val fPosition = val fPosition = binding.libraryGridRecycler.recycler.findFirstVisibleItemPosition()
(binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
when (val item = adapter.getItem(fPosition)) { when (val item = adapter.getItem(fPosition)) {
is LibraryHeaderItem -> return item is LibraryHeaderItem -> return item
is LibraryItem -> return item.header is LibraryItem -> return item.header
@ -897,7 +897,8 @@ class LibraryController(
bottom = 50.dpToPx + (activityBinding?.bottomNav?.height ?: 0) bottom = 50.dpToPx + (activityBinding?.bottomNav?.height ?: 0)
) )
} }
if (libraryLayout == 0) { useStaggered(preferences)
if (libraryLayout == LibraryItem.LAYOUT_LIST) {
spanCount = 1 spanCount = 1
updatePaddingRelative( updatePaddingRelative(
start = 0, start = 0,
@ -910,6 +911,17 @@ class LibraryController(
end = 5.dpToPx 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( listOf(
preferences.libraryLayout(), preferences.libraryLayout(),
preferences.uniformGrid(), preferences.uniformGrid(),
preferences.gridSize() preferences.gridSize(),
preferences.useStaggeredGrid()
).forEach { ).forEach {
it.asFlow() it.asFlow()
.drop(1) .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 { } else {
saveStaggeredState()
updateFilterSheetY() updateFilterSheetY()
closeTip() closeTip()
if (binding.filterBottomSheet.filterBottomSheet.sheetBehavior.isHidden()) { if (binding.filterBottomSheet.filterBottomSheet.sheetBehavior.isHidden()) {
@ -1003,6 +1021,7 @@ class LibraryController(
} }
displaySheet?.dismiss() displaySheet?.dismiss()
displaySheet = null displaySheet = null
saveStaggeredState()
super.onDestroyView(view) 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) { if (isControllerVisible) {
activityBinding?.appBar?.lockYPos = false 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) { if (!presenter.showAllCategories) {
presenter.switchSection(pos) presenter.switchSection(pos)
activeCategory = pos activeCategory = pos
@ -1168,7 +1213,7 @@ class LibraryController(
val activityBinding = activityBinding ?: return val activityBinding = activityBinding ?: return
val appbarOffset = if (pos <= 0) 0 else -fullAppBarHeight!! + activityBinding.cardFrame.height val appbarOffset = if (pos <= 0) 0 else -fullAppBarHeight!! + activityBinding.cardFrame.height
val previousHeader = adapter.getItem(adapter.indexOf(pos - 1)) as? LibraryHeaderItem val previousHeader = adapter.getItem(adapter.indexOf(pos - 1)) as? LibraryHeaderItem
(binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( binding.libraryGridRecycler.recycler.scrollToPositionWithOffset(
headerPosition, headerPosition,
( (
when { when {
@ -1209,14 +1254,9 @@ class LibraryController(
private fun reattachAdapter() { private fun reattachAdapter() {
libraryLayout = preferences.libraryLayout().get() libraryLayout = preferences.libraryLayout().get()
setRecyclerLayout() setRecyclerLayout()
val position = val position = binding.libraryGridRecycler.recycler.findFirstVisibleItemPosition()
(binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
binding.libraryGridRecycler.recycler.adapter = adapter binding.libraryGridRecycler.recycler.adapter = adapter
binding.libraryGridRecycler.recycler.scrollToPositionWithOffset(position, 0)
(binding.libraryGridRecycler.recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
position,
0
)
} }
fun search(query: String?): Boolean { fun search(query: String?): Boolean {
@ -1333,6 +1373,7 @@ class LibraryController(
override fun onItemClick(view: View?, position: Int): Boolean { override fun onItemClick(view: View?, position: Int): Boolean {
val item = adapter.getItem(position) as? LibraryItem ?: return false val item = adapter.getItem(position) as? LibraryItem ?: return false
return if (adapter.mode == SelectableAdapter.Mode.MULTI) { return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
snack?.dismiss()
lastClickPosition = position lastClickPosition = position
toggleSelection(position) toggleSelection(position)
false false
@ -1342,11 +1383,15 @@ class LibraryController(
} }
} }
private fun openManga(manga: Manga) = router.pushController( private fun saveStaggeredState() {
MangaDetailsController( if (binding.libraryGridRecycler.recycler.manager is StaggeredGridLayoutManager) {
manga staggeredBundle = binding.libraryGridRecycler.recycler.manager.onSaveInstanceState()
).withFadeTransaction() }
) }
private fun openManga(manga: Manga) {
router.pushController(MangaDetailsController(manga).withFadeTransaction())
}
/** /**
* Called when a manga is long clicked. * Called when a manga is long clicked.
@ -1354,7 +1399,15 @@ class LibraryController(
* @param position the position of the element clicked. * @param position the position of the element clicked.
*/ */
override fun onItemLongClick(position: Int) { 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() createActionModeIfNeeded()
when { when {
lastClickPosition == -1 -> setSelection(position) lastClickPosition == -1 -> setSelection(position)
@ -1409,11 +1462,11 @@ class LibraryController(
override fun onItemMove(fromPosition: Int, toPosition: Int) { override fun onItemMove(fromPosition: Int, toPosition: Int) {
// Because padding a recycler causes it to scroll up we have to scroll it back down... wild // Because padding a recycler causes it to scroll up we have to scroll it back down... wild
if (( val fromItem = adapter.getItem(fromPosition)
adapter.getItem(fromPosition) is LibraryItem && val toItem = adapter.getItem(toPosition)
adapter.getItem(fromPosition) is LibraryItem if (binding.libraryGridRecycler.recycler.layoutManager !is StaggeredGridLayoutManager && (
) || (fromItem is LibraryItem && toItem is LibraryItem) || fromItem == null
adapter.getItem(fromPosition) == null )
) { ) {
binding.libraryGridRecycler.recycler.scrollBy( binding.libraryGridRecycler.recycler.scrollBy(
0, 0,

View file

@ -3,17 +3,25 @@ package eu.kanade.tachiyomi.ui.library
import android.app.Activity import android.app.Activity
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import coil.clear import coil.clear
import coil.size.Precision import coil.size.Precision
import coil.size.Scale import coil.size.Scale
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.image.coil.loadManga import eu.kanade.tachiyomi.data.image.coil.loadManga
import eu.kanade.tachiyomi.databinding.MangaGridItemBinding import eu.kanade.tachiyomi.databinding.MangaGridItemBinding
import eu.kanade.tachiyomi.util.lang.highlightText 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.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.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. * 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( class LibraryGridHolder(
private val view: View, private val view: View,
adapter: LibraryCategoryAdapter, adapter: LibraryCategoryAdapter,
var width: Int,
compact: Boolean, compact: Boolean,
private var fixedSize: Boolean val fixedSize: Boolean
) : LibraryHolder(view, adapter) { ) : LibraryHolder(view, adapter) {
private val binding = MangaGridItemBinding.bind(view) private val binding = MangaGridItemBinding.bind(view)
@ -60,6 +67,12 @@ class LibraryGridHolder(
setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root) setCards(adapter.showOutline, binding.card, binding.unreadDownloadBadge.root)
binding.constraintLayout.isVisible = !item.manga.isBlank() binding.constraintLayout.isVisible = !item.manga.isBlank()
binding.title.text = item.manga.title.highlightText(item.filter, color) 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()) { val authorArtist = if (item.manga.author == item.manga.artist || item.manga.artist.isNullOrBlank()) {
item.manga.author?.trim() ?: "" item.manga.author?.trim() ?: ""
} else { } else {
@ -109,12 +122,24 @@ class LibraryGridHolder(
private fun setCover(manga: Manga) { private fun setCover(manga: Manga) {
if ((adapter.recyclerView.context as? Activity)?.isDestroyed == true) return if ((adapter.recyclerView.context as? Activity)?.isDestroyed == true) return
binding.coverThumbnail.loadManga(manga) { binding.coverThumbnail.loadManga(manga) {
if (!fixedSize) { val hasRatio = binding.coverThumbnail.layoutParams.height != ViewGroup.LayoutParams.WRAP_CONTENT
if (!fixedSize && !hasRatio) {
precision(Precision.INEXACT) precision(Precision.INEXACT)
scale(Scale.FIT) 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() { private fun playButtonClicked() {
adapter.libraryListener.startReading(flexibleAdapterPosition) adapter.libraryListener.startReading(flexibleAdapterPosition)
@ -134,3 +159,30 @@ class LibraryGridHolder(
binding.unreadDownloadBadge.badgeView.isDragged = false 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<ConstraintLayout.LayoutParams> {
if (ratio != null) {
height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
dimensionRatio = "W,1:$ratio"
} else {
height = ViewGroup.LayoutParams.WRAP_CONTENT
dimensionRatio = null
}
}
}

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.library
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractHeaderItem import eu.davidea.flexibleadapter.items.AbstractHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
@ -32,6 +33,8 @@ class LibraryHeaderItem(
payloads: MutableList<Any?>? payloads: MutableList<Any?>?
) { ) {
holder.bind(this) holder.bind(this)
val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams
layoutParams?.isFullSpan = true
} }
val category: Category val category: Category

View file

@ -1,14 +1,15 @@
package eu.kanade.tachiyomi.ui.library package eu.kanade.tachiyomi.ui.library
import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFilterable 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.source.SourceManager
import eu.kanade.tachiyomi.util.system.contextCompatDrawable import eu.kanade.tachiyomi.util.system.contextCompatDrawable
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.compatToolTipText
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -48,7 +50,7 @@ class LibraryItem(
get() = preferences.hideStartReadingButton().get() get() = preferences.hideStartReadingButton().get()
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return if (libraryLayout == 0 || manga.isBlank()) { return if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
R.layout.manga_list_item R.layout.manga_list_item
} else { } else {
R.layout.manga_grid_item R.layout.manga_grid_item
@ -60,22 +62,18 @@ class LibraryItem(
return if (parent is AutofitRecyclerView) { return if (parent is AutofitRecyclerView) {
val libraryLayout = libraryLayout val libraryLayout = libraryLayout
val isFixedSize = uniformSize val isFixedSize = uniformSize
if (libraryLayout == 0 || manga.isBlank()) { if (libraryLayout == LAYOUT_LIST || manga.isBlank()) {
LibraryListHolder(view, adapter as LibraryCategoryAdapter) LibraryListHolder(view, adapter as LibraryCategoryAdapter)
} else { } else {
view.apply { view.apply {
val binding = MangaGridItemBinding.bind(this) val binding = MangaGridItemBinding.bind(this)
val coverHeight = (parent.itemWidth / 3f * 4f).toInt() binding.behindTitle.isVisible = libraryLayout == LAYOUT_COVER_ONLY_GRID
if (libraryLayout == 1) { if (libraryLayout == LAYOUT_COMPACT_GRID) {
binding.gradient.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
(coverHeight * 0.66f).toInt(),
Gravity.BOTTOM
)
binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> { binding.card.updateLayoutParams<ConstraintLayout.LayoutParams> {
bottomMargin = 6.dpToPx 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( binding.constraintLayout.background = context.contextCompatDrawable(
R.drawable.library_comfortable_grid_selector R.drawable.library_comfortable_grid_selector
) )
@ -99,23 +97,22 @@ class LibraryItem(
binding.constraintLayout.minHeight = 0 binding.constraintLayout.minHeight = 0
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
binding.coverThumbnail.adjustViewBounds = false binding.coverThumbnail.adjustViewBounds = false
binding.coverThumbnail.layoutParams = FrameLayout.LayoutParams( binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
ViewGroup.LayoutParams.MATCH_PARENT, height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
(parent.itemWidth / 3f * 3.875f).toInt() dimensionRatio = "15:22"
)
} else {
binding.constraintLayout.minHeight = coverHeight / 2
binding.coverThumbnail.minimumHeight = (parent.itemWidth / 3f * 3.6f).toInt()
binding.coverThumbnail.maxHeight = (parent.itemWidth / 3f * 6f).toInt()
} }
} }
LibraryGridHolder( }
val gridHolder = LibraryGridHolder(
view, view,
adapter as LibraryCategoryAdapter, adapter as LibraryCategoryAdapter,
parent.itemWidth, libraryLayout == LAYOUT_COMPACT_GRID,
libraryLayout == 1,
isFixedSize isFixedSize
) )
if (!isFixedSize) {
gridHolder.setFreeformCoverRatio(manga, parent)
}
gridHolder
} }
} else { } else {
LibraryListHolder(view, adapter as LibraryCategoryAdapter) LibraryListHolder(view, adapter as LibraryCategoryAdapter)
@ -128,8 +125,16 @@ class LibraryItem(
position: Int, position: Int,
payloads: MutableList<Any?>? payloads: MutableList<Any?>?
) { ) {
if (holder is LibraryGridHolder && !holder.fixedSize) {
holder.setFreeformCoverRatio(manga, adapter.recyclerView as? AutofitRecyclerView)
}
holder.onSetValues(this) holder.onSetValues(this)
(holder as? LibraryGridHolder)?.setSelected(adapter.isSelected(position)) (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) result = 31 * result + (header?.hashCode() ?: 0)
return result 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
}
} }

View file

@ -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.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager 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.DelayedLibrarySuggestionsJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign 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.capitalizeWords
import eu.kanade.tachiyomi.util.lang.chopByWords import eu.kanade.tachiyomi.util.lang.chopByWords
import eu.kanade.tachiyomi.util.lang.removeArticles 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.executeOnIO
import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchIO
import eu.kanade.tachiyomi.util.system.withUIContext 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() { fun updateCustoms() {
val db: DatabaseHelper = Injekt.get() val db: DatabaseHelper = Injekt.get()
val cc: CoverCache = Injekt.get() val cc: CoverCache = Injekt.get()

View file

@ -5,6 +5,7 @@ import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
@ -49,6 +50,8 @@ class SearchGlobalItem : AbstractFlexibleItem<SearchGlobalItem.Holder>() {
payloads: MutableList<Any> payloads: MutableList<Any>
) { ) {
holder.bind(string) holder.bind(string)
val layoutParams = holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams
layoutParams?.isFullSpan = true
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View file

@ -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.FilterBottomSheet
import eu.kanade.tachiyomi.ui.library.filter.ManageFilterItem import eu.kanade.tachiyomi.ui.library.filter.ManageFilterItem
import eu.kanade.tachiyomi.util.bindToPreference 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.lang.withSubtitle
import eu.kanade.tachiyomi.util.system.bottomCutoutInset import eu.kanade.tachiyomi.util.system.bottomCutoutInset
import eu.kanade.tachiyomi.util.system.dpToPx 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 inflateBinding() = LibraryDisplayLayoutBinding.bind(this)
override fun initGeneralPreferences() { override fun initGeneralPreferences() {
binding.displayGroup.bindToPreference(preferences.libraryLayout()) 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.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.gridSeekbar.value = ((preferences.gridSize().get() + .5f) * 2f).roundToInt().toFloat()
binding.resetGridSize.setOnClickListener { binding.resetGridSize.setOnClickListener {
binding.gridSeekbar.value = 3f binding.gridSeekbar.value = 3f

View file

@ -80,6 +80,7 @@ import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.ui.setting.SettingsMainController import eu.kanade.tachiyomi.ui.setting.SettingsMainController
import eu.kanade.tachiyomi.ui.source.BrowseController import eu.kanade.tachiyomi.ui.source.BrowseController
import eu.kanade.tachiyomi.ui.source.browse.BrowseSourceController 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.manga.MangaShortcutManager
import eu.kanade.tachiyomi.util.system.contextCompatDrawable import eu.kanade.tachiyomi.util.system.contextCompatDrawable
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
@ -385,6 +386,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
} }
nav.isVisible = !hideBottomNav nav.isVisible = !hideBottomNav
updateControllersWithSideNavChanges()
binding.bottomView?.visibility = if (hideBottomNav) View.GONE else binding.bottomView?.visibility ?: View.GONE binding.bottomView?.visibility = if (hideBottomNav) View.GONE else binding.bottomView?.visibility ?: View.GONE
nav.alpha = if (hideBottomNav) 0f else 1f nav.alpha = if (hideBottomNav) 0f else 1f
router.addChangeListener( router.addChangeListener(
@ -438,7 +440,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
preferences.incognitoMode().set(false) preferences.incognitoMode().set(false)
// Show changelog if needed // Show changelog if needed
if (Migrations.upgrade(preferences)) { if (Migrations.upgrade(preferences, lifecycleScope)) {
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
content.post { content.post {
whatsNewSheet().show() whatsNewSheet().show()
@ -642,7 +644,12 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
super.onPause() super.onPause()
snackBar?.dismiss() snackBar?.dismiss()
setStartingTab() setStartingTab()
saveExtras()
}
fun saveExtras() {
mangaShortcutManager.updateShortcuts() mangaShortcutManager.updateShortcuts()
MangaCoverMetadata.savePrefs()
} }
private fun checkForAppUpdates() { private fun checkForAppUpdates() {
@ -816,7 +823,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
setStartingTab() setStartingTab()
} }
SecureActivityDelegate.locked = this !is SearchActivity SecureActivityDelegate.locked = this !is SearchActivity
mangaShortcutManager.updateShortcuts() saveExtras()
super.onBackPressed() super.onBackPressed()
} }
} }
@ -1051,6 +1058,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), DownloadServiceLi
nav.visibility = if (!hideBottomNav) View.VISIBLE else nav.visibility nav.visibility = if (!hideBottomNav) View.VISIBLE else nav.visibility
if (nav == binding.sideNav) { if (nav == binding.sideNav) {
nav.isVisible = !hideBottomNav nav.isVisible = !hideBottomNav
updateControllersWithSideNavChanges(from)
nav.alpha = 1f nav.alpha = 1f
} else { } else {
animationSet?.cancel() animationSet?.cancel()
@ -1077,6 +1085,27 @@ open class MainActivity : BaseActivity<MainActivityBinding>(), 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<ViewGroup.MarginLayoutParams> {
marginStart = if (sideNav.isVisible) {
if (isRootController) 0 else -navWidth
} else {
if (isRootController) navWidth else 0
}
}
}
}
}
fun showTabBar(show: Boolean, animate: Boolean = true) { fun showTabBar(show: Boolean, animate: Boolean = true) {
tabAnimation?.cancel() tabAnimation?.cancel()
if (animate) { if (animate) {

View file

@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.databinding.MigrationProcessItemBinding
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder 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.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.view.setCards import eu.kanade.tachiyomi.util.view.setCards
@ -50,6 +51,9 @@ class MigrationProcessHolder(
fun bind(item: MigrationProcessItem) { fun bind(item: MigrationProcessItem) {
this.item = item this.item = item
launchUI { launchUI {
binding.migrationMangaCardFrom.setFreeformCoverRatio(item.manga.manga())
binding.migrationMangaCardTo.setFreeformCoverRatio(null)
val manga = item.manga.manga() val manga = item.manga.manga()
val source = item.manga.mangaSource() val source = item.manga.mangaSource()

View file

@ -75,10 +75,10 @@ class BrowseSourceItem(
binding.constraintLayout.minHeight = 0 binding.constraintLayout.minHeight = 0
binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP binding.coverThumbnail.scaleType = ImageView.ScaleType.CENTER_CROP
binding.coverThumbnail.adjustViewBounds = false binding.coverThumbnail.adjustViewBounds = false
binding.coverThumbnail.layoutParams = FrameLayout.LayoutParams( binding.coverThumbnail.updateLayoutParams<ConstraintLayout.LayoutParams> {
ViewGroup.LayoutParams.MATCH_PARENT, height = ConstraintLayout.LayoutParams.MATCH_CONSTRAINT
(parent.itemWidth / 3f * 3.7f).toInt() dimensionRatio = "15:22"
) }
} }
BrowseSourceGridHolder(view, adapter, listType == 1, outlineOnCovers.get()) BrowseSourceGridHolder(view, adapter, listType == 1, outlineOnCovers.get())
} else { } else {

View file

@ -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<Long, Float>()
private var coverColorMap = ConcurrentHashMap<Long, Pair<Int, Int>>()
val preferences by injectLazy<PreferencesHelper>()
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<Int, Int>? {
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())
}
}

View file

@ -34,6 +34,7 @@ import androidx.core.view.updatePaddingRelative
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
@ -579,6 +580,7 @@ fun Controller.moveRecyclerViewUp(allTheWayUp: Boolean = false, scrollUpAnyway:
val appBarOffset = activityBinding.appBar.toolbarDistanceToTop val appBarOffset = activityBinding.appBar.toolbarDistanceToTop
if (allTheWayUp && recycler.computeVerticalScrollOffset() - recycler.paddingTop <= fullAppBarHeight ?: activityBinding.appBar.preLayoutHeight) { if (allTheWayUp && recycler.computeVerticalScrollOffset() - recycler.paddingTop <= fullAppBarHeight ?: activityBinding.appBar.preLayoutHeight) {
(recycler.layoutManager as? LinearLayoutManager)?.scrollToPosition(0) (recycler.layoutManager as? LinearLayoutManager)?.scrollToPosition(0)
(recycler.layoutManager as? StaggeredGridLayoutManager)?.scrollToPosition(0)
recycler.post { recycler.post {
activityBinding.appBar.updateAppBarAfterY(recycler) activityBinding.appBar.updateAppBarAfterY(recycler)
activityBinding.appBar.useSearchToolbarForMenu(false) activityBinding.appBar.useSearchToolbarForMenu(false)
@ -588,6 +590,8 @@ fun Controller.moveRecyclerViewUp(allTheWayUp: Boolean = false, scrollUpAnyway:
if (scrollUpAnyway || recycler.computeVerticalScrollOffset() - recycler.paddingTop <= 0 - appBarOffset) { if (scrollUpAnyway || recycler.computeVerticalScrollOffset() - recycler.paddingTop <= 0 - appBarOffset) {
(recycler.layoutManager as? LinearLayoutManager) (recycler.layoutManager as? LinearLayoutManager)
?.scrollToPositionWithOffset(0, activityBinding.appBar.yNeededForSmallToolbar) ?.scrollToPositionWithOffset(0, activityBinding.appBar.yNeededForSmallToolbar)
(recycler.layoutManager as? StaggeredGridLayoutManager)
?.scrollToPositionWithOffset(0, activityBinding.appBar.yNeededForSmallToolbar)
recycler.post { recycler.post {
activityBinding.appBar.updateAppBarAfterY(recycler) activityBinding.appBar.updateAppBarAfterY(recycler)
activityBinding.appBar.useSearchToolbarForMenu(recycler.computeVerticalScrollOffset() != 0) activityBinding.appBar.useSearchToolbarForMenu(recycler.computeVerticalScrollOffset() != 0)

View file

@ -77,6 +77,7 @@ import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.pxToDp import eu.kanade.tachiyomi.util.system.pxToDp
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.StaggeredGridLayoutManagerAccurateOffset
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
@ -310,21 +311,25 @@ fun NavigationBarView.getItemView(@IdRes id: Int): NavigationBarItemView? {
fun RecyclerView.smoothScrollToTop() { fun RecyclerView.smoothScrollToTop() {
val linearLayoutManager = layoutManager as? LinearLayoutManager val linearLayoutManager = layoutManager as? LinearLayoutManager
if (linearLayoutManager != null) { val staggeredLayoutManager = layoutManager as? StaggeredGridLayoutManagerAccurateOffset
if (linearLayoutManager != null || staggeredLayoutManager != null) {
val smoothScroller: SmoothScroller = object : LinearSmoothScroller(context) { val smoothScroller: SmoothScroller = object : LinearSmoothScroller(context) {
override fun getVerticalSnapPreference(): Int { override fun getVerticalSnapPreference(): Int {
return SNAP_TO_START return SNAP_TO_START
} }
} }
smoothScroller.targetPosition = 0 smoothScroller.targetPosition = 0
val firstItemPos = linearLayoutManager.findFirstVisibleItemPosition() val firstItemPos = linearLayoutManager?.findFirstVisibleItemPosition()
?: staggeredLayoutManager?.findFirstVisibleItemPosition() ?: 0
if (firstItemPos > 15) { if (firstItemPos > 15) {
scrollToPosition(15) scrollToPosition(15)
post { post {
linearLayoutManager.startSmoothScroll(smoothScroller) linearLayoutManager?.startSmoothScroll(smoothScroller)
staggeredLayoutManager?.startSmoothScroll(smoothScroller)
} }
} else { } else {
linearLayoutManager.startSmoothScroll(smoothScroller) linearLayoutManager?.startSmoothScroll(smoothScroller)
staggeredLayoutManager?.startSmoothScroll(smoothScroller)
} }
} else { } else {
scrollToPosition(0) scrollToPosition(0)

View file

@ -3,10 +3,19 @@ package eu.kanade.tachiyomi.widget
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.doOnNextLayout
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.pxToDp
import eu.kanade.tachiyomi.util.system.rootWindowInsetsCompat
import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -14,7 +23,7 @@ import kotlin.math.roundToInt
class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
androidx.recyclerview.widget.RecyclerView(context, attrs) { androidx.recyclerview.widget.RecyclerView(context, attrs) {
val manager = GridLayoutManagerAccurateOffset(context, 1) var manager: LayoutManager = GridLayoutManagerAccurateOffset(context, 1)
var lastMeasuredWidth = 0 var lastMeasuredWidth = 0
var columnWidth = -1f var columnWidth = -1f
@ -29,20 +38,29 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att
set(value) { set(value) {
field = value field = value
if (value > 0) { if (value > 0) {
manager.spanCount = value managerSpanCount = value
} }
} }
val itemWidth: Int val itemWidth: Int
get() { get() {
return if (spanCount == 0) measuredWidth / getTempSpan() return if (spanCount == 0) measuredWidth / getTempSpan()
else measuredWidth / manager.spanCount else measuredWidth / managerSpanCount
} }
init { init {
layoutManager = manager 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 { private fun getTempSpan(): Int {
if (spanCount == 0 && columnWidth > 0) { if (spanCount == 0 && columnWidth > 0) {
val dpWidth = (measuredWidth.pxToDp / 100f).roundToInt() 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) { override fun onMeasure(widthSpec: Int, heightSpec: Int) {
super.onMeasure(widthSpec, heightSpec) super.onMeasure(widthSpec, heightSpec)
setSpan() 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) { fun setGridSize(preferences: PreferencesHelper) {
@ -85,8 +163,14 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att
} }
private fun setSpan(force: Boolean = false) { private fun setSpan(force: Boolean = false) {
if ((spanCount == 0 || force || measuredHeight != lastMeasuredWidth) && columnWidth > 0) { if ((
val dpWidth = (measuredWidth.pxToDp / 100f).roundToInt() 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()) val count = max(1, (dpWidth / columnWidth).roundToInt())
spanCount = count spanCount = count
} }

View file

@ -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)
}
}

View file

@ -39,6 +39,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/comfortable_grid" /> android:text="@string/comfortable_grid" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/cover_only_grid" />
</RadioGroup> </RadioGroup>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -102,6 +107,14 @@
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:text="@string/uniform_grid_covers" /> android:text="@string/uniform_grid_covers" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/staggered_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:text="@string/use_staggered_grid" />
<com.google.android.material.checkbox.MaterialCheckBox <com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/outline_on_covers" android:id="@+id/outline_on_covers"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -31,16 +31,58 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"> app:layout_constraintVertical_bias="1.0">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cover_constraint"
android:layout_width="match_parent"
android:background="?attr/background"
android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/behind_title"
style="?textAppearanceBodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:textColor="?colorOnBackground"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:textAlignment="center"
android:gravity="center"
android:maxLines="3"
app:layout_constraintTop_toTopOf="@id/cover_thumbnail"
app:layout_constraintBottom_toBottomOf="@id/cover_thumbnail"
app:layout_constraintEnd_toEndOf="@id/cover_thumbnail"
app:layout_constraintStart_toStartOf="@id/cover_thumbnail"
tools:text="Sample name" />
<ImageView <ImageView
android:id="@+id/cover_thumbnail" android:id="@+id/cover_thumbnail"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:alpha="0.5"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" tools:adjustViewBounds="true"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:background="?attr/background"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
app:layout_constraintVertical_bias="0.0"
tools:src="@mipmap/ic_launcher" /> tools:src="@mipmap/ic_launcher" />
<View
android:id="@+id/gradient"
android:layout_width="0dp"
android:layout_height="125sp"
android:alpha="0.75"
android:background="@drawable/gradient_shape"
app:layout_constraintStart_toStartOf="@id/cover_thumbnail"
app:layout_constraintEnd_toEndOf="@id/cover_thumbnail"
app:layout_constraintBottom_toBottomOf="@id/cover_thumbnail"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout <FrameLayout
android:id="@+id/play_layout" android:id="@+id/play_layout"
android:layout_width="50dp" android:layout_width="50dp"
@ -67,14 +109,6 @@
</FrameLayout> </FrameLayout>
<View
android:id="@+id/gradient"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_gravity="bottom"
android:alpha="0.75"
android:background="@drawable/gradient_shape" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/compact_title" android:id="@+id/compact_title"
style="?textAppearanceLabelMedium" style="?textAppearanceLabelMedium"

View file

@ -174,12 +174,14 @@
<string name="can_be_found_in_library_filters">Can also be found by expanding library filters</string> <string name="can_be_found_in_library_filters">Can also be found by expanding library filters</string>
<string name="list">List</string> <string name="list">List</string>
<string name="comfortable_grid">Comfortable Grid</string> <string name="comfortable_grid">Comfortable Grid</string>
<string name="cover_only_grid">Cover-only grid</string>
<string name="compact_grid">Compact Grid</string> <string name="compact_grid">Compact Grid</string>
<string name="download_badge">Download badges</string> <string name="download_badge">Download badges</string>
<string name="language_badge">Language badges</string> <string name="language_badge">Language badges</string>
<string name="hide_start_reading_button">Hide start reading button</string> <string name="hide_start_reading_button">Hide start reading button</string>
<string name="badges">Badges</string> <string name="badges">Badges</string>
<string name="uniform_grid_covers">Uniform grid covers</string> <string name="uniform_grid_covers">Uniform grid covers</string>
<string name="use_staggered_grid">Use staggered grid</string>
<string name="show_outline_around_covers">Show outline around covers</string> <string name="show_outline_around_covers">Show outline around covers</string>
<string name="grid_size">Grid size</string> <string name="grid_size">Grid size</string>
<string name="_per_row">%d per row</string> <string name="_per_row">%d per row</string>