Touch propagation

When a user touches a UI control in your app, the Cascades framework delivers a touch event to that control, and you can handle the event in any way you want. These types of interactions are straightforward: users touch the control that they want to interact with, and you specify your app's response based on this intention.

What might not be obvious is that other controls in your app can also receive the touch event and have the opportunity to respond to it. This behavior is called touch propagation and determines how and when the controls in your app receive events. Objects in Cascades are arranged in a hierarchical structure, with parent/child relationships. Consider a  Container that includes two Button controls:

Container {
    Button {
        text: "Button 1"
    }
 
    Button {
        text: "Button 2"
    }
}

The Container is the parent of the Button controls, and the Button controls are children of the Container. Cascades represents this relationship by using a structure called a scene graph. This graph is a tree of controls (also called nodes) that shows the parent/child relationships between all elements in your app. You can't view the scene graph for your app in the Momentics IDE or anywhere else; it's just a concept that makes it easier to visualize how Cascades keeps track of object relationships internally.

Here's what the scene graph would look like for the code sample above:


Diagram showing the scene graph for the given example.

When a user touches a control and a touch event is generated, the control that was touched isn't the only control that receives the event. The parent control (and its parent, and so on up to the root of the scene graph) also receives the event and can act on it. In the example above, if a user touches one of the Button controls, both the Button and its parent Container receive the touch event.

Propagation phases

When a touch event occurs and is delivered to various parent and child nodes in the scene graph, the nodes don't receive the event at the same time. There are three phases of touch propagation, and each phase gives different nodes the opportunity to handle events at different times:

  • Capture phase
  • At-target phase
  • Bubble phase

Capture phase

This phase allows parent nodes to listen for and respond to events that are directed at one of their children. Parent nodes are the first nodes to receive the event, even before the child node that was targeted by the event. You can handle an event in this phase if you want to intercept the event before it reaches the node that was targeted. Consider the following scene graph:


Diagram showing a scene graph for a Container parent with a Label child.

If a user touches the  Label, its parent  Container receives the event first in the capture phase. When a control receives an event in this phase, it emits the  touchCapture() signal to give you the opportunity to handle the event.

Only nodes in the scene graph that have children receive events in the capture phase. Leaf nodes, which are nodes that don't have children, don't receive events in this phase.

Here's how to handle an event in the capture phase in QML. The text of the Label changes to indicate when an event is received in the capture phase. It's important to note that even if the touchCapture() signal is emitted, the parent Container will also emit the  touch() signal in its bubble phase. A custom captured property is used to indicate that an event occurred in the capture phase and the Label text shouldn't be updated when the touch() signal is emitted in the bubble phase.

import bb.cascades 1.0
 
Page {
    content: Container {
        id: captureContainer
         
        // This property indicates whether this container has received
        // a touch event in the capture phase
        property bool captured: false
        Label {
            id: captureLabel
            text: "Not captured"
        }
         
        onTouch: {
            // If the touch event wasn't already received in the
            // capture phase, update the text of the label
            if (!captureContainer.captured)
                captureLabel.text = "Not captured";
            captureContainer.captured = false;
        }
         
        onTouchCapture: {
            // If the touch event is received in the capture phase,
            // update the label text and set the captured property
            // to true (to prevent the text from being updated in
            // the onTouch signal handler)
            captureLabel.text = "Captured!"
            captureContainer.captured = true;
        }
    }
}

Here's how to accomplish the same thing in C++ by connecting the touch() and touchCapture() signals to 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 label and add it to the top-level container
captureLabel = Label::create("Not captured");
topContainer->add(captureLabel);
 
// Connect the signals of the top-level container to slots.
// 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(topContainer,
                            SIGNAL(touch(bb::cascades::TouchEvent*)),
                            this,
                            SLOT(onTouch(bb::cascades::TouchEvent*)));

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

res = QObject::connect(topContainer,
                     SIGNAL(touchCapture(bb::cascades::TouchEvent*)),
                     this,
                     SLOT(onTouchCapture(bb::cascades::TouchEvent*)));

// 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);
 
// Initialize the variable that indicates whether the container has
// received an event in the capture phase
captured = false;
 
