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 pre-populated code that's in the file already includes the appropriate import statement that lets us use Cascades controls. The 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 to accomplish this, and identify the file using the unique string PoemGenerator:

import bb.cascades 1.0
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 just need to add images to represent the rubber button that creates a new poem, along with three of our Note components to display the poem itself.

The pre-populated 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 pre-populated 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. Our background image is located in the assets/images folder, ready for us to use, so we add it here as well:

Page {
    content: Container {
        layout: AbsoluteLayout {
        } 
         
        ImageView {
            imageSource: "asset:///images/background.png"
            preferredWidth: 1280
            preferredHeight: 768
        }

Add the button images

Next, let's add the rubber button. The button actually consists of two images: one that represents the unpressed button, and one overlayed on top that, when visible, simulates the appearance of a pressed button. For the underlying button image, we want to handle touch events and respond appropriately by 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 or a release. If it's a press, 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. Finally, we change the opacity of the overlay image to 1.0 to make it appear as though the button is pressed. If the touch event is a release, we change the opacity of the overlay image to 0.0 to restore the appearance of the unpressed button.

ImageView {
    imageSource: "asset:///images/rubber.png"
     
    layoutProperties: AbsoluteLayoutProperties {
        positionX: 900
        positionY: 510
    }
     
    onTouch: {
        if (event.isDown ()) {
            // Hide the notes that are currently displayed
            note1.hideNote ();
            note2.hideNote ();
            note3.hideNote ();
 
            // Change the rotation of the notes slightly
            note1.rotationZ = 0;
            note2.rotationZ = -40;
            note3.rotationZ = 40;
 
            // Show the overlay image on the button
            rubber_depressed.opacity = 1.0;
             
        } else if (event.isUp ()) {
            // Hide the overlay image on the button
            rubber_depressed.opacity = 0.0;
        }
    }
}

For the overlay image, we set the opacity of the overlay image to 0.0 so that it's not visible initially and the button appears to be unpressed. We also specify a value for a property called overlapTouchPolicy, which is a property 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 image that's in the foreground; the background image does not receive touch events. In our app, it makes more sense conceptually for the unpressed button image, instead of the overlay image, 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 background button image.

ImageView {
    overlapTouchPolicy: OverlapTouchPolicy.Allow
    id: rubber_depressed
    imageSource: "asset:///images/rubber_depressed.png"
    opacity: 0.0
     
    layoutProperties: AbsoluteLayoutProperties {
        positionX: 900
        positionY: 510
    }

If we left things the way they are now, when the button is pressed, the change in opacity of the overlay image would be animated using an implicit animation. This isn't really the behavior that we want, so we use an  ImplicitAnimationController  control to disable implicit animation of the opacity property in the overlay image:

    attachedObjects: [
        ImplicitAnimationController {
            id: bulbController
            propertyName: "opacity"
        }
    ]
} // end of ImageView

Create the notes

Now, we create the first of the three notes by using our custom Note component. For a bit of added visual appeal, the note has a small amount of initial rotation. We expose it 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:

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.

    onNewNote: {
        showNote ();
        rotationZ = initialRotation;
        note1.poem = PoemGenerator.generatePoemLine (1);                
    }
} // end of Note

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

    Note {
        id: note2
        property int initialRotation: -12
         
        layoutProperties: AbsoluteLayoutProperties {
            positionX: 472
            positionY: 183
        }
         
        // Set the initial rotation and offscreen starting position
        rotationZ: initialRotation
        showAnimStartX: (-note2.layoutProperties.positionX - 238)
 
        // When the newNote() signal is emitted, show a new note
        onNewNote: {
            showNote ();                
            rotationZ = initialRotation;
            note2.poem = PoemGenerator.generatePoemLine (2);
        }
    }
 
    Note {
        id: note3
        property int initialRotation: -5
         
        layoutProperties: AbsoluteLayoutProperties {
            positionX: 826
            positionY: 147
        }
         
        // Set the initial rotation and offscreen starting position
        rotationZ: initialRotation
        showAnimStartX: (-note3.layoutProperties.positionX - 238)
         
        // When the newNote() signal is emitted, show a new note
        onNewNote: {                
            showNote ();
            rotationZ = initialRotation;
            note3.poem = PoemGenerator.generatePoemLine (3);        
        }
    }
} // end of top-level Container

Generate an initial poem

We're nearly done, but we have a small addition 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. We also make sure that button presses aren't animated by disabling implicit animations for the appropriate control:

    onCreationCompleted : {
        note1.poem = PoemGenerator.generatePoemLine (1);
        note2.poem = PoemGenerator.generatePoemLine (2);
        note3.poem = PoemGenerator.generatePoemLine (3);
 
        bulbController.enabled = false;
    }
} // end of Page

Set the orientation for the app

You may 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 make a change to 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 is run. Inside this tag, add the following lines (right after the <transparent>false</transparent> line) and save the file:

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

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


Screen showing the Poem Maker sample app.

Last modified: 2013-12-21

comments powered by Disqus