Overview
This is part 5 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, my second app (StackView Example) the third app (SwipeView Example) and 4th app (Tab Bar Example) before reading on.
All the navigation through your APP was done using a single root object:
- Page
- StackView
- SwipeView
- TabBar (with SwipeView)
What if an app will become more complex, where you need some tabs at root, where each tab can itself point to a Page, StackView, SwipeView or TabBar ?
For a long time there was only the Drawer sliding in from the left side. (per ex. used by Google Play APP)
Fortunately Google just introduced a new navigation component: Bottom Navigation. Bottom Navigation uses a ToolBar at the bottom with minimum 3 Buttons and maximum 5 Buttons. You only have two areas ? Use a TabBar, you have more then 5 areas ? Use a Drawer.
There’s a ToolBar with ToolButtons as part of new Qt Quick Controls 2, but no Bottom Navigation Bar. Don’t panic ! It’s easy to customize Qt Quick Controls to have your own Bottom Navigation Control.
Coming from BlackBerry10 Cascades ? TabbedPane is similar to the Drawer. Having only some Tabs, you can set showTabsOnActionBar: true to get a Bottom Navigation. Coming from iOS ? A Bottom Navigation Bar is used as the default navigation, so it could be the way for x-platform development with Bottom Navigation if 5 Buttons are enough.
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 Google Material Style Guide you know that there are many ways to style the Navigation Bar. I have tried to implement all these ways in this example APP. Here’s a screenshot using a (with Primary Color) colored Bottom Bar, where Buttons have white or black Icons. (Depends from Primary Color: Open Options Menu and select Yellow as primary Color: all Icons will become black)
Notice that the inactive Buttons are dimmed.
Having 4 or 5 Buttons, only the active Button will show the Label, having 3 Buttons will always show the Label where the Fontsize for inactive Buttons is smaller.
On iOS most APPs don’t colorize the Bottom Navigation Bar, you can configure this:
Without using a Primary Color Background where Icons are white, now the active Button is colored with Primary Color, inactive are black or white (depends from Theme) with reduced Opacity.
Settings and Toast (Popups)
Tapping on the FAB (Floating Action Button) a Popup with some Settings appears:
Try out the different configurations. How this kind of stuff works, I already have explained in previous Example APPs.
Settings uses ‘OK’ as default action – even if you tap outside the Popup to close. It’s up to you how you’ll implement such behavior – depends from use-case or customer requirements. One of my Enterprise customers has the requirement, that all Popups, Dialogs, … have a default key which will be executed if closed or auto-closed after a specified timeout. So I wanted to try out this, too and implemented a ‘Toast‘. Toast is used in Android to let the user know that something happened and Toast is closed automatically.
here’s the source /popups/PopupToast.qml:
Popup { id: popup closePolicy: Popup.NoAutoClose bottomMargin: isLandscape? 24 : 80 x: (appWindow.width - width) / 2 y: (appWindow.height - height) background: Rectangle{ color: toastColor radius: 24 opacity: toastOpacity } Timer { id: toastTimer interval: 3000 repeat: false onTriggered: { popup.close() } } // toastTimer Label { id: toastLabel leftPadding: 16 rightPadding: 16 font.pixelSize: 16 color: "white" } // toastLabel onAboutToShow: { toastTimer.start() } function start(toastText) { toastLabel.text = toastText if(!toastTimer.running) { open() } else { toastTimer.restart() } } // function start } // popup toastPopup
A Timer is used to close the Toast. If a new message should be displayed before first timeout reached, a ‘restart()‘ is done. It’s important to use restart() instead of start() to be sure that timeout is reset.
Using a Toast is easy done. From Settings – if something was modified – a Toast is shown:
PopupSettings { id: popupSettings onAboutToHide: { if(popupSettings.update()) { popupToast.start(qsTr("Settings modified")) } else { resetFocus() } } } // popupSettings // PopupToast PopupToast { id: popupToast onAboutToHide: { resetFocus() } }
Here’s the Toast:
SideBar Navigation
Using a Bottom Navigation Bar in Landscape occupies worthful space and doesn’t really look good with only 3 – 5 Buttons.
For Tablets and Desktop Google recommends to use a SideBar – so I implented a SideBar which will be shown automagically in Landscape.
Android:
iOS:
In Landscape the TitleBar is hidden completely or floating. How a floating TitleBar is implemented I explained in previous Example.
Bottom Navigation
How’s the Bottom Navigation done ?
This is the data model:
property var navigationModel: [{"name": "Car", "icon": "car.png", "source": "../pages/PageOne.qml"}, {"name": "Bus", "icon": "bus.png", "source": "../pages/PageTwo.qml"}, {"name": "Subway", "icon": "subway.png", "source": "../pages/PageThree.qml"}, {"name": "Truck", "icon": "truck.png", "source": "../pages/PageFour.qml"}, {"name": "Flight", "icon": "flight.png", "source": "../pages/PageFive.qml"}] property int navigationIndex: 0 onNavigationIndexChanged: { rootPane.activeDestination(navigationIndex) }
From this data model the ToolBar can be created:
// The Bar Pane { id: myBar z: 1 // ... height: 56 background: Rectangle { color: primaryColor } RowLayout { focus: false anchors.left: parent.left anchors.right: parent.right spacing: 0 Repeater { model: navigationModel NavigationButton { id: myButton isColored: false } } // repeater } // RowLayout } // bottomNavigationBar // The Buttons ToolButton { id: myButton // ... property bool isActive: index == navigationIndex // ... height: 56 width: myBar.width / navigationModel.length Column { spacing: 0 topPadding: myButton.isActive || !suppressInactiveLabels? 0 : 6 anchors.horizontalCenter: parent.horizontalCenter Item { anchors.horizontalCenter: parent.horizontalCenter width: 24 height: 24 Image { id: contentImage width: 24 height: 24 verticalAlignment: Image.AlignTop anchors.horizontalCenter: parent.horizontalCenter source: "qrc:/images/"+myIconFolder+"/"+modelData.icon opacity: isActive? myBar.activeOpacity : myBar.inactiveOpacity } ColorOverlay { id: colorOverlay visible: myButton.isColored && myButton.isActive anchors.fill: contentImage source: contentImage color: primaryColor } } // image and coloroverlay Label { visible: myButton.isActive || !suppressInactiveLabels anchors.horizontalCenter: parent.horizontalCenter text: modelData.name opacity: isColored? (isActive? 1.0 : 0.7) : (isActive? myBar.activeOpacity : myBar.inactiveOpacity) color: isColored? (isActive? primaryColor : flatButtonTextColor) : textOnPrimary font.pixelSize: myButton.isActive? fontSizeActiveNavigationButton : fontSizeInactiveNavigationButton } // label } // column onClicked: { navigationIndex = index } } // myButton
To show the destination from Button clicked, a StackView is used. This StackView always only shows one Control, which will be replaced if ToolButton changes index. In this Example APP we only use Pages – in a real-life-app you’ll replace StackView root object with StackView, SwipeView, TabBar etc.
Please note: There’s a InitialItemPage – this Page will show a BusyIndicator to be sure that there’s something visible at startup. Probably you won’t see this Page because the first Page will be loaded fast enough, but in real-life-apps it could take some time to instantiate a complex node.
The StackView:
StackView { id: rootPane focus: true // anchors...... initialItem: InitialItemPage{} replaceEnter: Transition { PropertyAnimation { property: "opacity" from: 0 to:1 duration: 300 } } replaceExit: Transition { PropertyAnimation { property: "opacity" from: 1 to:0 duration: 300 } } // support of BACK key // ... see sourcecode // some Shortcuts // see sourcecode Repeater { id: destinations model: navigationModel Destination { id: destinationLoader } Component.onCompleted: { destinations.itemAt(0).active = true } } function firstDestinationLoaded() { fab.visible = true } function activeDestination(navigationIndex) { if(destinations.itemAt(navigationIndex).status == Loader.Ready) { rootPane.replace(destinations.itemAt(navigationIndex).item) } else { destinations.itemAt(navigationIndex).active = true } } } // rootPane
StackView creates Destinations – a Destination is a Loader.
Loader { id: pageLoader active: false source: modelData.source onLoaded: { item.init() rootPane.replace(item) if(index == 0) { rootPane.firstDestinationLoaded() } } }
All Destinations are inactive at the beginning. As soon as the Repeater has created all Destinations, the first one will become active: true, which will cause the source to be instantiated. From signal loaded() the Item will replace the current root item from StackView.
In this Example APP all Destinations are following same policy and created lazy the first time used and then will stay instantiated whole app lifecycle. Later in more complex Drawer Example we’ll use different policies.
StackView has no currentIndex. To manage Destinations from Button clicked we set navigationIndex – so it’s easy to know if a Button is active: index == navigationIndex, where index is automatically provided from Repeater.
Summary
You learned HowTo customize Qt Quick Controls 2 to implement a Bootom Navigation Bar or Side Navigation Bar.
Using Bottom Navigation, a TabBar, a StackView or a SwipeView are not the only ways to navigate – stay tuned for the next Example App using a sliding Drawer together with Pages, StackView, SwipeView and TabBar.
← Back (Tab Bar APP)
→ Next Article (Drawer Navigation App)
⇐ Home (Overview / Topics)
question: How is the popuptoast width/height defined? I don’t see them in the code instead of x,y
take a look here: https://doc.qt.io/qt-5.11/qml-qtquick-controls2-popup.html#popup-layout
>The implicitWidth and implicitHeight of a popup are typically based on the implicit sizes of the background and the content item plus any >padding. These properties determine how large the popup will be when no explicit width or height is specified.