Touch interaction

Almost every app that you develop needs to provide support for user interaction. This interaction might be through keyboard input or sensors on the device, but most commonly, users will interact with your app by touching the screen of the device. Users can touch different UI controls that you display on the screen, and your app can handle each touch event in any way you want.


Diagram illustrating a touch interaction.

Using interactive controls

The Cascades framework provides some basic touch handling that you can use in your apps right away, without using a lot of code. Most Cascades UI controls are interactive and handle basic touch events automatically. For example, the  Button control handles touch events to determine whether it has been pressed or released and should change its appearance accordingly.

Many UI controls also contain signals that are emitted when users touch or otherwise interact with the control. For example, a Button emits the  clicked() signal when it's clicked, and a  CheckBox emits the  checkedChanged() signal when it's checked or cleared.

The signals that core controls emit typically correspond to the most common ways that users interact with the controls, which makes responding to these signals a great way to handle common interactions. The Cascades framework implements detection of the touch event automatically; all you need to do is take action when the appropriate signal is emitted.

Here's how to handle signals from a Button and a CheckBox by using predefined signal handlers in QML. Cascades provides these signal handlers for you to use, and you can access them by using the prefix "on," followed by the name of the signal that you want to handle. In this code sample, the checked status of the check box is displayed beside it, and clicking the button resets the checked status to its default value (unchecked).

import bb.cascades 1.0
 
Page {
    content: Container {
        CheckBox {
            id: myCheckBox
            text: "Unchecked"
             
            // When the checked status changes, update the check box's 
            // text to reflect the new state
            onCheckedChanged: {
                if (checked)
                    myCheckBox.text = "Checked";
                else
                    myCheckBox.text = "Unchecked";
            }
        }
         
        Button {
            text: "Reset"
             
            // When the button is clicked, reset the checked status
            // of the check box
            onClicked: {
                myCheckBox.resetChecked();
            }
        }        
    } // end of Container
} // end of Page

Here's how to do the same thing in C++ by connecting the signals from the Button and CheckBox to slot functions that you create yourself. For more information about handling events using signals and slots, see Signals and slots.

// In your application's source file
 
// Create the root page and top-level container
Page* root = new Page;
Container* topContainer = new Container;
 
// Create the check box and button, and add them to the
// top-level container
myCheckBox = CheckBox::create("Unchecked");
resetButton = Button::create("Reset");
topContainer->add(myCheckBox);
topContainer->add(resetButton);
 
// Connect the signals of the check box and button to slot functions.
// If any Q_ASSERT statement(s) indicate that the slot failed to
// connect to the signal, make sure you know exactly why this
// has happened. This is not normal, and will cause your app
// to stop working
bool res = QObject::connect(myCheckBox, SIGNAL(checkedChanged(bool)),
                            this, SLOT(handleCheckedChanged(bool)));

// This is only available in Debug builds
Q_ASSERT(res);

res = QObject::connect(resetButton, SIGNAL(clicked()),
                       this, SLOT(handleButtonClicked()));
// This is only available in Debug builds
Q_ASSERT(res);
 
// Since the variable is not used in the app, this is added to avoid a 
// compiler warning
Q_UNUSED(res);
 
// Set the content of the page and display it
root->setContent(topContainer);
app->setScene(root);
// Define the slot function for the checkedChanged() signal
void App::handleCheckedChanged(bool checked)
{
    if (checked) {
        myCheckBox->setText("Checked");
    } else {
        myCheckBox->setText("Unchecked");
    }
}
 
// Define the slot function for the clicked() signal
void App::handleButtonClicked()
{
    // The resetChecked() function of myCheckBox is actually a slot,
    // so you could simply connect it directly to the clicked()
    // signal instead of using this intermediate
    // handleButtonClicked() slot function
    myCheckBox->resetChecked();
}
// In your application's header file
 
public slots:
    void handleCheckedChanged(bool checked);
    void handleButtonClicked();
 
private:
    CheckBox* myCheckBox;
    Button* resetButton;

Handling gestures

