READY TO MERGE [android] fix for carousel late bottominset and one single game bugs (#3028)

kleidis found a rare condition that pops when using gesture navigation, in which by the lack of bottom inset availability in time, carousel sizes get oversized. then i've put some non zero value backup to cover.

Co-authored-by: Allison Cunha <allisonbzk@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3028
Reviewed-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
This commit is contained in:
xbzk 2025-11-20 19:19:14 +01:00 committed by crueter
parent 41192e6e3d
commit 65fa1a37e2
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
2 changed files with 59 additions and 46 deletions

View File

@ -53,6 +53,7 @@ class GamesFragment : Fragment() {
private var originalHeaderLeftMargin: Int? = null private var originalHeaderLeftMargin: Int? = null
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
private var fallbackBottomInset: Int = 0
companion object { companion object {
private const val SEARCH_TEXT = "SearchText" private const val SEARCH_TEXT = "SearchText"
@ -208,12 +209,12 @@ class GamesFragment : Fragment() {
else -> throw IllegalArgumentException("Invalid view type: $savedViewType") else -> throw IllegalArgumentException("Invalid view type: $savedViewType")
} }
if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) { if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) {
doOnNextLayout { (binding.gridGames as? View)?.let { it -> ViewCompat.requestApplyInsets(it)}
(this as? CarouselRecyclerView)?.setCarouselMode(true, gameAdapter) doOnNextLayout { //Carousel: important to avoid overlap issues
adapter = gameAdapter (this as? CarouselRecyclerView)?.notifyLaidOut(fallbackBottomInset)
} }
} else { } else {
(this as? CarouselRecyclerView)?.setCarouselMode(false) (this as? CarouselRecyclerView)?.setupCarousel(false)
} }
adapter = gameAdapter adapter = gameAdapter
lastViewType = savedViewType lastViewType = savedViewType
@ -237,9 +238,8 @@ class GamesFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) { if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
(binding.gridGames as? CarouselRecyclerView)?.restoreScrollState( (binding.gridGames as? CarouselRecyclerView)?.setupCarousel(true)
gamesViewModel.lastScrollPosition (binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition)
)
} }
} }
@ -494,6 +494,11 @@ class GamesFragment : Fragment() {
mlpFab.rightMargin = rightInset + fabPadding mlpFab.rightMargin = rightInset + fabPadding
binding.addDirectory.layoutParams = mlpFab binding.addDirectory.layoutParams = mlpFab
val navInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
val gestureInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
val bottomInset = maxOf(navInsets.bottom, gestureInsets.bottom, cutoutInsets.bottom)
fallbackBottomInset = bottomInset
(binding.gridGames as? CarouselRecyclerView)?.notifyInsetsReady(bottomInset)
windowInsets windowInsets
} }
} }

View File