// 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::onTouch(bb::cascades::TouchEvent* event)
{
    Q_UNUSED(event);
 
    // If the touch event wasn't already received in the capture phase,
    // update the text of the label
    if (!captured)
        captureLabel->setText("Not captured");
    captured = false;
}
 
// Define the slot function for the touchCapture() signal
void App::onTouchCapture(bb::cascades::TouchEvent* event)
{
    Q_UNUSED(event);
     
    // If the touch event is received in the capture phase, update
    // the label text and set the captured variable to true
    captureLabel->setText("Captured!");
    captured = true;
}
// In your application's header file
 
public slots:
    void onTouch(bb::cascades::TouchEvent* event);
    void onTouchCapture(bb::cascades::TouchEvent* event);
 
private:
    Label* captureLabel;
    bool captured;

At-target phase

This phase occurs when a node receives a touch event and that node is the intended target of the event, as opposed to an ancestor of the intended target. An ancestor of the intended target node is any node that is a parent of the target node, or a parent of the parent of the target node, and so on, all the way up to the root of the scene graph.

This phase is the most common one in which to handle touch events. When an event is received in this phase, you can safely assume that a user is interacting directly with a control on the screen, such as a  Button or a  Label. The  touch() signal is emitted in this phase, and you can handle this signal to respond to a touch that's targeted at a specific control.

Consider the following image of a Button inside a  Container:


Diagram showing a Button within a Container.

If a user touches the button, the Button receives the touch event in the at-target phase because the Button is interpreted as the intended target of the touch event. The Container doesn't receive the event in this phase; instead, it receives the event in the capture phase (as discussed above) and again in the bubble phase. If the user touches anywhere else in the Container (but not the Button), the Container control receives the touch event in the at-target phase.

Bubble phase

This phase occurs after the at-target phase. The event travels sequentially from leaf nodes toward the root of the scene graph, which gives each node along the way the opportunity to handle the event. The touch() signal is emitted in this phase (similar to the at-target phase).

Consider the following image of a  Button in two  Container controls:


Diagram showing a Container with a Button within another Container.

The corresponding scene graph for this arrangement looks like this:


Diagram showing the scene graph of a Container with a Button within another Container.

If a user touches the button, the Button receives the touch event in the at-target phase. If the event isn't consumed in this phase, the event travels up the nodes in the scene graph, and the dark gray Container receives the event in the bubble phase. If the event still isn't consumed, the event reaches the light gray Container, also in the bubble phase.

Determining the propagation phase of an event

You might notice that the  touch() signal is emitted in both the at-target phase and bubble phase. To determine which phase a control receives a touch event, you can use the propagationPhase property, which is part of the  TouchEvent parameter that's included in the touch() signal. By comparing the value of this property to enumeration values in the PropagationPhase class (NoneCapturingAtTarget, and Bubbling), you can respond to touch events in different phases using the same signal handler.

Here's how you might implement the onTouch() signal handler in QML to determine the phase in which a control received a touch event:

onTouch: {
    if (event.propagationPhase == PropagationPhase.AtTarget) {
        // Handle the event in the at-target phase
    } else if (event.propagationPhase == PropagationPhase.Bubbling) {
        // Handle the event in the bubble phase
    }
}

Here's how you might implement an onTouch() slot in C++. Remember that you still need to connect the touch() signal to this slot elsewhere in your app (for example, in the constructor of your app class).

void App::onTouch(bb::cascades::TouchEvent* event)
{
    if (event->propagationPhase() == PropagationPhase::AtTarget) {
        // Handle the event in the at-target phase
    } else if (event->propagationPhase() ==
        PropagationPhase::Bubbling) {
        // Handle the event in the bubble phase
    }
}

Propagation modes

By default, Cascades delivers touch events to all eligible nodes in the scene graph. If you want to consume an event and prevent other nodes from acting on the event, you need to create this functionality yourself. However, Cascades does give you a bit of control over how touch events are propagated to various nodes. You can control whether touch events are delivered to a visual node and its children by using the touchPropagationMode property, which is part of  VisualNode. This property accepts enumeration values from the  TouchPropagationMode class: FullNone, or PassThrough.

