×
Namespaces

Variants
Actions

QML paging using ListView

From Nokia Developer Wiki
Jump to: navigation, search
Article Metadata
Compatibility
Platform(s):
Symbian
Article
Created: bluechrism (06 Mar 2011)
Last edited: hamishwillee (24 Jul 2012)

There are many uses for paging in mobile apps. One use is to create something like a desktop view - Symbian and Maemo both have multiple desktops which slide across one at a time when you swipe. Another is for something like a full screen view in an image gallery.

After I started investigating this I have found that there is an example of this in the QML SDK in the Qt Documentation at VisualItemModel example. There is also another Wiki describing how to create paging using a PathView at How to create a Page Control component in QML. However, I have focused on the ListView system and have taken that and expanded it further, but the principle is still the same. This method provides a simple framework for adding the List content dynamically.

In this example, we create an ImageViewer. The QML doesn't define any images explicitly, instead, the images are added dynamically via JavaScript.

Flicking the image viewer in Symbian 3 simulated

(Gaps between images due to image capture on N900 which has a different image size ratio to the N8 and should disappear with images taken on the device running the Image viewer)


Contents

Creating the shell

We begin with 3 items: In a file called ImageViewer.qml, we have created A Rectangle, A Visual Data Model and a List View. For an image viewer, we may want it to fill the screen so we set the view to be maximized and the ListView to fill it's parent. We also set the model property on the ListView to the VisualDataModel:

import QtQuick 1.0
 
Rectangle {
width: view.width
height: view.height
 
VisualDataModel {
id: dataModel
}
 
ListView {
id: view
anchors.fill: parent;
model: dataModel
orientation: ListView.Horizontal
}
}

Flickable behavior

We set up the scrolling on the ListView - we only want it to scroll / flick one image at a time so we set the snapMode to SnapOneItem and have a low flickDecelertation. The Higher the flickDecelertaion, the longer it will take to stop so having a low number reduces the chance of skipping images. The cacheBuffer is the number of pixels before and after the displayed item that the delegate will be retained. Setting this to it's width ensures that at lease the image delegate either side of the current one is retained.

 ListView {
id: view
anchors.fill: parent;
model: dataModel
orientation: ListView.Horizontal
snapMode: ListView.SnapOneItem;
flickDeceleration: 500
cacheBuffer: width;
}

Creating the dynamic data model

In the VisualDataModel, we create a Delegate and assign it's properties. No source property is set at this time, just width, height and fillMode -which means the image will fill the maximum area it can whist retaining it's aspect ratio (The Image.PreserveAspectFit fillMode setting below is the reason for the gaps in the Simulator screenshot above). We also create assign the model property to a new ListModel and give it an id:

 VisualDataModel {
id: dataModel
model: ListModel{
id:innerModel
}
 
 
delegate: Image {
width: view.width;
height: view.height;
fillMode: Image.PreserveAspectFit
}
}

Now we add some JavaScript. In a new JavaScript file we can create 2 methods: createImages() and createImage(url). createImages is called when the ImageViewer.qml file is executed and creates an array of image sources. for each image in the array we call createImage(url) to add it to the ListView. I have used a fixed array here, but you can create your array however you want.

function createImages()
{
var files = ["waterfall.jpg","lake.jpg",
"river.jpg","boats.jpg"];
var i = 0;
for (i=0; i<files.length; i++){
createImage(files[i]);
}
}
 
function createImage(name) {
innerModel.append({"url": name});
}

The reason for the ListModel inside the VisualDataModel is that the VisualDataModel doesn't provide an method to append data to it, where as the ListModel does. It is here where we create the url property for the Images to bind to. Again, if you are not doing images, add whatever properties are relevant for your object. Don't Forget: You need to initialize your index in your loop before the for loop declaration when using JavaScript with QML.

We have three tasks remaining. So go back to the QML file and in the Image delegate add the source property, and a import statement to include the JavaScript file: Lastly add the code to call createImages() when the application starts.

import QtQuick 1.0
import "ImageLoader.js" as ImageLoader
 
