Hi everyone
Here is the thing - I'm planning to release a cards game soon (regular patience games like Solitaire, Freecell etc.) developped with QtQuick and C++ on SailfishOS. I have a small problem when I want to animate nicely my cards. I use dynamical binding of anchors.top and anchors.left properties on my QML Card objects; indeed, the top anchor of a given card is the top anchor of its parent card on the stack (or the top anchor of the stack if the card is alone on a tableau), same goes for the left anchor. When dragging a card begins, the JS binding in Card sets these anchors to "undefined" to be able to freely drag the card wherever you want. Also, in y Card properties, I put a behavior on anchors.left and anchors.right to animate the anchor changes with AnchorAnimation.
Here is the part of Card.qml that is interesting:
Flipable {
id: card
property int cid
property QtObject parentCard
property QtObject parentStack
property bool isValueShown
property bool isHint
property bool canMove
property bool dragged: false
Drag.active: dragArea.drag.active
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Behavior on anchors.top { AnchorAnimation { duration: 1000 } }
Behavior on anchors.left { AnchorAnimation { duration: 1000 } }
function aParentIsDragged() {
// returns true if any ancestor is being dragged, false otherwise
}
height: 150
width: height * 0.7333
back: Rectangle {
anchors.fill: parent
// card back graphics...
}
front: DropArea {
enabled: !(!isValueShown || dragged || aParentIsDragged())
anchors.fill: parent
onDropped: {
gameInterface.move(drag.source.cid, parentStack.id)
}
// only card graphics here...
}
transform: Rotation {
id: rotation
origin.x: card.width / 2.0
origin.y: card.height / 2.0
axis.x: 0; axis.y: 1; axis.z: 0 // set axis.y to 1 to rotate around y-axis
angle: -180 // the default angle
}
states: [
State {
name: "visible"
when: card.isValueShown
PropertyChanges {
target: rotation
angle: 0
}
}
]
transitions: Transition {
NumberAnimation { target: rotation; property: "angle"; duration: 75 }
}
MouseArea {
enabled: canMove
id: dragArea
anchors.fill: parent
drag.target: parent
onPressed: dragged = true
onCanceled: dragged = false
onReleased: {
parent.Drag.drop()
dragged = false
}
}
}
Flipable {
id: card
property int cid
property QtObject parentCard
property QtObject parentStack
property bool isValueShown
property bool isHint
property bool canMove
property bool dragged: false
Drag.active: dragArea.drag.active
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Behavior on anchors.top { AnchorAnimation { duration: 1000 } }
Behavior on anchors.left { AnchorAnimation { duration: 1000 } }
function aParentIsDragged() {
// returns true if any ancestor is being dragged, false otherwise
}
height: 150
width: height * 0.7333
back: Rectangle {
anchors.fill: parent
// card back graphics...
}
front: DropArea {
enabled: !(!isValueShown || dragged || aParentIsDragged())
anchors.fill: parent
onDropped: {
gameInterface.move(drag.source.cid, parentStack.id)
}
// only card graphics here...
}
transform: Rotation {
id: rotation
origin.x: card.width / 2.0
origin.y: card.height / 2.0
axis.x: 0; axis.y: 1; axis.z: 0 // set axis.y to 1 to rotate around y-axis
angle: -180 // the default angle
}
states: [
State {
name: "visible"
when: card.isValueShown
PropertyChanges {
target: rotation
angle: 0
}
}
]
transitions: Transition {
NumberAnimation { target: rotation; property: "angle"; duration: 75 }
}
MouseArea {
enabled: canMove
id: dragArea
anchors.fill: parent
drag.target: parent
onPressed: dragged = true
onCanceled: dragged = false
onReleased: {
parent.Drag.drop()
dragged = false
}
}
}
To copy to clipboard, switch view to plain text mode
And here the part of Solitaire.qml generating the cards:
Repeater {
id: cards
property bool ready: false
model: gameInterface.cards
delegate: Card {
cid: modelData.id
parentCard: modelData.parent
parentStack: modelData.cardStack
value: modelData.value
color: modelData.color
isValueShown: modelData.isVisible
isHint: modelData.isHint
canMove: modelData.canMove
z: {
if (cards.ready)
((parentCard) ? cards.itemAt(parentCard.id).z + 1 : modelData.depth) + 100 * dragged
else
modelData.depth
}
anchors.top: (dragged || !cards.ready) ? undefined : ((parentCard) ? cards.itemAt(parentCard.id).top : stacks[parentStack.id].top) // stacks are components defined just above
anchors.left: (dragged || !cards.ready) ? undefined : ((parentCard) ? cards.itemAt(parentCard.id).left : stacks[parentStack.id].left)
anchors.topMargin: {
if (!parentCard)
0
else if (stacks[parentStack.id].type === CCardStack.FANNED_DOWN)
parentCard.isVisible * 23 + !parentCard.isVisible * 10
else if (stacks[parentStack.id].type === CCardStack.FANNED_UP)
parentCard.isVisible * -23 + !parentCard.isVisible * -10
else
0
}
anchors.leftMargin: {
if (!parentCard)
0
else if (stacks[parentStack.id].type === CCardStack.FANNED_RIGHT)
parentCard.isVisible * 23 + !parentCard.isVisible * 10
else if (stacks[parentStack.id].type === CCardStack.FANNED_LEFT)
parentCard.isVisible * -23 + !parentCard.isVisible * -10
else
0
}
}
onItemAdded: if (index + 1 == count) ready = true // I find this a little dirty, but it seems that Component.onCompleted is fired BEFORE the repeater has instanciated all its Card's
}
Repeater {
id: cards
property bool ready: false
model: gameInterface.cards
delegate: Card {
cid: modelData.id
parentCard: modelData.parent
parentStack: modelData.cardStack
value: modelData.value
color: modelData.color
isValueShown: modelData.isVisible
isHint: modelData.isHint
canMove: modelData.canMove
z: {
if (cards.ready)
((parentCard) ? cards.itemAt(parentCard.id).z + 1 : modelData.depth) + 100 * dragged
else
modelData.depth
}
anchors.top: (dragged || !cards.ready) ? undefined : ((parentCard) ? cards.itemAt(parentCard.id).top : stacks[parentStack.id].top) // stacks are components defined just above
anchors.left: (dragged || !cards.ready) ? undefined : ((parentCard) ? cards.itemAt(parentCard.id).left : stacks[parentStack.id].left)
anchors.topMargin: {
if (!parentCard)
0
else if (stacks[parentStack.id].type === CCardStack.FANNED_DOWN)
parentCard.isVisible * 23 + !parentCard.isVisible * 10
else if (stacks[parentStack.id].type === CCardStack.FANNED_UP)
parentCard.isVisible * -23 + !parentCard.isVisible * -10
else
0
}
anchors.leftMargin: {
if (!parentCard)
0
else if (stacks[parentStack.id].type === CCardStack.FANNED_RIGHT)
parentCard.isVisible * 23 + !parentCard.isVisible * 10
else if (stacks[parentStack.id].type === CCardStack.FANNED_LEFT)
parentCard.isVisible * -23 + !parentCard.isVisible * -10
else
0
}
}
onItemAdded: if (index + 1 == count) ready = true // I find this a little dirty, but it seems that Component.onCompleted is fired BEFORE the repeater has instanciated all its Card's
}
To copy to clipboard, switch view to plain text mode
Sadly, the behavior is somehow not triggered... When I begin to drag, "dragged" is set to true so anchors are modified to "undefined", the card should smoothly detach from its parent and join the cursor position. And more importantly, when I drop a card on another one, I it should attach itself to its new stack smoothly too. But none of these are happening for now, and I don't know why :/ I even tried in using transitions and states, and not behavior, but I had no luck neither (moreover, latency was WAY more present with this approach, so this is not a solution, would it have worked). Notice that the margins and anchors are correctly handled (ie. the cards have always their anchors correctly updated).
Would you have some ideas on how I could make these animations possible ? Or maybe did I do something wrong ?
Thank you very much in advance!
Bookmarks