Animate the notes

We have our custom Note component, so let's use it to create the UI of our app and provide animations for the notes.

Import the poemgenerator.js file

Open the main.qml file that's located in the assets folder of the project. The prepopulated code that's in the file already includes the appropriate import statement that lets us use Cascades controls. Our poem maker app also uses a JavaScript function called generatePoemLine() to create the poem text for each note. To use this function, we need to import the poemgenerator.js file that contains the function. We use the import statement and identify the file using the unique string PoemGenerator:

import bb.cascades 1.4

// Import a JavaScript file used to generate the text for the poem
import "poemgenerator.js" as PoemGenerator

For more information about importing JavaScript functions, see JavaScript in QML.

Create the UI

Our UI has the following structure:


Diagram showing the structure of the Poem Maker app.

The background image already includes most of the graphics that we need. We need to add images to represent the rubber button that creates a poem, along with three of our Note components to display the poem itself.

The prepopulated code in main.qml includes a  Page element to hold the content of our app. The root element of a QML document in Cascades must be a subclass of  AbstractPane, so the Page element is used here. You can delete the  Container that was included with the prepopulated code.

We start by creating the top-level Container that holds all of the controls in our app. We use an absolute layout for this container so that we can precisely position the controls that we add. We use an ImagePaintDefinition so that we can repeat the texture of the background image in our Container. This technique is useful if you want to reuse a background image on devices with different screen sizes. You can read more about this techique in Use assets and colors efficiently. We'll define this ImagePaintDefinition later in the tutorial.

