From 65fa1a37e27579db54b14904a732dab785eebf99 Mon Sep 17 00:00:00 2001 From: xbzk Date: Thu, 20 Nov 2025 19:19:14 +0100 Subject: [PATCH] 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 Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3028 Reviewed-by: Caio Oliveira Reviewed-by: MaranBr Reviewed-by: Lizzie Co-authored-by: xbzk Co-committed-by: xbzk --- .../org/yuzu/yuzu_emu/ui/GamesFragment.kt | 19 ++-- .../yuzu_emu/views/CarouselRecyclerView.kt | 86 ++++++++++--------- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt index 80055628e1..106bf3faa2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt @@ -53,6 +53,7 @@ class GamesFragment : Fragment() { private var originalHeaderLeftMargin: Int? = null private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID + private var fallbackBottomInset: Int = 0 companion object { private const val SEARCH_TEXT = "SearchText" @@ -208,12 +209,12 @@ class GamesFragment : Fragment() { else -> throw IllegalArgumentException("Invalid view type: $savedViewType") } if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) { - doOnNextLayout { - (this as? CarouselRecyclerView)?.setCarouselMode(true, gameAdapter) - adapter = gameAdapter + (binding.gridGames as? View)?.let { it -> ViewCompat.requestApplyInsets(it)} + doOnNextLayout { //Carousel: important to avoid overlap issues + (this as? CarouselRecyclerView)?.notifyLaidOut(fallbackBottomInset) } } else { - (this as? CarouselRecyclerView)?.setCarouselMode(false) + (this as? CarouselRecyclerView)?.setupCarousel(false) } adapter = gameAdapter lastViewType = savedViewType @@ -237,9 +238,8 @@ class GamesFragment : Fragment() { override fun onResume() { super.onResume() if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) { - (binding.gridGames as? CarouselRecyclerView)?.restoreScrollState( - gamesViewModel.lastScrollPosition - ) + (binding.gridGames as? CarouselRecyclerView)?.setupCarousel(true) + (binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition) } } @@ -494,6 +494,11 @@ class GamesFragment : Fragment() { mlpFab.rightMargin = rightInset + fabPadding 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 } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt index 2c35e7349a..33ca9ff328 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/views/CarouselRecyclerView.kt @@ -20,9 +20,8 @@ import androidx.core.view.doOnNextLayout import org.yuzu.yuzu_emu.YuzuApplication import androidx.preference.PreferenceManager 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, * joypad & fling navigation and mid-screen swipe-to-refresh. */ @@ -34,6 +33,7 @@ class CarouselRecyclerView @JvmOverloads constructor( private var overlapFactor: Float = 0f private var overlapPx: Int = 0 + private var bottomInset: Int = -1 private var overlapDecoration: OverlappingDecoration? = null private var pagerSnapHelper: PagerSnapHelper? = 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) { - useCustomDrawingOrder = true + val gameAdapter = adapter as? GameAdapter ?: return + if (gameAdapter.cardSize == 0) return + if (bottomInset < 0) return - val insets = rootWindowInsets?.let { WindowInsetsCompat.toWindowInsetsCompat(it, this) } - val bottomInset = insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.bottom ?: 0 - 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) + useCustomDrawingOrder = true + val cardSize = gameAdapter.cardSize - val internalOverlapFactor = resources.getFraction( - R.fraction.carousel_overlap_factor, - 1, - 1 - ) - overlapFactor = preferences.getFloat(CAROUSEL_OVERLAP_FACTOR, internalOverlapFactor).coerceIn( - 0f, - 1f - ) + 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() - val internalFlingMultiplier = resources.getFraction( - R.fraction.carousel_fling_multiplier, - 1, - 1 - ) + val internalFlingMultiplier = resources.getFraction(R.fraction.carousel_fling_multiplier,1,1) flingMultiplier = preferences.getFloat( CAROUSEL_FLING_MULTIPLIER, internalFlingMultiplier ).coerceIn(1f, 5f) - gameAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + gameAdapter .registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onChanged() { if (pendingScrollAfterReload) { - post { - jigglyScroll() + doOnNextLayout { + refreshView() pendingScrollAfterReload = false } } @@ -257,7 +272,7 @@ class CarouselRecyclerView @JvmOverloads constructor( addItemDecoration(overlapDecoration!!) } - // Gradual scalingAdd commentMore actions + // Gradual scaling on scroll if (scalingScrollListener == null) { scalingScrollListener = object : OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -315,8 +330,7 @@ class CarouselRecyclerView @JvmOverloads constructor( super.scrollToPosition(position) (layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(position, overlapPx) doOnNextLayout { - updateChildScalesAndAlpha() - focusCenteredCard() + refreshView() } } @@ -382,12 +396,6 @@ class CarouselRecyclerView @JvmOverloads constructor( return sorted[i].first } - fun jigglyScroll() { - scrollBy(-1, 0) - scrollBy(1, 0) - focusCenteredCard() - } - inner class OverlappingDecoration(private val overlap: Int) : ItemDecoration() { override fun getItemOffsets( outRect: Rect,