Create the UI

Now that we've set up our project, let's create the UI for our app.

Create the traffic light

If you think of a traffic light, it has a few features and behaviors that we want to model in our app. In particular, a traffic light has a frame (the static portion that contains the lights) and three lights inside that frame (red, yellow, and green). A traffic light also needs to be able to change from one state to another (for example, turning from red to green).

We'll use a custom component in Cascades to represent our traffic light. This custom component contains the frame image, which looks like a traffic light with all of the lights dimmed. The component also includes images for each of the lights, and these light images are placed on top of the frame in their appropriate positions. When we want to set the state of the traffic light (red, yellow, or green), we simply set the opacity of the corresponding light image to 1.0 and the opacities of the other two light images to 0.0. This gives the impression that one light is turned on and the other lights are turned off.


Screen showing the opacities of each image in the traffic light app.

Our custom component also includes a JavaScript function that changes the state of the traffic light. We can call this function whenever we want the light to change. If you want to learn more about how custom components work in Cascades, see Custom QML components.

Start by creating a new QML file called TrafficLight.qml in the assets folder of your project (right-click the assets folder and click New > QML File). It doesn't matter what template you use, because we'll remove the pre-populated code and start from scratch. This file will define a custom component called TrafficLight.

When you've created the new QML file, you can delete the code that's in it by default. We create a top-level Container to hold all of the pieces of the traffic light (the frame and the three light images). We use an AbsoluteLayout for this container to allow us to precisely position the light images on top of the frame image:

import bb.cascades 1.0
 
Container {
    id: root
    layout: AbsoluteLayout {}

Now, let's add the frame and light images. Some of the work's been done for you; the proper x and y coordinates of the light images have already been calculated. The traffic light starts in the red state, with the opacity property of the red light set to 1.0 and the opacity property of the yellow and green lights set to 0.0:

// The frame of the traffic light
ImageView {
    id: trafficLightFrame
    imageSource: "asset:///images/traffic_light_frame.png"
}
     
// The red light
ImageView {
    id: redLight
    layoutProperties: AbsoluteLayoutProperties {
        positionX: 40
        positionY: 41
    }
    imageSource: "asset:///images/red_light.png"
    opacity: 1.0
}
     
// The yellow light
ImageView {
    id: yellowLight
    layoutProperties: AbsoluteLayoutProperties {
        positionX: 39
        positionY: 196
    }
    imageSource: "asset:///images/yellow_light.png"
    opacity: 0.0
}
     
// The green light
ImageView {
    id: greenLight
    layoutProperties: AbsoluteLayoutProperties {
        positionX: 38
        positionY: 351
    }
    imageSource: "asset:///images/green_light.png"
    opacity: 0.0
}

We want our traffic light to be able to change its state (for example, from red to green), so we create a custom JavaScript function inside the root container to accomplish this:

    function changeLight() {
        if (redLight.opacity == 1.0) {
            // If the light is red, change it to green
            redLight.opacity = 0.0;
            greenLight.opacity = 1.0;
        } else if (greenLight.opacity == 1.0) {
            // If the light is green, change it to yellow
            greenLight.opacity = 0.0;
            yellowLight.opacity = 1.0;
        } else {
            // If the light is yellow, change it to red
            yellowLight.opacity = 0.0;
            redLight.opacity = 1.0;
        }
    } // end of changeLight() function
} // end of top-level Container

The changeLight() function changes the opacities of the light images to model the behavior of a traffic light in real life, depending on the current state of the light. That is, it can change the state from red to green, from green to yellow, and from yellow to red. However, it can't change the state from red to yellow because real traffic lights don't (usually) work that way.

Create the main screen

Our traffic light is finished, so now we'll create the UI for the main screen of our app. The UI has the following structure:

Diagram showing the structure of the UI.