Page {
    Container {
        background: backgroundPaint.imagePaint
        
        layout: AbsoluteLayout {
        }

Then we add the image of the straw to our background using an ImageView:

        ImageView {
            imageSource: "asset:///images/straw.png"
        }

Add the button images

Next, let's add the rubber button using an ImageButton. This type of button acts like a Button except it has three different images, each of which represents a visual state. One image represents the unpressed button, one image represents the pressed button, and one image represents the disabled button. We use the same image for the unpressd button and the disabled button because we don't have a case where we disable the button in our app.

        ImageButton {
            defaultImageSource: "asset:///images/rubber.png"
            pressedImageSource: "asset:///images/rubber_depressed.png"
            disabledImageSource: "asset:///images/rubber.png"
            
            layoutProperties: AbsoluteLayoutProperties {
                positionX: 900
                positionY: 510
            }

For the button image, we want to handle touch events and respond appropriately by hiding the current poem and creating a new poem. We can use the onTouch signal handler that's provided automatically to handle touch events.

In this signal handler, we determine whether the touch event is a press by checking for the isDown() touch event. We hide each note by calling the JavaScript function hideNote(). For visual variety, we also change the rotation of the notes slightly as they disappear.

// This is the touch signal handler for the button.
// Usually, for an image button, you would use the onClicked
// handler.
// For this sample, it makes more sense for the notes to appear
// when the button is pressed so we use the isDown() touch event. 
// At that point, the "wind" blows through the straw and the 
// animations are triggered.
onTouch: {
    if (event.isDown()) {

        // The hide animations are triggered in the 
        // hideNote function in Note.qml
        note1.hideNote();
        note2.hideNote();
        note3.hideNote();

        // Change the rotation on the notes as they
        // are blown away
        note1.rotationZ = 0;
        note2.rotationZ = -40;
        note3.rotationZ = 40;
        }
    }
} // end of ImageButton

After the button is released, the ImageButton resets the image automatically to its defaultImageSource so that the button appears unpressed.

Create the notes

Now, we create the first of the three notes by using our custom Note component. We expose the rotation of the note by using a custom property called initialRotation that we associate with the rotationZ property of the note. In this way, we can rotate the note as it disappears off the screen and then easily reset the rotation back to its initial value when we create the new note. We also use the positionX and positionY layout properties to precisely position the note on the screen:

// The first note: <adjective> + <noun>
Note {
    id: note1
    property int initialRotation: 0
    layoutProperties: AbsoluteLayoutProperties {
        positionX: 94
        positionY: 184
    }

We also specify a value for the showAnimStartX property of the note. When we created the Note component, we created this property alias and bound it to the fromX property of the note's show animation. This animation determines how new notes appear from the left side of the screen. Because we use the same custom Note component for each note in our app, all three notes share the same animations. All of the animations start from the same x and y coordinates, and they all have the same duration and amount of translation.

However, in our finished app, each note has a different position on the screen. To use the same animations for each note, we need to be able to specify different starting positions. By exposing the fromX property using the alias showAnimStartX, we can make sure that each note's animation starts at the appropriate offscreen position and animates to the proper position on the screen.

showAnimStartX: (- note1.layoutProperties.positionX - 238)

Recall that the Note component includes a signal, newNote(), that's emitted when the note's hide animation finishes. We can handle this signal by using the onNewNote signal handler that Cascades provides. When the signal is emitted, we call the custom JavaScript function showNote() to show a new note and reset the note's rotation to its initial value. We also use the custom JavaScript function, generatePoemLine(), that we imported at the beginning of this file to generate new poem text for the note.

// The Note component emits a signal called newNote 
// We connect to this signal for the first note only
// and trigger all the show animations here 
// (it's emitted when the hide animation has ended)
onNewNote: {

    // The show animation is started by calling 
    // the showNote function in Note.qml (it is not
    // possible to trigger animations using IDs 
    // if they reside in a separate QML document)
    showNote();

    // The note is animated back to its original 
    // rotation using implicit animations
    rotationZ = initialRotation;

    // Update the poem while the note is not visible on screen
    note1.poem = PoemGenerator.generatePoemLine(1);
    }
} // end of first note

Next, we create the second and third notes, using an approach that's nearly identical to the way we created the first note:

        // The second note: <verb> + <adverb>
        Note {
            id: note2
            property int initialRotation: -12
            layoutProperties: AbsoluteLayoutProperties {
                positionX: 472
                positionY: 183
            }
            rotationZ: initialRotation
            showAnimStartX: (- note2.layoutProperties.positionX - 238)
            onNewNote: {
                showNote();
                rotationZ = initialRotation;
                note2.poem = PoemGenerator.generatePoemLine(2);
            }
        }

        // The third note: <preposition> + <noun>
        Note {
            id: note3
            property int initialRotation: -5
            layoutProperties: AbsoluteLayoutProperties {
                positionX: 826
                positionY: 147
            }
            rotationZ: initialRotation
            showAnimStartX: (- note3.layoutProperties.positionX - 238)
            onNewNote: {
                showNote ();
                rotationZ = initialRotation;
                note3.poem = PoemGenerator.generatePoemLine (3);
            }
        }
    } // end of Container

Generate an initial poem

We're nearly done, but we have two small additions to make. All UI objects in Cascades emit a signal when they're created. It can be useful to respond to this signal by performing any last-minute setup or initialization operations. Our app still needs an initial poem to appear when it starts, so we use the onCreationCompleted signal handler to generate the first poem:

    // When the Page is completely created we call 
    // the updatePoem function to generate
	// a new poem with each note called.
    onCreationCompleted: {
        note1.poem = PoemGenerator.generatePoemLine (1);
        note2.poem = PoemGenerator.generatePoemLine (2);
        note3.poem = PoemGenerator.generatePoemLine (3);
    }

We also attach two definitions that define the text style that we used for the note in Add visual elements and the background image definition that we used in Create the UI:

    attachedObjects: [
        // Non UI objects are specified as attached objects
        TextStyleDefinition {
            id: noteStyle
            base: SystemDefaults.TextStyles.BodyText
            textAlign: TextAlign.Center
        },
        ImagePaintDefinition {
            id: backgroundPaint
            imageSource: "asset:///images/background.png"
        }
    ]
}
import bb.cascades 1.4

// Import a JavaScript file used to generate the text for the poem
import "poemgenerator.js" as PoemGenerator

