Create the UI

Now that we've set up our project, we can start creating our UI.

Create the overall layout

In your project, open the main.qml file, which is where we put the QML code for the UI of our app. Remove the pre-populated code that's included in the file and start coding from a blank file.

We start by creating a Page with a Container that displays the main content of our UI, a MapView.

We use a DockLayout to format our Container so that we can align the contents of the container in the UI. For more information about using layouts, see Layouts.

We also include some key import statements so that we can use these libraries in our UI. For now, our MapView just has a set of starting coordinates and an ID. We make sure to give the MapView an objectName, which lets us populate the coordinates from C++.

import bb.cascades 1.0
import bb.cascades.maps 1.0
import QtMobility.sensors 1.2
import QtMobilitySubset.location 1.1

Page {
    // The main contents of the UI
    Container {
        id: root

        layout: DockLayout {
                }
        MapView {
            id: mapview
            objectName: "mapViewObj"
            altitude: 3000
            latitude: 43.449488
            longitude: -80.406777
        } // End of MapView
    } // End of main UI Container
} // End of Page 

We want to monitor the signals that are emitted when the user performs some actions on the map. Signals are emitted when the user taps the map or elements on the map. You can provide more details about a location when these signals are emitted.

In our app, we put the details in a status button or in a few specific labels. We add the following signal handlers to our MapView:

        MapView {
            id: mapview
            objectName: "mapViewObj"
            altitude: 3000
            latitude: 43.449488
            longitude: -80.406777

            onAltitudeChanged: {
              alt.setText(qsTr("Alt: %1").arg(newAlt));
            }
            onHeadingChanged: {
              heading.setText(qsTr("Heading: %1\u00B0").arg(newHeading));
            }
            onLatitudeChanged: {
              lat.setText(qsTr("Lat: %1").arg(newLat));
            }
            onLongitudeChanged: {
              lon.setText(qsTr("Lon: %1").arg(newLon));
            }
            onTiltChanged: {
              tilt.setText(qsTr("Tilt: %1\u00B0").arg(newTilt));
            }
            onMapLongPressed: {
              status.setText(qsTr("map long pressed"));
            }

            onFollowedIdChanged: {
              status.setText(qsTr("followed id changed to %1")
                .arg(idOfFollowed));
            }
            onFocusedIdChanged: {
              status.setText(qsTr("focused id changed to %1")
                .arg(idWithFocus));
            }
            onCaptionButtonClicked: {
              status.setText(qsTr("button clicked %1").arg(focusedId));
            }
            onCaptionLabelTapped: {
              status.setText(qsTr("label clicked %1").arg(focusedId));
            }
            onLocationTapped: {
              status.setText(qsTr("location tapped %1").arg(id));
            }
            onLocationLongPressed: {
              status.setText(qsTr("location long pressed %1").arg(id));
            }
        } // End of MapView

