refactored the way the game carousel scrolls and animates. added snapping.
This commit is contained in:
+114
-10
@@ -100,17 +100,111 @@ QtObject {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// TODO: Tune this stuff !!! it feels just slightly janky and off for now, game cards don't align when scrolling, they go all the way out
|
||||
// to the edge with no spacing at all when on the leftmost or rightmost cards, and vertical navigation from the dock feels *wrong*, idk,
|
||||
// fix it sometime.
|
||||
height: 140
|
||||
// these properties are primarily for the gamesList snapping
|
||||
readonly property int cardWidth: 120
|
||||
readonly property int cardSpacing: 16
|
||||
readonly property int cardPitch: cardWidth + cardSpacing
|
||||
readonly property int safeLeft: 90
|
||||
readonly property int safeRight: width - 90
|
||||
readonly property int edgePeek: 98
|
||||
readonly property int snapOffset: cardWidth - edgePeek
|
||||
|
||||
property real scrollTarget: contentX
|
||||
|
||||
height: 200
|
||||
orientation: ListView.Horizontal
|
||||
preferredHighlightBegin: 200
|
||||
preferredHighlightEnd: 420
|
||||
highlightRangeMode: ListView.ApplyRange
|
||||
spacing: 20
|
||||
spacing: 16
|
||||
leftMargin: 16
|
||||
rightMargin: 16
|
||||
|
||||
model: 20 // this is a placeholder for now, I don't know how I really want to handle max game carousel size yet. another time.
|
||||
|
||||
// provides the smoothing animation for game selection
|
||||
NumberAnimation {
|
||||
id: scrollAnimation
|
||||
target: gamesList
|
||||
property: "contentX"
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
// used to scroll to a new game on the game list
|
||||
function scrollTo(targetX) {
|
||||
if (Math.abs(targetX - gamesList.contentX) < 0.5) {
|
||||
return
|
||||
}
|
||||
|
||||
scrollAnimation.stop()
|
||||
scrollAnimation.from = gamesList.contentX
|
||||
scrollAnimation.to = targetX
|
||||
scrollAnimation.start()
|
||||
}
|
||||
|
||||
// provides snapping logic so that the games list is snapped to a card pitch
|
||||
function snappedContentX(snapTargetX) {
|
||||
// set up a relative coord for our snapping logic, so we line up right
|
||||
let relativeTarget = snapTargetX - gamesList.originX
|
||||
|
||||
// finds the nearest point on the snap grid
|
||||
// it's pretty obtuse, so as an explanation: we subtract snapOffset first so the grid becomes a
|
||||
// simple 0, cardPitch, 2*cardPitch, etc grid. Divide by cardPitch to get the nearest grid index,
|
||||
// round it, then multiply by cardPitch to convert that index back to a position. Finally we
|
||||
// wanna add snapOffset (and originX) back to line it up with our true grid.
|
||||
let snappedTargetX = gamesList.originX + Math.round((relativeTarget - gamesList.snapOffset) / gamesList.cardPitch) * gamesList.cardPitch + gamesList.snapOffset
|
||||
|
||||
// calculate the furthest possible ends of the carousel
|
||||
// Math.max is for clamping it to make sure even if there are very few games on the board that it
|
||||
// wont return something stupid
|
||||
let minimum = gamesList.originX - gamesList.leftMargin
|
||||
let maximum = gamesList.originX + Math.max(0, gamesList.contentWidth + gamesList.leftMargin + gamesList.rightMargin - gamesList.width) - gamesList.leftMargin
|
||||
|
||||
// this section of code is here to make sure that when the game carousel is flicked all the way
|
||||
// to the leftmost or rightmost ends, it doesn't snap and cutoff the leftmost or rightmost
|
||||
// card in a way that feels janky. it works by calculating the distance from the current target
|
||||
// to the very edges, and if it's closer to one of the edges, just snap to the edge.
|
||||
let bestTarget = snappedTargetX
|
||||
let bestDistance = Math.abs(snapTargetX - snappedTargetX)
|
||||
|
||||
let distanceToMinimum = Math.abs(snapTargetX - minimum)
|
||||
|
||||
if (distanceToMinimum < bestDistance) {
|
||||
bestTarget = minimum
|
||||
bestDistance = distanceToMinimum
|
||||
}
|
||||
|
||||
let distanceToMaximum = Math.abs(snapTargetX - maximum)
|
||||
|
||||
if (distanceToMaximum < bestDistance) {
|
||||
bestTarget = maximum
|
||||
}
|
||||
|
||||
return Math.max(minimum, Math.min(bestTarget, maximum))
|
||||
}
|
||||
|
||||
// this is a function to make sure the games list scrolls, but in a predictable, nice-feeling way.
|
||||
// specifically, it scrolls but makes sure the games list is snapped to a specific card pitch.
|
||||
function ensureCurrentInSafeZone() {
|
||||
if (!gamesList.currentItem) {
|
||||
return
|
||||
}
|
||||
|
||||
let item = gamesList.currentItem
|
||||
|
||||
let itemLeft = item.x - gamesList.contentX
|
||||
let itemRight = itemLeft + item.width
|
||||
|
||||
let targetContentX = gamesList.contentX
|
||||
|
||||
if (itemLeft < gamesList.safeLeft) {
|
||||
targetContentX -= gamesList.safeLeft - itemLeft
|
||||
} else if (itemRight > gamesList.safeRight) {
|
||||
targetContentX += itemRight - gamesList.safeRight
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
gamesList.scrollTo(gamesList.snappedContentX(targetContentX))
|
||||
}
|
||||
|
||||
Component.onCompleted: { // on startup, this should select the first game in the list.
|
||||
if (currentItem) {
|
||||
@@ -118,11 +212,21 @@ QtObject {
|
||||
}
|
||||
}
|
||||
|
||||
// every time the index changes, we wanna re-calculate the game carousel's position
|
||||
onCurrentIndexChanged: {
|
||||
ensureCurrentInSafeZone()
|
||||
}
|
||||
|
||||
// after flicking, we re-snap the carousel wherever it landed
|
||||
onMovementEnded: {
|
||||
gamesList.scrollTo(gamesList.snappedContentX(gamesList.contentX))
|
||||
}
|
||||
|
||||
delegate: Rectangle { // TODO: design a real game card QML to replace this placeholder
|
||||
id: gameTile
|
||||
|
||||
width: 110
|
||||
height: 110
|
||||
width: 120
|
||||
height: 180
|
||||
|
||||
color: navigationManager.currentItem === gameTile ? "blue" : "white"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user