Pong

October 1st 2014, Kinetic Scrolling

September 29th 2014 Hiking Phone | | October 2nd 2014 Summary of UI changes

Adding kinetic scrolling to Qt mobile applications to mimic the behavior of native Android apps is pretty simple. For example, making a Qt scroll area move kinetically with Qt 5.3 is a one-liner:

QScroller::grabGesture(scrollArea, QScroller::LeftMouseButtonGesture);

But what about swipe gestures for arbitrary widgets, which do not have a default implementation for scrolling, e.g. for a tab widget? Let’s consider the following use case:

Having a tab widget, we would like to change the current tab on horizontally swiping left or right. The swipe should also be animated, so that the swipe will look like if the current tab widget is scrolled out of sight, until it is replaced by the left or right neighbor tab widget. Additionally a tab widget, which is another scroll area, should get kinetically scrolled for vertical swipes.

Can we do this with QScroller? Principially, we can, but the scroller just scrolls any given QObject by sending scroll resp. gesture events to the object. Also changing the current tab upon a left or right swipe requires to subclass the tab and to specify its reaction upon receiving a scroll event from the scroller by reimplementing the event() method to catch any QScrollEvent and react accordingly.

Is that easy? No, it is not, because one needs to understand the implementation of QScroller to see which events eventually are generated in which case and at which point in time and how one should react on them. I was reading the implementation of QAbstractScrollArea and QScrollArea and did not get much further. Reading the QScroller examples wasn’t helpful either because they just show how to scroll a scroll area. What is more, it is not apparent how multiple scrollers for the horizontal and vertical swipes, which have different receiving widgets, would interact.

So, I decided to make my own dedicated kinetic scroller, that is flexible to scroll and also is able to detect swipes to let arbitrary widgets react on them. It basically is an event filter for mouse events:

  • on MousePressEvent:
    • remember actual mouse position
    • consume the event, so that a mouse click is not generated before we know whether or not it is a swipe
  • on MouseMoveEvent:
    • calculate the relative mouse position to the remembered position
    • if the relative position exceeds a threshold, determine if its a left, right, down or up swipe
  • on MouseReleaseEvent:
    • if there was a swipe detected send an according signal
    • else send a mouse click event
    • if the speed of the mouse movement (in pixels/s) exceeds a threshold, trigger a repeating timer, that drives the kinetic scrolling
      • let fps be the frame rate of the kinetic scrolling
      • then the trigger period of the timer is 1/fps
      • for each timeout of the timer:
        • send kinetic scroll signals that continue the mouse motion with the last recognized speed (in pixels/s)
        • damp the speed with a damping coefficient
          • if the speed is less than 0.5px/s*fps stop the kinetic motion and the timer

The above scheme is easy enough to understand and implement, so I spare the details on that. The difficult part is about which object to install the above event filter on:

Supposed we want to make a QWidget react on a swipe then we create an instance of the above event filter, install the event filter on the widget and connect the swipe signal of the filter with a slot of the widget. Does that work? No, it does not work, because Qt propagates mouse events directly to the widget that has been clicked at. This means that the top-level widget’s slot does not receive the mouse events it’s ought to filter, because its children are receiving them instead. A solution would be to install filters on all visible widgets, but then each widget needs to be sub-classed, an infeasible solution.

The solution is to install an event filter on the qApp object, since all events are passed through this object. Then the filter must determine if the event is meant to be delivered to the particular widget in question. In that case the widget receives the swipe events.

Here’s the rest of the implementation of the swipe filter:

Supposed we want to make a QWidget “widget” react on a swipe we use the SwipeFilter like so:

SwipeFilter *filter = new SwipeFilter(&widget);

connect(filter, SIGNAL(swipe(SwipeDirection, double)),
        &widget, SLOT(swipe(SwipeDirection, double)));

If the receiving widget is a sub-classed tab widget as in our example, we use the swipe signals to change the current tab like so:

void SwipeTabWidget::swipe(SwipeDirection direction, double speed)
{
   int index = currentIndex();

   if (direction == SwipeLeft)
      index++;
   else if (direction == SwipeRight)
      index--;

   if (index >= count())
      index = count()-1;
   else if (index < 0)
      index = 0;

   setCurrentIndex(index);
}

And here’s the rest of the implementation of the swipe tab widget:


September 29th 2014 Hiking Phone | | October 2nd 2014 Summary of UI changes

Options: