Overview
This is part 3 of a series of demo apps helping you to understand Qt 5.7 for x-platform development on Android and iOS.
You should have read the blogs about my first app using Qt 5.7 Material Style and my second app (StackView Example) before reading on.
This time I’ll explain HowTo use SwipeView – one of the new Qt Quick Controls 2.
Coming from BlackBerry10 Cascades ? SwipeView is one of the less things I ever missed at Cascades. In Apps where I had to implement a Wizard going through some Pages back and force I used Container visibility to make this work.
You should have installed Qt 5.7 RC.
New to Qt 5 development ? Please read my blog series from the beginning to learn some basic stuff and to understand this app.
From screenshot above you know about the Business Rules for this SwipeView Example App:
- SwipeView is used to implement a Wizard
- There are 5 Pages the User must go through from 1 –> 2 –> 3 –> 4 –> 5
- A Page must be valid before going to the next Page
- Validation can be simulated for each Page using the Floating Action Button (FAB)
Also – same as for StackView Example App –
- When a Page is loaded: call init()
- Before destroyed: call cleanup()
The Source
This app can be downloaded as Open Source from github: https://github.com/ekke/swiped_pages_x
Project structure and .pro
The project structure is similar to my StackView Example App – we’re using also 5 Pages here for SwipeView to make it easy for you to learn about the different ways to navigate through a bunch of Pages. Pages are named same as for the StackView app, but sourcecode is slightly different.
main.cpp and ApplicationUI (C++) are also similar to the other apps (One Page Example App, StackView Example App)
ApplicationWindow (main.qml)
main.qml imports, ApplicationWindow and header are similar to stacked-pages-x app – but header is using another ToolBar from Sources/common/SwipeTextTitle:
header: SwipeTextTitle { id: titleBar text: navPane.currentItem? navPane.currentItem.title : qsTr("A simple Swiped - Pages APP") }
SwipeTextTitle’s text is bound to currentItem from SwipeView and text changes if another Page becomes the current page. The ‘BACK’ ToolButton is slightly different to StackTextTitle:
ToolButton { enabled: navPane.currentIndex > 0 focusPolicy: Qt.NoFocus Image { id: backImageImage visible: navPane.currentIndex > 0 anchors.centerIn: parent source: "qrc:/images/"+iconOnPrimaryFolder+"/arrow_back.png" } onClicked: { navPane.onePageBack() } }
BACK Button will only work if currentIndex > 0 – so on the very first Page the Button disappears.
The options menu (three dots at right side from ToolBar) looks same as for StackView Example:
- switch between dark and light theme
- select primary color
- select accent color
main.qml also contains the Popup to select the colors – already described in one-page-x app.
Four other controls we’ll find in main.qml:
- SwipeView
- PageIndicator
- Floating Action Button (FAB)
- PopupInfo
ApplicationWindow –> FAB
The FAB always stays on top on a fixed position and is defining in main.qml besides SwipeView.
FloatingActionButton { id: fab visible: !navPane.pageValidation[navPane.currentIndex] property string imageName: "/done.png" z: 1 anchors.margins: 16 anchors.right: parent.right anchors.bottom: parent.bottom imageSource: "qrc:/images/"+iconOnPrimaryDarkFolder+imageName backgroundColor: primaryDarkColor onClicked: { // simulating check isValid() // only if valid next page can be swiped to navPane.pageValidation[navPane.currentIndex] = 1 navPane.validationChanged() visible = false // Attention: since 5.7 RC you MUST RESET FOCUS // otherwise Shortcuts not working well resetFocus() } } // FAB SwipeView { id: navPane signal validationChanged() // ... currentIndex: 0 // currentIndex is the NEXT index swiped to onCurrentIndexChanged: { fab.visible = !pageValidation[currentIndex] // ... } } // we can loose the focus if Menu or Popup is opened function resetFocus() { navPane.focus = true }
Please notice: if clicked on FAB you must reset the Focus to use Shortcuts properly. This is new with 5.7 RC.
Tapping on the FAB does some tasks:
- Sets current Page as valid: navPane.pageValidation[navPane.currentIndex] = 1
- Emits a SIGNAL: validationChanged()
- Makes FAB invisible for current Page
Please take a look at FAB visibility: Tapping on FAB makes the Button invisible, swiping to another Page changes currentIndex and Visibility will be checked again for current Page.
Explore the sourcecode for the Pages: they’re connected to the SIGNAL to enable/disable Buttons. Here’s from PageTwo.qml:
Connections { target: navPane onValidationChanged: { button3.enabled = navPane.pageValidation[1] button5.enabled = navPane.pageValidation[2] && navPane.pageValidation[3] } }
ApplicationWindow –> PageIndicator
It’s a good idea to show the User the current Page and how many Pages exist.
Simply bind a PageIndicator to your SwipeView:
PageIndicator { id: pageIndicator count: navPane.count currentIndex: navPane.currentIndex anchors.bottom: navPane.bottom anchors.horizontalCenter: parent.horizontalCenter }
See PageIndicator and FAB / enabled-disabled Buttons in action:
ApplicationWindow –> SwipeView
Our Example uses 5 Pages:
In most cases your Pages will be defined as static Pages as described in the SwipeView docs.
I have developed some Business Apps for Enterprise Customers where Wizards are really complex with more or less business logic and many controls inside. My goal is always to develop performant apps, so I’m always looking to do it a dynamic way:
Only instantiate Controls / Components you really need or have to show in UI.
In StackView Example App we have seen that StackView manages this dynamic behavior for you and loads / unloads Pages as they’re pushed / popped.
A SwipeView works different: it’s like placing some sheets of paper side by side and you move the content right/left.
SwipeView Example App uses Loader to be dynamic.
Dynamic SwipeView -> Loader
Here’s a short overview of Dynamic SwipeView using Loader:
While swiping through the Pages only the currently visible Page plus one Page back and one Page ahead are loaded. The image above shows a snapshot while Page 3 (index 2) is currentItem: Page 2 and Page 4 are loaded and Page 1 and Page 5 are unloaded.
Swiping in-action:
Here’s one of the Loader:
Notice: the Page inside a Loader must be accessed via item property if loaded.
Hint: All Properties stored inside a Page will be destroyed if unloaded. That’s the reason why the validation property is stored outside – in this case at StackView:
// 0: validation failed, 1: validation done property var pageValidation: [0,0,0,0,0]
The Loader for Page 3 (Index 2):
Loader { // index 2 id: pageThreeLoader property string title: active? item.title:"..." active: navPane.currentIndex == 1 || navPane.currentIndex == 2 || navPane.currentIndex == 3 source: "pages/PageThree.qml" onLoaded: item.init() }
‘active‘ is bound to currentIndex and – in this case – is active when index 1, 2 or 3 (Page 2, 3 or 4)
As soon as a Page gets loaded, the init() function was called.
Unfortunately there’s no ‘aboutToUnload’ or so to call the cleanup() function. Instead I’m using Component.onDestruction inside the Page:
Component.onDestruction: { cleanup() }
SwipeView – Navigation Back and Forward
Similar to StackView Example you can go different ways to navigate through the Pages:
- click on a (Raised or Flat) Button to go to next or previous Page
- click on ‘Back’ from ToolBar to go one Page back
- click on Android System ‘Back’ key to go one Page back
- swipe using your fingers to go back or forward
- use Shortcuts (see StackView example about Shortcuts)
Unfortunately there’s no SIGNAL or so to know that User reached first or last Page. (Will do Bugreport)
Using Android BACK, ToolBar BACK or Shortcuts I implemented a Popup to let the user know that first or last püage reached:
ApplicationWindow –> PopupInfo
PopupInfo is defined in main.qml:
PopupInfo { id: popupInfo onAboutToHide: { popupInfo.stopTimer() resetFocus() } } // popupInfo
You’ll find PopupInfo under /common/PopupInfo.qml:
Popup { id: popup property alias text: popupLabel.text property alias buttonText: okButton.text // ... the Controls onAboutToHide: { stopTimer() } onAboutToShow: { closeTimer.start() } Timer { id: closeTimer interval: 3000 repeat: false onTriggered: { popup.close() } } function stopTimer() { closeTimer.stop() } } // popup
PopupInfo uses a Timer to auto-hide if User doesn’t tap on the Popup or outside.
Coming from BlackBerry10 Cascades ? Timer (QTimer) is available as a Type without any extra declaration. Also there’s no attachedObjects[].
PopupInfo also is used while swiping if next Page isn’t valid:
onCurrentIndexChanged: { fab.visible = !pageValidation[currentIndex] if (lastIndex == currentIndex) { return } if (lastIndex > currentIndex) { // swiping back lastIndex = currentIndex return } // swiping forward for (var i = lastIndex; i <= currentIndex; i++) { if(!pageValidation[i]) { if(i < currentIndex) { pageNotValid(i+1) lastIndex = i currentIndex = i return } } } // for lastIndex = currentIndex } // ... function pageNotValid(pageNumber) { popupInfo.text = qsTr("Page %1 not valid.\nPlease tap 'Done' Button","").arg(pageNumber) popupInfo.buttonText = qsTr("So Long, and Thx For All The Fish") popupInfo.open() }
Please explore the sources – this blog article is only a snapshot.
Summary
This app is only using some of the powerful features SwipeView provides to you for navigation through some pages. Remember: this is not a production-ready app.
When to use a SwipeView and when to use a StackView ?
SwipeViews are great if you’re following a well-defined path from Page-to-Page as explained for a Wizard here.
StackView is great when user decides wich Page should be pushed on Top or removed from.
Using a StackView or SwipeView is not the only ways to navigate – stay tuned for the next Example App using a TabBar.
> Please notice: if clicked on FAB you must reset the Focus to use Shortcuts properly. This is new with 5.7 RC.
Buttons and sliders now have a “strong” (tab & click) focus policy by default. To disable click focus, you can set either “focusPolicy: Qt.NoFocus” or “focusPolicy: Qt.TabFocus” depending whether you want to keep tab focus: http://doc-snapshots.qt.io/qt5-5.7/qml-qtquick-controls2-control.html#focusPolicy-prop
thanks for the info – will change this.
With Qt Quick Controls 2.1 in Qt 5.8, it will be easier to unload pages that are outside the reach:
SwipeView {
Repeater {
model: 6
Loader {
active: SwipeView.isCurrentItem || SwipeView.isNextItem || SwipeView.isPreviousItem // <==
sourceComponent: Text {
text: index
Component.onCompleted: console.log("created:", index)
Component.onDestruction: console.log("destroyed:", index)
}
}
}
}
great ! this really sounds cool.