As described above, using interactive UI controls is a good way to handle common touch interactions. In some situations, you might want to change these interactions or add interactions to non-interactive controls (such as containers or labels). You can listen for and respond to specific touch interactions called gestures. A gesture is a type of touch event that represents a simple, complete action that users can perform. The Cascades framework provides classes that let you handle the following gestures:

  • Tap
  • Double tap
  • Pinch
  • Long press (press-and-hold)

Supporting gestures can be a good way to enhance the user experience of your apps. You can support interactions that users have come to expect from apps on mobile devices, and provide an intuitive, easy-to-use interface.

Consider an app that lets users browse a list of photos. When a user views a single photo, a double tap might resize the photo so that it fills the entire screen, and a pinch might let the user zoom in and out of the photo. A long press could display a menu that lets the user send the photo to their friends or save it in an album. By anticipating the things that users will want to do in your app and supporting gestures that users expect, you can provide an exceptional user experience.

Each supported gesture has a corresponding handler class:  TapHandler DoubleTapHandler, PinchHandler, and  LongPressHandler. These handler classes all inherit from GestureHandler, which is the base class for gesture handlers.

Diagram showing the class structure of the GestureHandler class and its subclasses.

When you want to listen for a gesture, you add the appropriate handler to the UI control that users will interact with. Every object that inherits from  VisualNode includes a gestureHandlers list property, and you can use this property to add gesture handlers to the object.

Each gesture handler emits a signal (or multiple signals) when its gesture is detected. A TapHandler simply emits the  tapped() signal when a tap occurs, but a PinchHandler emits signals such as pinchStarted() (when a user places a second finger on the screen to start a pinch) and pinchUpdated() (when the user moves one or both fingers while touching the screen).

Each signal includes an object parameter that corresponds to the type of event that occurred: TapEvent DoubleTapEvent PinchEvent, and  LongPressEvent. These objects contain information about the event, such as its location and the time it occurred. For pinch gestures, the PinchEvent includes properties for the distance, rotation, and midpoint of the pinch gesture.

Here's how to add a handler to respond to pinch gestures on an  ImageView in QML. When a pinch gesture is detected on the ImageView, the pinchRatio property of the PinchEvent is used to increase or decrease the scale of the image as the pinch expands or contracts.

import bb.cascades 1.0
 
Page {
    content: Container {
        // The top-level container uses a dock layout so that the
        // image can always remain centered on the screen as it
        // changes size
        layout: DockLayout {}
         
        ImageView {
            id: myImage
 
            // This custom property stores the initial scale of the
            // image when a pinch gesture begins
            property double initialScale: 1.0
             
            // This custom property determines how quickly the image
            // grows or shrinks in response to the pinch gesture
            property double scaleFactor: 0.8
 
            horizontalAlignment: HorizontalAlignment.Center
            verticalAlignment: VerticalAlignment.Center
 
            imageSource: "asset:///images/myImage.png"
             
            gestureHandlers: [
                // Add a handler for pinch gestures
                PinchHandler {
                    // When the pinch gesture starts, save the initial
                    // scale of the image
                    onPinchStarted: {
                        myImage.initialScale = myImage.scaleX;
                    }
                     
                    // As the pinch expands or contracts, change
                    // the scale of the image
                    onPinchUpdated: {
                        myImage.scaleX = myImage.initialScale +
                                ((event.pinchRatio - 1) *
                                    myImage.scaleFactor);
                        myImage.scaleY = myImage.initialScale +
                                ((event.pinchRatio - 1) *
                                    myImage.scaleFactor); 
                    }
                }
            ]
        } // end of ImageView
    } // end of Container
} // end of Page

In C++, the process is similar. When the PinchHandler is created, the onPinch builder argument is used to specify the slots that should handle each signal. The onPinch builder argument uses QObject::connect() to connect the signals to the slots that you specify.

Here, only slots that handle the pinchStarted() and pinchUpdated() signals are specified (0 is specified for the other slots), but you could add slots for the pinchEnded() and pinchCancelled() signals as well. For simplicity, it's assumed that the ImageView and the slot functions (along with the variables initialScale and scaleFactor) are declared in a corresponding header file.