After the MapView, we add a container that holds two more containers for our UI. The first container has a group of labels that are updated with information about the map. The second container has a status label that is updated if the user performs an action on the map.

        Container {
            horizontalAlignment: HorizontalAlignment.Fill
            verticalAlignment: VerticalAlignment.Top
            topPadding: 5
            leftPadding: 5
            bottomPadding: 5
            background: Color.create("#ddffffff")


    
            // This container contains labels that show the latitude,
            // longitude, altitude, heading, and tilt of the map.
            Container {
                layout: StackLayout {
                    orientation: LayoutOrientation.LeftToRight
                }
                horizontalAlignment: HorizontalAlignment.Center
                
                Label {

                    id: lat
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
                Label {
                    id: lon
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
                Label {
                    id: alt
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
                Label {
                    id: heading
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
                Label {
                    id: tilt
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
            } // End of the Container of map characteristics labels

            // This container holds the status label 
            Container {
                horizontalAlignment: HorizontalAlignment.Center
                Label {
                    id: status
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Gray
                        fontWeight: FontWeight.Bold
                    }
                }
            } // End of the status label container
        } // End of the label container 

Now we add a container to place the pins on the map. This container uses the overlapTouchPolicy property to allow it to sit on top of the MapView and send touch events to the MapView.

The overlapTouchPolicy property is part of VisualNode. By default, when you place two controls in the same position so that they overlap, any touch events are received only by the container that's in the foreground; the background container doesn't receive touch events.

In our app, it makes more sense for the MapView, instead of the pin, to handle touch events. By specifying OverlapTouchPolicy.Allow for the overlapTouchPolicy property, we ensure that touch events pass through the overlay image and are handled by the MapView. For more information, see Overlap touch policies.

        
        Container {
            leftPadding: 20
            rightPadding: 20
            bottomPadding: 20
            topPadding: 20
            horizontalAlignment: HorizontalAlignment.Right
            verticalAlignment: VerticalAlignment.Bottom
            overlapTouchPolicy: OverlapTouchPolicy.Allow
            

        } // End of the Container for the pins

In the container for the pins, we need to add the compass and a toggle button to allow the user to follow their device's location on their map. The compass is an ImageView that is animated by using an ImplicitAnimationController. To learn more about animations, see Animations.

The ToggleButton is added to this Container so that we can let the user decide if they want to follow the device's location. The checkChanged() signal is used to set the followedId property of our MapView. The followedId is the geographic element that is currently being followed on our map.

            
            ImageView {
                id: compassImage
                imageSource: "asset:///images/compass.png"
                horizontalAlignment: HorizontalAlignment.Center
                attachedObjects: [
                    ImplicitAnimationController {
                        // Disable animations to avoid 
                        // jumps between 0 and 360 degree
                        enabled: false
                    }
                ]
            }

            ToggleButton {
                id: sensorToggle
                horizontalAlignment: HorizontalAlignment.Center
                checked: true
                onCheckedChanged: {
                    if (checked) {
                        mapview.setFollowedId("device-location-id");
                    } else {
                        mapview.setFollowedId("");
                    }
                }
                onCreationCompleted: {
                    mapview.setFollowedId("device-location-id");
                }
            }

Receive sensor and position source updates

One of the features of this mapping app is that it responds to changes to the device heading and rotation. When the device heading changes, the heading of the map view changes accordingly. When the device is rotated toward or away from the user, the app tilts the view of the map so that the user can still view it.

To receive updates on the device rotation and heading, we add a Compass and RotationSensor to our app as attachedObjects.

When the reading changes on the RotationSensor, we use the setTilt() function to tilt the view of the map in our UI. When the reading changes on the Compass, we use the setHeading() function to set the heading of the view of the map (in degrees). For more information, see Sensors.

    // The main content of the page
    Container {
        id: root

    ... 

    } // End of main UI Container

    attachedObjects: [
        RotationSensor {
            id: rotation
            property real x: 0
            active: sensorToggle.checked
            alwaysOn: false
            skipDuplicates: true
            onReadingChanged: {
                x = reading.x - 30
                if (x <= 40 && x > 0) {
                    mapview.setTilt(x);
                }
            }
        },
        Compass {
            property double azimuth: 0
            active: sensorToggle.checked
            axesOrientationMode: Compass.UserOrientation
            alwaysOn: false
            // Called when a new compass reading is available
            onReadingChanged: {
                mapview.setHeading(reading.azimuth);
                compassImage.rotationZ = 360 - reading.azimuth;
            }
        },
        PositionSource {
            id: positionSource
            updateInterval: 1000
            active: sensorToggle.checked
            onPositionChanged: {
                _mapViewTest.updateDeviceLocation(
                    positionSource.position.coordinate.latitude, 
                    positionSource.position.coordinate.longitude);
            }
        }
    ]
} // End of Page

Add the actions on the page

To finish our UI, we need to return to the Page and add an application menu that lets the user drop a pin, remove all of their pins, or center the map on the screen. For now, we limit the maps that we display to two locations (Waterloo and Manhattan).

import bb.cascades 1.0
import bb.cascades.maps 1.0

Page {
    actions: [
        ActionItem {
            title: qsTr("Drop Pin")
            imageSource: "asset:///images/pin.png"
            ActionBar.placement: ActionBarPlacement.OnBar
            onTriggered: {
                _mapViewTest.addPinAtCurrentMapCenter();
            }
        },
        ActionItem {
            title: qsTr("Remove Pins")
            imageSource: "asset:///images/clearpin.png"
            ActionBar.placement: ActionBarPlacement.OnBar
            onTriggered: {
                _mapViewTest.clearPins();
            }
        },
        ActionItem {
            title: qsTr("Center URL")
            imageSource: "asset:///images/url.png"
            ActionBar.placement: ActionBarPlacement.InOverflow
            onTriggered: {
                status.setText(mapview.url());
            }
        },
        ActionItem {
            title: qsTr("Waterloo")
            imageSource: "asset:///images/pin.png"
            ActionBar.placement: ActionBarPlacement.InOverflow
            onTriggered: {
                mapview.latitude = 43.468245;
                mapview.longitude = -80.519603;
            }
        },
        ActionItem {
            title: qsTr("Manhattan")
            imageSource: "asset:///images/pin.png"
            ActionBar.placement: ActionBarPlacement.InOverflow
            onTriggered: {
                mapview.latitude = 40.791556;
                mapview.longitude = -73.967394;
            }
        }
    ]

    // The main content of the page
    Container {
        id: root

        ... 

    } // End of main UI Container
} // End of Page

You can also extend the functionality of your app to let a user choose the number of displayed locations, but that's outside the scope of this tutorial. For more information, see Adding an application menu.

import bb.cascades 1.0
import bb.cascades.maps 1.0
import QtMobility.sensors 1.2
import QtMobilitySubset.location 1.1

Page {
    actions: [
        ActionItem {
            title: qsTr("Drop Pin")
            imageSource: "asset:///images/pin.png"
            ActionBar.placement: ActionBarPlacement.OnBar
            onTriggered: {
                _mapViewTest.addPinAtCurrentMapCenter();
            }
        },
        ActionItem {
            title: qsTr("Remove Pins")
            imageSource: "asset:///images/clearpin.png"
            ActionBar.placement: ActionBarPlacement.OnBar
            onTriggered: {
                _mapViewTest.clearPins();
            }
        },
        ActionItem {
            title: qsTr("Center URL")
            imageSource: "asset:///images/url.png"
            ActionBar.placement: ActionBarPlacement.InOverflow
            onTriggered: {
                status.setText(mapview.url());
            }
        },
        ActionItem {
            title: qsTr("Waterloo")
            imageSource: "asset:///images/pin.png"
            ActionBar.placement: ActionBarPlacement.InOverflow
            onTriggered: {
                mapview.latitude = 43.468245;
                mapview.longitude = -80.519603;
            }
        },
        ActionItem {
            title: qsTr("Manhattan")
            imageSource: "asset:///images/pin.png"
            ActionBar.placement: ActionBarPlacement.InOverflow
            onTriggered: {
                mapview.latitude = 40.791556;
                mapview.longitude = -73.967394;
            }
        }
    ]
    // The main contents of the UI
    Container {
        id: root
        
        layout: DockLayout {
        }
        MapView {
            id: mapview
            objectName: "mapViewObj"
            altitude: 3000
            latitude: 43.449488
            longitude: -80.406777
            
            onAltitudeChanged: {
                alt.setText(qsTr("Alt: %1").arg(newAlt));
            }
            onHeadingChanged: {
                heading.setText(qsTr("Heading: %1\u00B0").arg(newHeading));
            }
            onLatitudeChanged: {
                lat.setText(qsTr("Lat: %1").arg(newLat));
            }
            onLongitudeChanged: {
                lon.setText(qsTr("Lon: %1").arg(newLon));
            }
            onTiltChanged: {
                tilt.setText(qsTr("Tilt: %1\u00B0").arg(newTilt));
            }
            onMapLongPressed: {
                status.setText(qsTr("map long pressed"));
            }
            
            onFollowedIdChanged: {
                status.setText(qsTr("followed id changed to %1")
                    .arg(idOfFollowed));
            }
            onFocusedIdChanged: {
                status.setText(qsTr("focused id changed to %1")
                    .arg(idWithFocus));
            }
            onCaptionButtonClicked: {
                status.setText(qsTr("button clicked %1").arg(focusedId));
            }
            onCaptionLabelTapped: {
                status.setText(qsTr("label clicked %1").arg(focusedId));
            }
            onLocationTapped: {
                status.setText(qsTr("location tapped %1").arg(id));
            }
            onLocationLongPressed: {
                status.setText(qsTr("location long pressed %1").arg(id));
            }
        } // End of MapView
        
        Container {
            horizontalAlignment: HorizontalAlignment.Fill
            verticalAlignment: VerticalAlignment.Top
            topPadding: 5
            leftPadding: 5
            bottomPadding: 5
            background: Color.create("#ddffffff")
            
            
            
            // This container contains labels that show the 
            // latitude, longitude, altitude, heading and 
            // tilt of the map.
            Container {
                layout: StackLayout {
                    orientation: LayoutOrientation.LeftToRight
                }
                horizontalAlignment: HorizontalAlignment.Center
                
                Label {
                    
                    id: lat
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
                Label {
                    id: lon
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
                Label {
                    id: alt
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
                Label {
                    id: heading
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
                Label {
                    id: tilt
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Black
                        fontWeight: FontWeight.Bold
                    }
                }
            } // End of the Container of map characteristics labels
            
            // This container holds the status label 
            Container {
                horizontalAlignment: HorizontalAlignment.Center
                Label {
                    id: status
                    textStyle {
                        base: SystemDefaults.TextStyles.SmallText
                        color: Color.Gray
                        fontWeight: FontWeight.Bold
                    }
                }
            } // End of the status label container
        } // End of the label container
        
        Container {
            leftPadding: 20
            rightPadding: 20
            bottomPadding: 20
            topPadding: 20
            horizontalAlignment: HorizontalAlignment.Right
            verticalAlignment: VerticalAlignment.Bottom
            overlapTouchPolicy: OverlapTouchPolicy.Allow
            
            
            ImageView {
                id: compassImage
                imageSource: "asset:///images/compass.png"
                horizontalAlignment: HorizontalAlignment.Center
                attachedObjects: [
                    ImplicitAnimationController {
                        // Disable animations to avoid 
                        // jumps between 0 and 360 degree
                        enabled: false
                    }
                ]
            }
            ToggleButton {
                id: sensorToggle
                horizontalAlignment: HorizontalAlignment.Center
                checked: true
                onCheckedChanged: {
                    if (checked) {
                        mapview.setFollowedId("device-location-id");
                    } else {
                        mapview.setFollowedId("");
                    }
                }
                onCreationCompleted: {
                    mapview.setFollowedId("device-location-id");
                }
            }    
        
        } // End of the Container for the pins        
        
    } // End of main UI Container
    attachedObjects: [
        RotationSensor {
            id: rotation
            property real x: 0
            active: sensorToggle.checked
            alwaysOn: false
            skipDuplicates: true
            onReadingChanged: {
                x = reading.x - 30
                if (x <= 40 && x > 0) {
                    mapview.setTilt(x);
                }
            }
        },
        Compass {
            property double azimuth: 0
            active: sensorToggle.checked
            axesOrientationMode: Compass.UserOrientation
            alwaysOn: false
            // Called when a new compass reading is available
            onReadingChanged: { 
                mapview.setHeading(reading.azimuth);
                compassImage.rotationZ = 360 - reading.azimuth;
            }
        },
        PositionSource {
            id: positionSource
            updateInterval: 1000
            active: sensorToggle.checked
            onPositionChanged: {
                _mapViewTest.updateDeviceLocation(
                    positionSource.position.coordinate.latitude, 
                    positionSource.position.coordinate.longitude);
            }
        }
    ]
} // End of Page

Add custom components for pins and bubbles

To add pins and bubbles to our map, we create two custom UI components: a Pin and a Bubble. For more information about custom UI components, see Custom QML components.

To create the Pin component, create a file named pin.qml in the assets folder of your project. The Pin component uses an AbsoluteLayout to position a bubble and an image of a pin on the map.

We use the onPositionXChanged() and onPositionYChanged() signal handlers to respond to changes in the position of our map, and to control the visibility of the Bubble. The bubble on the pin is not visible when it is dropped so we set the visible property of the Bubble component to false.

The clipContentToBounds property of the Container is used to keep the Bubble and the Pin together in the Container.

When the user taps the pin on the map, the focusChanged() signal of the ImageView is emitted, and we set the visible property of the Bubble to true and animate it using a ScaleTransition and a BounceOut easing curve. For more information about creating animations, see Explicit animations.

import bb.cascades 1.0

Container {
    id: root
    property alias x: position.positionX
    property alias y: position.positionY
    property double lat
    property double lon
    property alias anim: anim

    clipContentToBounds: false


    layout: AbsoluteLayout {
    }

    layoutProperties: AbsoluteLayoutProperties {
        id: position
        onPositionXChanged: {
            bubble.visible = false
        }
        onPositionYChanged: {
            bubble.visible = false
        }
    }

    Bubble {
        id: bubble

        visible: false

        layoutProperties: AbsoluteLayoutProperties {
            positionX: 30
            positionY: -140
        }
    }

    ImageView {
        animations: [
            ScaleTransition {
                id: anim
                fromX: 0
                toX: 1
                fromY: 0
                toY: 1
                duration: 500
                easingCurve: StockCurve.BounceOut
            }
        ]
        imageSource: "asset:///images/on_map_pin.png"
        focusPolicy: FocusPolicy.Touch
        onFocusedChanged: {
            if (focused) {
                anim.play();
                bubble.visible = true
            } else {
                bubble.visible = false
            }
        }
    }

    overlapTouchPolicy: OverlapTouchPolicy.Allow
}

The final part of our UI is the Bubble custom component, which is a Container with an image of a bubble. Go ahead and create a file called bubble.qml in the assets folder of your project:

import bb.cascades 1.0

Container {
    preferredHeight: 200
    preferredWidth: 300
    ImageView {
        imageSource: "asset:///images/bubble.png"
    }
}

Last modified: 2013-12-21



Got questions about leaving a comment? Get answers from our Disqus FAQ.

comments powered by Disqus