When the propagation mode for a node is set to Full (the default value), touch events are delivered to that node and all other eligible nodes that are connected to it in the scene graph. This includes all children of a particular node, if the children were the intended targets of the touch. When the propagation mode is set to None, events are not delivered to the node or any children of the node. When the propagation mode is set to PassThrough, events are not delivered to the node itself, but any eligible children of the node will receive events.

For example, consider the same image of a  Button inside two  Container controls that was presented in the previous section:


Diagram showing a Container with a Button within another Container.

If the touch propagation mode of the dark gray Container is set to None, that Container won't receive touch events. In addition, the Button won't receive touch events either. If the touch propagation mode of the dark gray Container is set to PassThrough, that Container won't receive touch events but the Button will receive them.

Overlap touch policies

Like propagation modes, overlap touch policies let you control which nodes receive touch events in your app. Unlike propagation modes, these policies determine propagation based on how UI controls are displayed on the screen, instead of how they're arranged in the scene graph. You can control whether touch events are allowed to pass through a control and be received by controls behind it by using the overlapTouchPolicy property, which is part of  VisualNode. This property accepts enumeration values from the  OverlapTouchPolicy class (Deny or Allow).

When the overlap touch policy for a control is set to Deny (the default value), touch events aren't delivered to any controls that are placed behind the control on the screen. When the overlap touch policy is set to Allow, touch events pass through controls and are received by controls behind.

It's important to note that overlap touch policies apply only to nodes that aren't ancestors of each other. For example, if you add a  Button to a  Container, the Container is the parent (and, thus, an ancestor) of the Button. Any overlap touch policy that you specify for the Button won't prevent touch events from being received by the Container, even though the Container appears beneath the Button visually. In contrast, if you add two Button controls to the same Container, the buttons aren't ancestors of each other. If the buttons overlap visually on the screen, an overlap touch policy that you apply to the top Button would affect whether touch events are received by the bottom Button.


Diagram illustrating how overlap touch policies apply only to nodes that aren't ancestors of each other.

An example of touch propagation

The three phases of touch propagation don't necessarily occur sequentially, and it can be difficult to visualize the order in which nodes receive a touch event. The following is an example of touch propagation in action.

Consider the following tree of nodes (on the left) and their visual appearance on the screen (on the right):


A diagram illustrating a tree of nodes and their visual appearance.

A user touches exactly where the gray circle is placed (on the image on the right). A touch event is generated and sent to several of the nodes in the tree. From top to bottom, nodes E, D, B, and A all receive the event (as long as none of these nodes consume the event, and the overlap touch policy on node E is set to allow events to pass through to nodes beneath).

Because node E is displayed on top of all of the other nodes, you might expect that this node receives the event first. However, if you recall the different propagation phases, you'll see that things aren't quite that straightforward. To make it easier to visualize the propagation of the touch event, the tree can be broken into two propagation paths: E - A and D - B - A:


A diagram illustrating the propagation of a touch event.

Propagation of the touch event starts on the path that contains the top node that was touched (path E - A). If none of the nodes in the graph consume the event and node E has an overlap touch policy of Allow, the event propagates as follows:

  1. Node A receives the event in the capture phase. This node is the parent of node E, so it's the first node that's eligible to receive and respond to the event.
  2. Node E receives the event in the at-target phase. This node is considered the intended target of the touch event. This node doesn't receive the event in the capture phase because it's a leaf node.
  3. Node B receives the event in the capture phase. This node is the parent of node D, which is immediately beneath node E visually, so node B is the next eligible node to receive the event. Node A doesn't receive the event again because it already received the event during the capture phase.
  4. Node D receives the event in the at-target phase. Similar to node E, this leaf node doesn't receive the event in the capture phase.
  5. Node B receives the event in the bubble phase. Now that all nodes that are eligible to receive the event in the at-target phase (namely, nodes E and D) have processed the event, the event starts to propagate up the tree toward the root node.
  6. Node A receives the event in the bubble phase. Propagation continues up the tree until the root node is reached.

At each step in the propagation, the nodes emit signals ( touchCapture() in the capture phase, touch() in the at-target and bubble phases) so that an app can respond appropriately.

Last modified: 2013-12-21

comments powered by Disqus