refactored the way the game carousel scrolls and animates. added snapping.
This commit is contained in:
+114
-10
@@ -100,29 +100,133 @@ QtObject {
|
|||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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
|
// these properties are primarily for the gamesList snapping
|
||||||
// to the edge with no spacing at all when on the leftmost or rightmost cards, and vertical navigation from the dock feels *wrong*, idk,
|
readonly property int cardWidth: 120
|
||||||
// fix it sometime.
|
readonly property int cardSpacing: 16
|
||||||
height: 140
|
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
|
orientation: ListView.Horizontal
|
||||||
preferredHighlightBegin: 200
|
spacing: 16
|
||||||
preferredHighlightEnd: 420
|
leftMargin: 16
|
||||||
highlightRangeMode: ListView.ApplyRange
|
rightMargin: 16
|
||||||
spacing: 20
|
|
||||||
|
|
||||||
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.
|
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.
|
Component.onCompleted: { // on startup, this should select the first game in the list.
|
||||||
if (currentItem) {
|
if (currentItem) {
|
||||||
navigationManager.focusItem(currentItem)
|
navigationManager.focusItem(currentItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
delegate: Rectangle { // TODO: design a real game card QML to replace this placeholder
|
||||||
id: gameTile
|
id: gameTile
|
||||||
|
|
||||||
width: 110
|
width: 120
|
||||||
height: 110
|
height: 180
|
||||||
|
|
||||||
color: navigationManager.currentItem === gameTile ? "blue" : "white"
|
color: navigationManager.currentItem === gameTile ? "blue" : "white"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user