Page {
    Container {
        background: backgroundPaint.imagePaint
        
        layout: AbsoluteLayout {
        }

        ImageView {
            imageSource: "asset:///images/straw.png"
        }

        ImageButton {
            defaultImageSource: "asset:///images/rubber.png"
            pressedImageSource: "asset:///images/rubber_depressed.png"
            disabledImageSource: "asset:///images/rubber.png"
            
            layoutProperties: AbsoluteLayoutProperties {
                positionX: 900
                positionY: 510
            }
            
            // This is the touch signal handler for the button.
            // Usually, for an image button, you would use the onClicked
            // handler.
            // For this sample, it makes more sense for the notes to appear
            // when the button is pressed so we use the isDown() touch event. 
            // At that point, the "wind" blows through the straw and the 
            // animations are triggered.
            onTouch: {
                if (event.isDown()) {
                    
                    // The hide animations are triggered in the 
                    // hideNote function in Note.qml
                    note1.hideNote();
                    note2.hideNote();
                    note3.hideNote();
                    
                    // Change the rotation on the notes as they
                    // are blown away
                    note1.rotationZ = 0;
                    note2.rotationZ = -40;
                    note3.rotationZ = 40;
                }
            }
        } // end of ImageButton

        // The first note: <adjective> + <noun>
        Note {
            id: note1
            property int initialRotation: 0
            layoutProperties: AbsoluteLayoutProperties {
                positionX: 94
                positionY: 184
            }
            
            showAnimStartX: (- note1.layoutProperties.positionX - 238)

            // The Note component emits a signal called newNote 
            // We connect to this signal for the first note only
            // and trigger all the show animations here 
            // (it's emitted when the hide animation has ended)
            onNewNote: {
                
                // The show animation is started by calling 
                // the showNote function in Note.qml (it is not
                // possible to trigger animations using IDs 
                // if they reside in a separate QML document)
                showNote();
                
                // The note is animated back to its original 
                // rotation using implicit animations
                rotationZ = initialRotation;
                
                // Update the poem while the note is not visible on screen
                note1.poem = PoemGenerator.generatePoemLine(1);
            }
        } // end of first note

        // The second note: <verb> + <adverb>
        Note {
            id: note2
            property int initialRotation: -12
            layoutProperties: AbsoluteLayoutProperties {
                positionX: 472
                positionY: 183
            }
            rotationZ: initialRotation
            showAnimStartX: (- note2.layoutProperties.positionX - 238)
            onNewNote: {
                showNote();
                rotationZ = initialRotation;
                note2.poem = PoemGenerator.generatePoemLine(2);
            }
        } // end of second note
        
        // The third note: <preposition> + <noun>
        Note {
            id: note3
            property int initialRotation: -5
            layoutProperties: AbsoluteLayoutProperties {
                positionX: 826
                positionY: 147
            }
            rotationZ: initialRotation
            showAnimStartX: (- note3.layoutProperties.positionX - 238)
            onNewNote: {
                showNote ();
                rotationZ = initialRotation;
                note3.poem = PoemGenerator.generatePoemLine (3);
            }
        } // end of third note
    } // end of Container

    // When the Page is completely created we call 
    // the updatePoem function to generate
    // a new poem with each note called
    onCreationCompleted: {
        note1.poem = PoemGenerator.generatePoemLine (1);
        note2.poem = PoemGenerator.generatePoemLine (2);
        note3.poem = PoemGenerator.generatePoemLine (3);
    }
    
    attachedObjects: [
        // Non UI objects are specified as attached objects
        TextStyleDefinition {
            id: noteStyle
            base: SystemDefaults.TextStyles.BodyText
            textAlign: TextAlign.Center
        },
        ImagePaintDefinition {
            id: backgroundPaint
            imageSource: "asset:///images/background.png"
        }
    ]
}

Set the orientation for the app

You might have noticed that our app is designed to run in landscape orientation, instead of portrait orientation. Because portrait is the default orientation for apps that you create, we need to change one of our project files to tell the app to run in landscape orientation instead.

In our main project folder, open the bar-descriptor.xml file in a text editor (right-click the bar-descriptor.xml file and click Open With > Text Editor). This file contains configuration information for the app, in XML format. We want to set the orientation to landscape, and we also want to prevent the app from trying to change its orientation automatically when the orientation of the device changes.

Find the <initialWindow> tag, which determines what the app window looks like when the app runs. Inside this tag, add the following lines (replace the <autoOrients>true</autoOrients> line) and save the file:

<aspectRatio>landscape</aspectRatio>
<autoOrients>false</autoOrients>

To make sure that your app looks good on all devices, we need to set the visual style of our project so that the text of the poem shows up in the note. If we don't set the visual style to bright, the poem doesn't appear in the note on devices that use a dark style, because the note background is light and so is the text of the poem. Go ahead and add the environment variable called CASCADES_THEME and set it to bright in your bar-descriptor file:

<env var="CASCADES_THEME" value="dark"/>

For more information, see Setting the visual style.

We're finished! Go ahead and run the app to see the result:


Screen showing the Poem Maker sample app.

If you run the app on a BlackBerry Z10 smartphone, the app looks great, right? But we made a lot of assumptions in our design of this app. We assumed that we had a device with a rectangular screen that could be rotated to landscape orientation. We also assumed that the positions of the bulb and the straw on the background of our UI were in fixed locations in an absolute layout. Some of these assumptions might not be true on devices such as the BlackBerry Passport smartphone or the BlackBerry Classic smartphone.

Challenge yourself to change this app so that it looks great on all devices. To find out more about devices, see Device characteristics. To learn how to adapt your UI to all screen resolutions, densities, and device sizes, see Resolution independence. You can check your solution against the full source code for the poemmaker sample app.

Last modified: 2015-03-31



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

comments powered by Disqus