// In your application's source file
 
// Create the root page and top-level container
Page* root = new Page;
Container* topContainer = Container::create()
        .layout(DockLayout::create());
 
// Create the image to scale using pinch gestures
myImage = ImageView::create("asset:///images/myImage.png")
           .horizontal(HorizontalAlignment::Center)
           .vertical(VerticalAlignment::Center);
 
// Create the pinch handler, connect its pinchStarted() and 
// pinchUpdated() signals to slots, and add the handler
// to the image
PinchHandler* pinchHandler = PinchHandler::create()
        .onPinch(this,
            SLOT(onPinchStart(bb::cascades::PinchEvent*)),
            SLOT(onPinchUpdate(bb::cascades::PinchEvent*)),
            0,
            0);
myImage->addGestureHandler(pinchHandler);
 
// Initialize a variable to store the initial scale of the image
// when a pinch gesture begins
initialScale = 1.0;
 
// Initialize a variable to determine how quickly the image grows or
// shrinks in response to the pinch gesture
scaleFactor = 0.8;
 
// Add the image to the top-level container, and add the top-level
// container to the page and display it
topContainer->add(myImage);
root->setContent(topContainer);
app->setScene(root);
 
...
 
// Define the slot function for the pinchStarted() signal
void App::onPinchStart(bb::cascades::PinchEvent* event)
{
    // Indicate that the event argument isn't used in this function,
    // to prevent a compiler warning
    Q_UNUSED(event);
     
    // When the pinch gesture starts, save the initial scale of
    // the image
    initialScale = myImage->scaleX();
}
 
// Define the slot function for the pinchUpdated() signal
void App::onPinchUpdate(bb::cascades::PinchEvent* event)
{
    // As the pinch expands or contracts, change the scale of the image
    myImage->setScaleX(initialScale +
                       ((event->pinchRatio() - 1) * scaleFactor));
    myImage->setScaleY(initialScale +
                       ((event->pinchRatio() - 1) * scaleFactor));
}

Handling generic touch events

As you write more complex apps, you may need to handle touch events with more precision or flexibility. The controls that you use might not emit signals for the types of user interaction that you want to respond to. Or, you might want your app to react to gestures other than the predefined ones that Cascades supports. In these cases, you can capture touch events directly and respond to them.

Every object that inherits from the  Control class includes a signal called  touch(), which is emitted when the object receives a touch event. This includes core UI controls (such as  Button and Container) and any custom controls that you create by extending the  CustomControl class. You can respond to the touch event by using the predefined signal handler onTouch in QML, or by connecting the touch() signal to a slot in C++.

Here's how to respond to touch events on a Container by changing its color in QML.

import bb.cascades 1.0
 
Page {
    content: Container {
        Container {
            preferredWidth: 300
            preferredHeight: 300
            background: Color.Blue
             
            // When the container is touched, change its background
            // color to green
            onTouch: {
                background = Color.Green;
            }
        }
    } // end of top-level Container.
} // end of Page.

Here's how to accomplish the same thing in C++. Similar to previous code samples, the Container that changes color and the slot that handles the touch() signal should both declared in a corresponding header file. The parameter of the touch() signal is a type that's specific to Cascades ( TouchEvent ), so you need to use the fully qualified name (bb::cascades::TouchEvent) when you specify it in connect().

// In your application's source file.
 
// Create the root page and top-level container.
Page* root = new Page;
Container* topContainer = new Container;
 
// Create the container that changes color and add it to the top-level
// container.
colorContainer = Container::create()
        .preferredWidth(300)
        .preferredHeight(300)
        .background(Color::Blue);
topContainer->add(colorContainer);
 
// Connect the container's touch() signal to a slot.
// If any Q_ASSERT statement(s) indicate that the slot failed to
// connect to the signal, make sure you know exactly why
// this has happened. This is not normal, and will cause your
// app to stop working
bool connectResult = QObject::connect(colorContainer,
                            SIGNAL(touch(bb::cascades::TouchEvent*)),
                            this, SLOT(handleTouch()));
                            
// This is only available in Debug builds.
Q_ASSERT(connectResult);
 