@ -20,9 +20,8 @@ import androidx.core.view.doOnNextLayout
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
/** /**
* CarouselRecyclerView encapsulates all carousel logic for the games UI. * CarouselRecyclerView encapsulates all carousel content for the games UI.
* It manages overlapping cards, center snapping, custom drawing order, * It manages overlapping cards, center snapping, custom drawing order,
* joypad & fling navigation and mid-screen swipe-to-refresh. * joypad & fling navigation and mid-screen swipe-to-refresh.
*/ */
@ -34,6 +33,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
private var overlapFactor: Float = 0f private var overlapFactor: Float = 0f
private var overlapPx: Int = 0 private var overlapPx: Int = 0
private var bottomInset: Int = -1
private var overlapDecoration: OverlappingDecoration? = null private var overlapDecoration: OverlappingDecoration? = null
private var pagerSnapHelper: PagerSnapHelper? = null private var pagerSnapHelper: PagerSnapHelper? = null
private var scalingScrollListener: OnScrollListener? = null private var scalingScrollListener: OnScrollListener? = null
@ -202,46 +202,61 @@ class CarouselRecyclerView @JvmOverloads constructor(
} }
} }
fun setCarouselMode(enabled: Boolean, gameAdapter: GameAdapter? = null) { fun refreshView() {
updateChildScalesAndAlpha()
focusCenteredCard()
}
fun notifyInsetsReady(newBottomInset: Int) {
if (bottomInset != newBottomInset) {
bottomInset = newBottomInset
}
setupCarousel(true)
}
fun notifyLaidOut(fallBackBottomInset: Int) {
if (bottomInset < 0) bottomInset = fallBackBottomInset
var gameAdapter = adapter as? GameAdapter ?: return
var newCardSize = cardSize(bottomInset)
if (gameAdapter.cardSize != newCardSize) {
gameAdapter.setCardSize(newCardSize)
}
setupCarousel(true)
}
fun cardSize(bottomInset: Int): Int {
val internalFactor = resources.getFraction(R.fraction.carousel_card_size_factor, 1, 1)
val userFactor = preferences.getFloat(CAROUSEL_CARD_SIZE_FACTOR, internalFactor).coerceIn(
0f,
1f
)
return (userFactor * (height - bottomInset)).toInt()
}
fun setupCarousel(enabled: Boolean) {
if (enabled) { if (enabled) {
val gameAdapter = adapter as? GameAdapter ?: return
if (gameAdapter.cardSize == 0) return
if (bottomInset < 0) return
useCustomDrawingOrder = true useCustomDrawingOrder = true
val cardSize = gameAdapter.cardSize
val insets = rootWindowInsets?.let { WindowInsetsCompat.toWindowInsetsCompat(it, this) } val internalOverlapFactor = resources.getFraction(R.fraction.carousel_overlap_factor,1,1)
val bottomInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom ?: 0 overlapFactor = preferences.getFloat(CAROUSEL_OVERLAP_FACTOR, internalOverlapFactor).coerceIn(0f,1f)
val internalFactor = resources.getFraction(R.fraction.carousel_card_size_factor, 1, 1)
val userFactor = preferences.getFloat(CAROUSEL_CARD_SIZE_FACTOR, internalFactor).coerceIn(
0f,
1f
)
val cardSize = (userFactor * (height - bottomInset)).toInt()
gameAdapter?.setCardSize(cardSize)
val internalOverlapFactor = resources.getFraction(
R.fraction.carousel_overlap_factor,
1,
1
)
overlapFactor = preferences.getFloat(CAROUSEL_OVERLAP_FACTOR, internalOverlapFactor).coerceIn(
0f,
1f
)
overlapPx = (cardSize * overlapFactor).toInt() overlapPx = (cardSize * overlapFactor).toInt()
val internalFlingMultiplier = resources.getFraction( val internalFlingMultiplier = resources.getFraction(R.fraction.carousel_fling_multiplier,1,1)
R.fraction.carousel_fling_multiplier,
1,
1
)
flingMultiplier = preferences.getFloat( flingMultiplier = preferences.getFloat(
CAROUSEL_FLING_MULTIPLIER, CAROUSEL_FLING_MULTIPLIER,
internalFlingMultiplier internalFlingMultiplier
).coerceIn(1f, 5f) ).coerceIn(1f, 5f)
gameAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { gameAdapter .registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() { override fun onChanged() {
if (pendingScrollAfterReload) { if (pendingScrollAfterReload) {
post { doOnNextLayout {
jigglyScroll() refreshView()
pendingScrollAfterReload = false pendingScrollAfterReload = false
} }
} }
@ -257,7 +272,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
addItemDecoration(overlapDecoration!!) addItemDecoration(overlapDecoration!!)
} }
// Gradual scalingAdd commentMore actions // Gradual scaling on scroll
if (scalingScrollListener == null) { if (scalingScrollListener == null) {
scalingScrollListener = object : OnScrollListener() { scalingScrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@ -315,8 +330,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
super.scrollToPosition(position) super.scrollToPosition(position)
(layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(position, overlapPx) (layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(position, overlapPx)
doOnNextLayout { doOnNextLayout {
updateChildScalesAndAlpha() refreshView()
focusCenteredCard()
} }
} }
@ -382,12 +396,6 @@ class CarouselRecyclerView @JvmOverloads constructor(
return sorted[i].first return sorted[i].first
} }
fun jigglyScroll() {
scrollBy(-1, 0)
scrollBy(1, 0)
focusCenteredCard()
}
inner class OverlappingDecoration(private val overlap: Int) : ItemDecoration() { inner class OverlappingDecoration(private val overlap: Int) : ItemDecoration() {
override fun getItemOffsets( override fun getItemOffsets(
outRect: Rect, outRect: Rect,