Bottom Navigation APP

June 14, 2016 — 2 Comments

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)

 

android_portrait

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.

android_all_labels

On iOS most APPs don’t colorize the Bottom Navigation Bar, you can configure this:

ios_all_labels

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:

android_settings

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:

android_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:

android_landscape

 

iOS:

ios_landscape

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 ?

bottom_nav

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)

 

2 responses to Bottom Navigation APP

  1. 

    question: How is the popuptoast width/height defined? I don’t see them in the code instead of x,y

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s