// Since the variable is not used in the app, this is added to avoid a 
// compiler warning.
Q_UNUSED(connectResult);
 
// Set the content of the page and display it.
root->setContent(topContainer);
app->setScene(root);
 
// ...
 
// Define the slot function for the touch() signal.
void App::handleTouch()
{
    colorContainer->setBackground(Color::Green);
}

Retrieving touch event information

You might have noticed that when you connect the  touch() signal to a slot, the signal includes a TouchEvent parameter. This parameter holds the real power of handling touch events manually; it contains additional information about the touch event, such as the position that the touch occurred (in either screen coordinates or local coordinates) and the type of touch interaction (up, down, or move). You can use enumeration values in the  TouchType class (such as TouchType::Up and TouchType::Down) to determine the kind of touch interaction that a touch event represents.

Here's how to use some of the properties that are included in TouchEvent to move a blue square around the screen in QML. Touching an area of the screen moves the square to that position, and touching and holding the screen lets you drag the square around the screen. The color of the square also changes to green as long as a finger is held on the screen (that is, until an "up" touch event is received, which indicates that the finger has been released).

import bb.cascades 1.0
 
Page {
    content: Container {
        // A container to represent a blue square.
        Container {
            id: movingContainer
            preferredWidth: 150
            preferredHeight: 150
            background: Color.Blue
        }
         
        onTouch: {
            // If the touch event is a move event, change the color
            // of the square to green. If the touch event is an
            // up event, change the color back to blue
            if (event.touchType == TouchType.Move)
                movingContainer.background = Color.Green;
            else if (event.touchType == TouchType.Up)
                movingContainer.background = Color.Blue;
                 
            // Determine the location inside the container that
            // was touched, and move the blue square to that
            // location
            movingContainer.translationX = event.localX -  
                (movingContainer.preferredWidth / 2);
            movingContainer.translationY = event.localY - 
                (movingContainer.preferredHeight / 2);
        }
    } // end of top-level Container.
} // end of Page.

Here's how to achieve the same result in C++. Again, for simplicity, it's assumed that the Container that moves and the slot that handles the touch() signal are both declared in a corresponding header file.

// In your application's source file.
 
// Create the root page and top-level container.
Page* root = new Page;
Container* topContainer = new Container;
 
// Create the container that represents a blue square and add it to
// the top-level container.
movingContainer = Container::create()
        .preferredWidth(150)
        .preferredHeight(150)
        .background(Color::Blue);
topContainer->add(movingContainer);
 
// Connect the top-level container's touch() signal to a slot.
// If any Q_ASSERT statement(s) indicate that the slot failed to
// connect to the signal, make sure you know exactly why this
// has happened. This is not normal, and will cause your app
// to stop working
bool connectResult = QObject::connect(topContainer,
                        SIGNAL(touch(bb::cascades::TouchEvent*)),
                        this, 
                        SLOT(handleTouch(bb::cascades::TouchEvent*)));
                            
// This is only available in Debug builds.
Q_ASSERT(connectResult);
 
 // Since the variable is not used in the app, this is added to
 // avoid a compiler warning.
Q_UNUSED(connectResult);
 
// Set the content of the page and display it.
root->setContent(topContainer);
app->setScene(root);
 
...
 
// Define the slot function for the touch() signal.
void App::handleTouch(bb::cascades::TouchEvent* event)
{
    // If the touch event is a move event, change the color of the
    // square to green. If the touch event is an up event, change
    // the color back to blue
    if (event->touchType() == TouchType::Move)
        movingContainer->setBackground(Color::Green);
    else if (event->touchType() == TouchType::Up)
        movingContainer->setBackground(Color::Blue);
 
    // Determine the location inside the container that was touched,
    // and move the blue square to that location.
    movingContainer->setTranslationX(event->localX() -
        (movingContainer->preferredWidth() / 2));
    movingContainer->setTranslationY(event->localY() -
        (movingContainer->preferredHeight() / 2));
}

Related resources

Web-based training

Sample apps

Last modified: 2013-12-21

comments powered by Disqus