Building a CoverFlow component with QML
Article Metadata
Code Example
Article
This article shows how to build a Cover Flow UI component (with flipable elements) using Qt Quick.
Contents |
Using PathView
A CoverFlow component is composed of multiple items placed on a (typically straight) path, with variable size and angle depending on their position on the path itself. Specifically:
- items closer to the path center are bigger in size, and with angle closer to zero
- items closer to the path boundaries are smaller in size, and the item face is oriented towards the path center
The component is contained into a root Rectangle defined as follows. It also defines three properties:
- itemWidth and itemHeight, that define the width and height of a single item of the CoverFlow
- listModel, that defines the model of the component's PathView
Rectangle {
id: coverFlow
property int itemWidth: 100
property int itemHeight: 100
property ListModel listModel
}
Within the root Rectangle, a PathView is defined, containing a Path composed of two straight PathLines.
The two PathLine's start from the component's left/right boundaries and end at its center.
Appropriate PathAttributes are used to correctly place and resize the items on the PathView. The following attributes are defined:
- angle - is +/- 60 at the Path boundaries, to orient the items towards the Path center, and is zero at the Path center
- iconScale - is 0.5 at the Path boundaries, to scale down icons, and is 1.0 at the Path center
- z - to place items at the boundaries below items closer to the center, a lower value is used at the boundaries, and a higher value is used at the center
PathView {
id: myPathView
anchors.fill: parent
preferredHighlightBegin: 0.5
preferredHighlightEnd: 0.5
focus: true
interactive: true
model: listModel
path: Path {
startX: 0
startY: coverFlow.height / 2
PathAttribute { name: "z"; value: 0 }
PathAttribute { name: "angle"; value: 60 }
PathAttribute { name: "iconScale"; value: 0.5 }
PathLine { x: coverFlow.width / 2; y: coverFlow.height / 2; }
PathAttribute { name: "z"; value: 100 }
PathAttribute { name: "angle"; value: 0 }
PathAttribute { name: "iconScale"; value: 1.0 }
PathLine { x: coverFlow.width; y: coverFlow.height / 2; }
PathAttribute { name: "z"; value: 0 }
PathAttribute { name: "angle"; value: -60 }
PathAttribute { name: "iconScale"; value: 0.5 }
}
}
To handle key navigation, the Keys onRightPressed and onLeftPressed handlers are defined as follows:
PathView {
id: myPathView
Keys.onRightPressed: if (!moving && interactive) incrementCurrentIndex()
Keys.onLeftPressed: if (!moving && interactive) decrementCurrentIndex()
[...]
}
The ListModel
The base structure of the ListModel used by the PathView delegate is defined as follows. Each ListElement has two properties:
- icon - is the image displayed in the PathView item
- name - will be used in the detail view of the PathView item
ListModel {
ListElement { name: "Google"; icon: "pics/0.png" }
ListElement { name: "YouTube"; icon: "pics/1.png" }
ListElement { name: "Facebook"; icon: "pics/2.png" }
[...]
}
The PathView delegate
Each item on the PathView consists of a Flipable object. The front side of the Flipable represents the item in its "normal" state, during user interaction with the PathView, while the back side will be used to display detail information about the single item, as shown in the image below.
The Flipable component base structure is defined as follows:
- width and height properties are bound to the component's itemWidth and itemHeight properties
- z and scale properties are bound to the Path's z' and iconScale attributes
- a Rotation element is used to rotate the item accordingly to the Path angle attribute
Component {
id: appDelegate
Flipable {
id: myFlipable
width: itemWidth; height: itemHeight
z: PathView.z
scale: PathView.iconScale
transform: Rotation {
id: rotation
origin.x: myFlipable.width/2
origin.y: myFlipable.height/2
axis.x: 0; axis.y: 1; axis.z: 0
angle: PathView.angle
}
front: Rectangle {
smooth: true
width: itemWidth; height: itemHeight
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: "black"
border.color: "white"
border.width: 3
Image {
id: myIcon
anchors.centerIn: parent
source: icon
smooth: true
}
}
back: Rectangle {
anchors.fill: parent
color: "black"
Text {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
color: "white"
text: "This is the back view for " + name
}
}
}
}
The PathView can be now modified to use this delegate Component:
PathView {
id: myPathView
[...]
delegate: appDelegate
}
The base CoverFlow component is now complete: sliding it left and right will move the icons with the classic CoverFlow effect. What is left to do is to show the detail view of each item, once it is clicked.
Showing the Flipable back view
First, a flipped bool property is added to the Flipable component: this property will hold the current item state (flipped or not).
property bool flipped: false
Then, a "back" State is declared, that will actually show the Flipable's back view: this is accomplished by modifying the angle of the Flipable rotation, setting it to 180.
The width and height properties are also changed, so that the back view, once shown, will take the whole PathView area.
states: State {
name: "back"
PropertyChanges { target: rotation; angle: 180 }
PropertyChanges {target: myFlipable; width: myPathView.width; height: myPathView.height }
when: myFlipable.flipped
}
To add a nice Animation during the state changes, a Transition consisting of two Animations is used:
- a NumberAnimation is used to animate the Rotation's angle
- another NumberAnimation is used to animate the Flipable's width and height
transitions: Transition {
ParallelAnimation {
NumberAnimation { target: rotation; property: "angle"; duration: 250 }
NumberAnimation {target: myFlipable; properties: "height,width"; duration: 250}
}
}
The JavaScript logic
The state changes are performed by a JavaScript function, that does the following:
- if the clicked item is now the PathView's current item, then set it to be the current item
- if the clicked item is the PathView's current item, then:
- switch the flipable property
- if the item is flipped (so, the back view is displayed) then set the PathView interactive to false, otherwise set it to true
function itemClicked()
{
if(PathView.isCurrentItem) {
myFlipable.flipped = !myFlipable.flipped
myPathView.interactive = !myFlipable.flipped
}
else if(myPathView.interactive) {
myPathView.currentIndex = index
}
}
The itemClicked() function must be called when the Flipable is clicked and, to handle key interaction, when the Return key is pressed. So, the Flipable is modified as follows:
Flipable {
id: myFlipable
[...]
Keys.onReturnPressed: itemClicked()
MouseArea {
anchors.fill: parent
onClicked: itemClicked()
}
}
Adding a signal to the component
It would be useful, for a QML application using the CoverFlow component defined above, to know when the PathView current item has changed. In order to do this, a new signal is defined by the component: indexChanged(int index). This signal must be called when the PathView currentIndex property changes: for this reason, it is enough to call it when the currentIndexChanged signal is called.
Rectangle {
id: coverFlow
[...]
signal indexChanged(int index)
Component.onCompleted: {
myPathView.currentIndexChanged.connect(function(){
indexChanged(myPathView.currentIndex);
})
}
}How to use the CoverFlow
The following video shows the CoverFlow component in action on a Nokia N8 device:
The media player is loading...
The code below shows how to use the CoverFlow component. The following steps are performed:
- a ListModel is defined, to be used by the PathView
- the indexChanged signal is used to show the index of the currently selected item
Rectangle {
width: 400; height: 240
ListModel {
id: appModel
ListElement { name: "Google"; icon: "pics/0.png" }
ListElement { name: "YouTube"; icon: "pics/1.png" }
ListElement { name: "Facebook"; icon: "pics/2.png" }
ListElement { name: "MySpace"; icon: "pics/3.png" }
ListElement { name: "Blogger"; icon: "pics/4.png" }
ListElement { name: "Flickr"; icon: "pics/5.png" }
ListElement { name: "WordPress"; icon: "pics/6.png" }
ListElement { name: "Technorati"; icon: "pics/7.png" }
ListElement { name: "Heart"; icon: "pics/8.png" }
ListElement { name: "Twitter"; icon: "pics/9.png" }
ListElement { name: "Yahoo"; icon: "pics/10.png" }
ListElement { name: "DesignFloat"; icon: "pics/11.png" }
ListElement { name: "Reddit"; icon: "pics/12.png" }
ListElement { name: "Stumbleupon"; icon: "pics/13.png" }
ListElement { name: "Delicious"; icon: "pics/14.png" }
ListElement { name: "Digg"; icon: "pics/15.png" }
ListElement { name: "RSS"; icon: "pics/16.png" }
}
Text {
id: myText
anchors.bottom: parent.bottom
text: "current"
anchors.horizontalCenter: parent.horizontalCenter
}
CoverFlow {
listModel: appModel
width: parent.width
anchors.top: parent.top
anchors.bottom: myText.top
onIndexChanged: {
myText.text = "Current index: " + index
}
itemWidth: 120
itemHeight: 120
color: "lightblue"
}
}
Related content
The full source code presented in this article is available here, as a complete Qt Creator project: File:CoverFlowQuickApp.zip



(no comments yet)