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,