Open the main.qml file that's located in the assets folder of the project. The pre-populated code that's in the file already includes a Page element to hold the content of our app. Remember that the root element of a QML document must be a subclass of AbstractPane, so the Page element is used here. Go ahead and delete the Container control that was included with the pre-populated code, along with everything inside it.

For the content of the Page, we create the top-level Container, with an id of root, that we'll add all of our components and UI controls to. This container uses a DockLayout to make it easy to use a background image for the app:

import bb.cascades 1.0
 
Page {
    Container {
        id: root
        layout: DockLayout {}

Let's add the background image by using an ImageView control, making sure that it fills the entire screen by using alignment properties:

ImageView {
    horizontalAlignment: HorizontalAlignment.Fill
    verticalAlignment: VerticalAlignment.Fill
     
    imageSource: "asset:///images/background.png"
}

Our UI consists of two sections. The upper section contains contains the traffic light, and the lower section contains the countdown timer and the button that's used to change the traffic light. We create a container to hold both of these sections, and give it a StackLayout with a top-to-bottom layout direction. Remember that top-to-bottom is the default direction if we don't explicitly specify a layout direction for the container. We also want the container to fill the entire screen, so we use alignment properties to specify this:

// A container to hold both the upper and lower sections of the UI
Container {
    layout: StackLayout {}
     
    horizontalAlignment: HorizontalAlignment.Fill
    verticalAlignment: VerticalAlignment.Fill

Next, we create the upper container. We give it a dock layout, and specify that it should fill the width of its parent container. We also specify a space quota for it so that it will expand to fill the available vertical space (we'll also specify a space quota for the lower container later on). We add our custom TrafficLight component to this container, and give it an id so that we can refer to it later when we need to change its state:

// A container for the upper section
Container {
    layout: DockLayout {}
    horizontalAlignment: HorizontalAlignment.Fill
    layoutProperties: StackLayoutProperties {
        spaceQuota: 1
    }
     
    // Our custom TrafficLight component
    TrafficLight {
        id: trafficLight
 
        horizontalAlignment: HorizontalAlignment.Center
        verticalAlignment: VerticalAlignment.Bottom
    }
}

The upper section is complete, so let's create the lower container now. This container also uses a dock layout and fills the width of its parent container. It also has a space quota value so that it fills the vertical screen space that was left over after we created the upper container. This container holds a Button that's centered in the container, and also holds a  TextArea that represents our countdown timer. We make sure to set the id property for the Button and TextArea so that we can refer to them later.

            // A container for the lower section
            Container {
                layout: DockLayout {}
                horizontalAlignment: HorizontalAlignment.Fill
                layoutProperties: StackLayoutProperties {
                    spaceQuota: 1
                }
                 
                // The button that changes the traffic light
                Button {
                    id: changeButton
                    horizontalAlignment: HorizontalAlignment.Center
                    verticalAlignment: VerticalAlignment.Center
                    text: "Change!"
                }
                 
                // The text area that displays the countdown
                TextArea {
                    id: timerDisplay
                    horizontalAlignment: HorizontalAlignment.Center
                    preferredWidth: 67
                     
                    // Apply a text style to create centered body text
                    textStyle {
                        base: SystemDefaults.TextStyles.BodyText
                        textAlign: TextAlign.Center
                    }
                    text: "0"
                }
            } // end of lower section Container
        } // end of upper and lower section Container
    } // end of top-level Container
} // end of Page

Now that our UI is finished, build and run the app to see the result:


Screen showing the finished app.

import bb.cascades 1.0
  
Page {
    Container {
        id: root
        layout: DockLayout {}

        ImageView {
            horizontalAlignment: HorizontalAlignment.Fill
            verticalAlignment: VerticalAlignment.Fill

            imageSource: "asset:///images/background.png"
        }
        
        // A container to hold both the upper and lower sections of the UI
		Container {
		    layout: StackLayout {}
		      
		    horizontalAlignment: HorizontalAlignment.Fill
		    verticalAlignment: VerticalAlignment.Fill
		    
		    // A container for the upper section
			Container {
			    layout: DockLayout {}
			    horizontalAlignment: HorizontalAlignment.Fill
			    layoutProperties: StackLayoutProperties {
			        spaceQuota: 1
			    }
			      
			    // Our custom TrafficLight component
			    TrafficLight {
			        id: trafficLight
			  
			        horizontalAlignment: HorizontalAlignment.Center
			        verticalAlignment: VerticalAlignment.Bottom
			    }
			}
			
            // A container for the lower section
            Container {
                layout: DockLayout {
                }
                horizontalAlignment: HorizontalAlignment.Fill
                layoutProperties: StackLayoutProperties {
                    spaceQuota: 1
                }

                // The button that changes the traffic light
                Button {
                    id: changeButton
                    horizontalAlignment: HorizontalAlignment.Center
                    verticalAlignment: VerticalAlignment.Center
                    text: "Change!"
                }

                // The text area that displays the countdown
                TextArea {
                    id: timerDisplay
                    horizontalAlignment: HorizontalAlignment.Center
                    preferredWidth: 67

                    // Apply a text style to create centered body text
                    textStyle {
                        base: SystemDefaults.TextStyles.BodyText
                        textAlign: TextAlign.Center
                    }
                    text: "0"
                }
            } // end of lower section Container
        } // end of upper and lower section Container
    } // end of top-level Container
}// end of Page

For devices with physical keyboards

If you're running your app on a device with a physical keyboard, you need to modify your UI slightly to fit the smaller screen on this type of device. There are several approaches that we could use to adapt to a smaller screen. We could design image assets that are smaller and then create a new traffic light component using these smaller images. Or, we could add custom properties to the traffic light component and use these properties to resize the traffic light and all of its visual elements. To learn about different ways to adapt your own UIs to fit different screen sizes, see Resolution independence.

For this tutorial, we're simply going to change the layout of our app to fit on a smaller screen. In the Create the main screen section above, we used a StackLayout with a top-to-bottom layout direction. Because a device with a physical keyboard doesn't have as much vertical screen space, we're going to use a left-to-right layout direction instead. Our traffic light component will be placed inside a container on the left, and the countdown timer and button will be placed inside a container on the right.


Screen showing the arrangement of UI components on keyboard devices.

To implement this new layout, we use a Cascades feature called the static asset selector. This feature lets you organize your app's assets (such as images and QML files) into subfolders in your project's assets folder, and when it's time to run your app on devices with different resolutions or themes, Cascades automatically selects the assets that are best for a particular device. To learn more about the static asset selector, see Static asset selection.

Resolution-specific assets need to be placed in a folder with the same name as the screen resolution of the target device. For a device with a physical keyboard, the screen resolution is 720x720. So, in the assets folder of your project, create a new folder called 720x720 (right-click the assets folder and click New > Folder).

In our app, only the main.qml file needs to be changed to accommodate the smaller screen size; we can still use the same images. Copy the main.qml file from the root of the assets folder, and then paste the file into the assets/720x720 folder that we just created. Your assets folder should look similar to the image on the right. Now, when you run your app on a device with a physical keyboard, the main.qml file in the assets/720x720 folder is used to create the UI.

Screen showing the folder structure for the keyboard device layout.

Next, open the main.qml file that's in the 720x720 folder. We need to change a few lines in this file to create the new layout that we want to use. Find the Container that we created to hold the upper and lower sections of the UI. It should start like this:

// A container to hold both the upper and lower sections of the UI
Container {
    layout: StackLayout {}

Go ahead and delete this container from the file. We'll create a new container that's similar to the old one, but that includes the proper alignment properties for our new layout. We start by creating a container that uses a left-to-right StackLayout instead of a top-to-bottom one. We still want this new container to fill the entire screen, though, so we use the same alignment properties as we did before:

// A container to hold both the left and right sections of the UI
Container {
    layout: StackLayout {
        orientation: LayoutOrientation.LeftToRight
    }
      
    horizontalAlignment: HorizontalAlignment.Fill
    verticalAlignment: VerticalAlignment.Fill

We add a container to represent the left section of the UI, which contains our TrafficLight custom component. We adjust the alignment properties so that the container fills the entire height of its parent container and the TrafficLight component is centered in the container:

// A container for the left section
Container {
    layout: DockLayout {}
    verticalAlignment: VerticalAlignment.Fill
    layoutProperties: StackLayoutProperties {
        spaceQuota: 1
    }
			      
    // Our custom TrafficLight component
    TrafficLight {
        id: trafficLight
			  
        horizontalAlignment: HorizontalAlignment.Center
        verticalAlignment: VerticalAlignment.Center
    }
}

Similarly, we create another container for the right section of the UI. Our Button is still centered in this container, but we adjust the TextArea so that it aligns to the left side of the container:

        // A container for the right section
        Container {
            layout: DockLayout {
        }
        verticalAlignment: VerticalAlignment.Fill
        layoutProperties: StackLayoutProperties {
            spaceQuota: 1
        }

        // The button that changes the traffic light
        Button {
            id: changeButton
            horizontalAlignment: HorizontalAlignment.Center
            verticalAlignment: VerticalAlignment.Center
            text: "Change!"
        }

        // The text area that displays the countdown
        TextArea {
            id: timerDisplay
            verticalAlignment: VerticalAlignment.Center
            preferredWidth: 67

            // Apply a text style to create centered body text
            textStyle {
                base: SystemDefaults.TextStyles.BodyText
                textAlign: TextAlign.Center
            }
            text: "0"
        }
    } // end of lower section Container
} // end of upper and lower section Container

Our modified UI is now complete, and our app will appear correctly when it's run on a device with a physical keyboard.


Screen showing the finished layout for keyboard devices.

import bb.cascades 1.0
  
Page {
    Container {
        id: root
        layout: DockLayout {}

        ImageView {
            horizontalAlignment: HorizontalAlignment.Fill
            verticalAlignment: VerticalAlignment.Fill

            imageSource: "asset:///images/background.png"
        }

        // A container to hold both the left and right sections of the UI
        Container {
            layout: StackLayout {
                orientation: LayoutOrientation.LeftToRight
            }

            horizontalAlignment: HorizontalAlignment.Fill
            verticalAlignment: VerticalAlignment.Fill

            // A container for the left section
            Container {
                layout: DockLayout {
                }
                verticalAlignment: VerticalAlignment.Fill
                layoutProperties: StackLayoutProperties {
                    spaceQuota: 1
                }

                // Our custom TrafficLight component
                TrafficLight {
                    id: trafficLight

                    horizontalAlignment: HorizontalAlignment.Center
                    verticalAlignment: VerticalAlignment.Center
                }
            }

            // A container for the right section
            Container {
                layout: DockLayout {
                }
                verticalAlignment: VerticalAlignment.Fill
                layoutProperties: StackLayoutProperties {
                    spaceQuota: 1
                }

                // The button that changes the traffic light
                Button {
                    id: changeButton
                    horizontalAlignment: HorizontalAlignment.Center
                    verticalAlignment: VerticalAlignment.Center
                    text: "Change!"
                }

                // The text area that displays the countdown
                TextArea {
                    id: timerDisplay
                    verticalAlignment: VerticalAlignment.Center
                    preferredWidth: 67

                    // Apply a text style to create centered body text
                    textStyle {
                        base: SystemDefaults.TextStyles.BodyText
                        textAlign: TextAlign.Center
                    }
                    text: "0"
                }
            } // end of lower section Container
        } // end of upper and lower section Container
    } // end of top-level Container
}// end of Page

Last modified: 2013-12-21

comments powered by Disqus