Rectangle {
width: view.width
height: view.height
 
Component.onCompleted: ImageLoader.createImages()
 
.....
 
delegate: Image {
width: view.width;
height: view.height;
source: url
}

Working with devices without a touch-screen

This means adding key handling to the scrolling can be done using the keyboard. To do that we need to keep track of the current index of the ListView and change it when keys are pressed. To track the current index in the ListView for either keyboard use or swiping, you need to add a couple of lines to the ListView object to do that:

 ListView {
id: view
anchors.fill: parent;
model: dataModel
orientation: ListView.Horizontal
snapMode: ListView.SnapOneItem;
flickDeceleration: 500
cacheBuffer: width;
preferredHighlightBegin: 0; preferredHighlightEnd: 0 //this line means that the currently highlighted item will be central in the view
highlightRangeMode: ListView.StrictlyEnforceRange //this means that the currentlyHighlightedItem will not be allowed to leave the view
highlightFollowsCurrentItem: true //updates the current index property to match the currently highlighted item
}

The next part is to add keyboard handling code that uses the index to move the image when certain keys are pressed. The following code will hook up the Left and Right d-pad buttons and should be added to the ListView below the code above:

        focus: true
Keys.onLeftPressed: {
if (currentIndex > 0 )
currentIndex = currentIndex-1;}
Keys.onRightPressed: {
if (currentIndex < count)
currentIndex = currentIndex+1;}

The focus line ensure the ListView has keyboard focus. OnLeftPressed and onRightPressed capture the keyboard button, and then by setting the currentIndex, because we have the highlight properties set in the previous code segment, the ListView will move the new selection to the centre of the screen. Before changing the index, we check to make sure it isn't already at the beginning or end of the list, so we add the if statement before we adjust the index.

Switching the iage using the DPad simulated

Making it a component

So now we have this, it would be nice if it were more reusable. So the thing to do here is to separate the paging ListView from the content it displays so you can re-use that ListView in many places. To do this we divide the visual paging logic from the paging content, by putting the ListView in a separate file, and making some properties public so the user has enough control over it for it to be useful. The VisualDataModel is removed, because it doesn't have to be a VisualDataModel, it could be some other sort of model like a ListModel. Once we have separated the code, we save the new component in the same folder as the qml file which will use it (or add a reference to it using an import statement), and add an instance of it to the main file.

So first off, the Paging component - Pager.qml

import QtQuick 1.0
 
Rectangle {
id:pager
anchors.fill: parent
 
property bool enableKeys: true
property QtObject model
property bool isHorizontal: false
 
property int index: view.currentIndex
property Item item: view.currentItem
 
signal indexChanged
 
 
ListView {
 
id: view
anchors.fill: pager
model: pager.model
orientation: if (isHorizontal){ListView.Horizontal;} else {ListView.Vertical}
snapMode: ListView.SnapOneItem;
flickDeceleration: 500
highlightFollowsCurrentItem: true
highlightRangeMode: ListView.StrictlyEnforceRange
preferredHighlightBegin: 0; preferredHighlightEnd: 0
cacheBuffer: width;
focus: pager.focus
onCurrentIndexChanged: pager.indexChanged()
Keys.onLeftPressed: {
if (enableKeys && isHorizontal && (currentIndex > 0 ))
currentIndex = currentIndex-1;}
Keys.onRightPressed: {
if (enableKeys && isHorizontal && (currentIndex < count))
currentIndex = currentIndex+1;}
Keys.onUpPressed: {
if (enableKeys && !isHorizontal && (currentIndex > 0 ))
currentIndex = currentIndex-1;}
Keys.onDownPressed: {
if (enableKeys && !isHorizontal && (currentIndex < count))
currentIndex = currentIndex+1;}
}
}

The base rectangle is set to fill the parent (the idea of a paging control is to fill the screen) and the ListView is set to fill the rectangle. Of course, you can override this by setting it to something else when using it in another file. The same goes for the default background color of black (as it's already the default, I haven't explicitly assigned it here), the focus, or any property that is natively on a rectangle. I have added properties to enable key bindings, pass in a mode, and set the orientation. I have also added properties for the index and item currently displayed which just pass those values from the ListView, and also pass on the currentIndexChanged signal.

In the ListView i have set the model to the property we declared, and added a bit of logic to use the isHorizontal property to set the list orientation. I have left the rest of the list set with options that are suitable for most Paging systems. The focus is important as this allows key bindings. focus is a property of rectangle so i have not explicitly defined it again, but I have pointed the ListView's focus property to use pager.focus. I have the same key binding logic as before, but with Up and Down logic added for when using vertical orientation, and a each of the key handlers first checks if key binding is enabled.

Now to use it:

import QtQuick 1.0
import "ImageLoader.js" as Loader
 
Rectangle {
width: 360
height: 640
color: "#FFFFFF"
Component.onCompleted: Loader.createImages()
 
Text {
anchors.top: parent.top
text: "Hello World"
color: "blue"
height: 30
}
Pager {
id: myPager
isHorizontal: false
model: dataModel
color: "blue"
enableKeys: true
focus: true
anchors {topMargin:30; fill:parent}
}
VisualDataModel {
id: dataModel
model: ListModel{
id:innerModel
}
 
delegate: Image {
width: myPager.width;
height: myPager.height;
source: url
fillMode: Image.PreserveAspectFit
}
}
}

The code above creates a similar interface to the image viewer we had before, but I added a hello world text box at the top and set the background blue, and it scrolls Vertically. The VisualDataModel is exactly the same as before, but now the Pager component is seen, just above it. I'm setting several of the properties I defined in Pager.qml, plus some native ones like color, anchors, and focus. The model property is set to the VisualDataModel so that separation of the layout vs the data is there. With focus being set true and keys on, this means that key bindings will work in non-touch screen devices with a d-pad.

Extending this system

This can be used as a component and added inside other objects, or used as an application on it's own. Now we are tracking the index, you could, displaying a number showing the page for a book application perhaps, or by creating a system similar to the Symbian desktop switching button. An example of the latter is shown in the VisualItemModel example.

Download this code

You can download the code for this from the link below. There are no images in this zip file so please add any images you want to the folder you extract it to, and edit ImageLoader.js to include the correct file names.

File:PagingWithListViewDemo.zip

This file contains:

  • ImageViewer.qmlproject - project file
  • ImageViewer.qml - QML code
  • ImageLoader.js - JavaScript file to get the image list
  • readme.txt - reminder to swap out the image file names in ImageLoader.js
This page was last modified on 24 July 2012, at 05:05.
336 page views in the last 30 days.