mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-30 02:16:06 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d77c14a1a4 |
@@ -47,7 +47,6 @@ import info.debatty.java.stringsimilarity.Jaccard
|
|||||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.view.doOnNextLayout
|
|
||||||
|
|
||||||
class GamesFragment : Fragment() {
|
class GamesFragment : Fragment() {
|
||||||
private var _binding: FragmentGamesBinding? = null
|
private var _binding: FragmentGamesBinding? = null
|
||||||
@@ -59,7 +58,6 @@ 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
|
|
||||||
private var pendingPostReloadListSettle = false
|
private var pendingPostReloadListSettle = false
|
||||||
private var pendingPostReloadListSettleGeneration = 0
|
private var pendingPostReloadListSettleGeneration = 0
|
||||||
private var gameListSubmitGeneration = 0
|
private var gameListSubmitGeneration = 0
|
||||||
@@ -227,12 +225,7 @@ 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) {
|
||||||
(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)?.setupCarousel(false)
|
(this as? CarouselRecyclerView)?.setupCarousel(false)
|
||||||
}
|
}
|
||||||
adapter = gameAdapter
|
adapter = gameAdapter
|
||||||
@@ -590,11 +583,6 @@ class GamesFragment : Fragment() {
|
|||||||
qlaunchButton.layoutParams = mlpQLaunch
|
qlaunchButton.layoutParams = mlpQLaunch
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ import androidx.recyclerview.widget.PagerSnapHelper
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.pow
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||||
import androidx.core.view.doOnNextLayout
|
import androidx.core.view.doOnNextLayout
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
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
|
||||||
|
import org.yuzu.yuzu_emu.utils.FullscreenHelper
|
||||||
/**
|
/**
|
||||||
* CarouselRecyclerView encapsulates all carousel content 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,
|
||||||
@@ -32,7 +35,9 @@ 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 bottomInset: Int = 0
|
||||||
|
private var latestWindowInsets: WindowInsetsCompat? = null
|
||||||
|
private var cardGeometryInitialized: Boolean = false
|
||||||
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
|
||||||
@@ -91,6 +96,38 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
setChildrenDrawingOrderEnabled(true)
|
setChildrenDrawingOrderEnabled(true)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets ->
|
||||||
|
latestWindowInsets = insets
|
||||||
|
updateCardGeometry()
|
||||||
|
applyCarouselPadding()
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
ViewCompat.requestApplyInsets(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
if (w != oldw || h != oldh) {
|
||||||
|
updateCardGeometry()
|
||||||
|
applyCarouselPadding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom)
|
||||||
|
if (isCarouselMode) updateChildScalesAndAlpha()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
if (hasFocus) {
|
||||||
|
ViewCompat.requestApplyInsets(this)
|
||||||
|
post { updateCardGeometry() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setAdapter(adapter: Adapter<*>?) {
|
override fun setAdapter(adapter: Adapter<*>?) {
|
||||||
@@ -103,6 +140,8 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
super.setAdapter(adapter)
|
super.setAdapter(adapter)
|
||||||
|
|
||||||
(adapter as? GameAdapter)?.registerAdapterDataObserver(carouselAdapterObserver)
|
(adapter as? GameAdapter)?.registerAdapterDataObserver(carouselAdapterObserver)
|
||||||
|
updateCardGeometry()
|
||||||
|
applyCarouselPadding()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateCenter(width: Int, paddingStart: Int, paddingEnd: Int): Int {
|
private fun calculateCenter(width: Int, paddingStart: Int, paddingEnd: Int): Int {
|
||||||
@@ -253,40 +292,71 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyInsetsReady(newBottomInset: Int) {
|
private fun resolveBottomInset(windowInsets: WindowInsetsCompat): Int {
|
||||||
if (bottomInset != newBottomInset) {
|
val navigationBottom = if (FullscreenHelper.isFullscreenEnabled(context)) {
|
||||||
bottomInset = newBottomInset
|
0
|
||||||
}
|
|
||||||
|
|
||||||
if (isCarouselMode) {
|
|
||||||
setupCarousel(true)
|
|
||||||
} else {
|
} else {
|
||||||
setupCarousel(false)
|
windowInsets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||||
}
|
}
|
||||||
|
val gestureInsets = windowInsets.getInsetsIgnoringVisibility(
|
||||||
|
WindowInsetsCompat.Type.systemGestures()
|
||||||
|
)
|
||||||
|
val cutoutInsets = windowInsets.getInsetsIgnoringVisibility(
|
||||||
|
WindowInsetsCompat.Type.displayCutout()
|
||||||
|
)
|
||||||
|
return maxOf(navigationBottom, gestureInsets.bottom, cutoutInsets.bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notifyLaidOut(fallBackBottomInset: Int) {
|
private fun updateCardGeometry() {
|
||||||
if (bottomInset < 0) bottomInset = fallBackBottomInset
|
if (!isCarouselMode || height <= 0) return
|
||||||
var gameAdapter = adapter as? GameAdapter ?: return
|
|
||||||
var newCardSize = cardSize(bottomInset)
|
|
||||||
if (gameAdapter.cardSize != newCardSize) {
|
|
||||||
gameAdapter.setCardSize(newCardSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCarouselMode) {
|
val gameAdapter = adapter as? GameAdapter ?: return
|
||||||
setupCarousel(true)
|
val windowInsets = latestWindowInsets ?: ViewCompat.getRootWindowInsets(this) ?: return
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cardSize(bottomInset: Int): Int {
|
if (cardGeometryInitialized && !hasWindowFocus()) return
|
||||||
|
|
||||||
|
val newBottomInset = resolveBottomInset(windowInsets).coerceIn(0, height)
|
||||||
val internalFactor = resources.getFraction(R.fraction.carousel_card_size_factor, 1, 1)
|
val internalFactor = resources.getFraction(R.fraction.carousel_card_size_factor, 1, 1)
|
||||||
val userFactor = preferences.getFloat(CAROUSEL_CARD_SIZE_FACTOR, internalFactor).coerceIn(
|
val userFactor = preferences.getFloat(CAROUSEL_CARD_SIZE_FACTOR, internalFactor).coerceIn(
|
||||||
0f,
|
0f,
|
||||||
1f
|
1f
|
||||||
)
|
)
|
||||||
val scaledHeight = height * userFactor
|
val screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||||
val availableHeight = height - bottomInset
|
val screenHeight = resources.displayMetrics.heightPixels.toFloat()
|
||||||
return minOf(scaledHeight.toInt(), availableHeight.toInt())
|
val aspectFactor = ((screenWidth / screenHeight) / (20f / 9f))
|
||||||
|
.pow(0.75f)
|
||||||
|
.coerceIn(0.5f, 1f)
|
||||||
|
val newCardSize = minOf(
|
||||||
|
(height * userFactor).toInt(),
|
||||||
|
height - newBottomInset,
|
||||||
|
(height * aspectFactor).toInt()
|
||||||
|
)
|
||||||
|
if (newCardSize <= 0) return
|
||||||
|
val insetChanged = bottomInset != newBottomInset
|
||||||
|
val cardSizeChanged = gameAdapter.cardSize != newCardSize
|
||||||
|
|
||||||
|
bottomInset = newBottomInset
|
||||||
|
cardGeometryInitialized = true
|
||||||
|
|
||||||
|
if (cardSizeChanged) gameAdapter.setCardSize(newCardSize)
|
||||||
|
if (insetChanged || cardSizeChanged) setupCarousel(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyCarouselPadding() {
|
||||||
|
if (!isCarouselMode) return
|
||||||
|
|
||||||
|
val gameAdapter = adapter as? GameAdapter ?: return
|
||||||
|
val cardSize = gameAdapter.cardSize
|
||||||
|
if (cardSize <= 0 || bottomInset < 0) return
|
||||||
|
|
||||||
|
val topPadding = ((height - bottomInset - cardSize) / 2).coerceAtLeast(0)
|
||||||
|
val sidePadding = (width - cardSize) / 2
|
||||||
|
if (paddingLeft != sidePadding || paddingTop != topPadding ||
|
||||||
|
paddingRight != sidePadding || paddingBottom != 0
|
||||||
|
) {
|
||||||
|
setPadding(sidePadding, topPadding, sidePadding, 0)
|
||||||
|
}
|
||||||
|
clipToPadding = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupCarousel(enabled: Boolean) {
|
fun setupCarousel(enabled: Boolean) {
|
||||||
@@ -315,9 +385,6 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
internalFlingMultiplier
|
internalFlingMultiplier
|
||||||
).coerceIn(1f, 5f)
|
).coerceIn(1f, 5f)
|
||||||
|
|
||||||
// Detach SnapHelper during setup
|
|
||||||
pagerSnapHelper?.attachToRecyclerView(null)
|
|
||||||
|
|
||||||
// Add overlap decoration if not present
|
// Add overlap decoration if not present
|
||||||
if (overlapDecoration == null) {
|
if (overlapDecoration == null) {
|
||||||
overlapDecoration = OverlappingDecoration(overlapPx)
|
overlapDecoration = OverlappingDecoration(overlapPx)
|
||||||
@@ -335,12 +402,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
addOnScrollListener(scalingScrollListener!!)
|
addOnScrollListener(scalingScrollListener!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cardSize > 0) {
|
applyCarouselPadding()
|
||||||
val topPadding = ((height - bottomInset - cardSize) / 2).coerceAtLeast(0) // Center vertically
|
|
||||||
val sidePadding = (width - cardSize) / 2 // Center first/last card
|
|
||||||
setPadding(sidePadding, topPadding, sidePadding, 0)
|
|
||||||
clipToPadding = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pagerSnapHelper == null) {
|
if (pagerSnapHelper == null) {
|
||||||
pagerSnapHelper = CenterPagerSnapHelper()
|
pagerSnapHelper = CenterPagerSnapHelper()
|
||||||
@@ -362,6 +424,7 @@ class CarouselRecyclerView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
savedItemAnimator = null
|
savedItemAnimator = null
|
||||||
}
|
}
|
||||||
|
cardGeometryInitialized = false
|
||||||
useCustomDrawingOrder = false
|
useCustomDrawingOrder = false
|
||||||
// Reset padding and fling
|
// Reset padding and fling
|
||||||
setPadding(0, 0, 0, 0)
|
setPadding(0, 0, 0, 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user