Sunday, April 13, 2014

Creating Custom Swipe handler in QML

I was often request for sample that shows how custom swipe handler can be created in QML. In this post I will show how same can be achieved.

Please note that code is just prototype level code and is not tested well with actual use. It has also lots of hard coded value that assume certain size of application.

But, you should be able to change those according to your use and can try sample with your app.

This sample implement three QML views and you can swipe on that to change view from one to next. This code also implement some parallax effect on view and view transition animation. In addition to swipe you can also change view using keyboard Left/Right arrow key. Following is demo for the sample app.



So let's start with code.

Following code is from SwipeHandler.qml, it extends MouseArea and try to detect swipe based on mouse's x position change. Swipe can be generated by two way, by flicking on view or dragging it.
Flick is detected, if there is large change in mouse x position in less time. In case of drag, if mouse travel certain distance then code consider it as a swipe.

import QtQuick 2.0

MouseArea{
    id: root

    property int oldX: mouseX;
    property int swipeOffset: 100;
    property int originX:mouseX;

    property var gestureStartTime;
    property bool gestureStarted: false;

    signal swipeEnded(var diff);
    signal swipeContinues(var diff);

    anchors.fill: parent

    onReleased: {
        if( gestureStarted ) {
            //swipe canceled
            root.swipeEnded(0);
            resetGesture();
        }
        //else swipe is already ended
    }

    onPressed: {
        gestureStarted =  true;
        gestureStartTime = new Date();
    }

    onMouseXChanged: {
        if( mouseX < parent.x
        || mouseX > parent.width || gestureStarted == false )
            return;

        if( originX == 0 ) {
            originX = mouseX; oldX = mouseX;
            return;
        }

        var diff = (oldX - mouseX);
        if(handleFlick(diff)){
            return;
        }

        if( haldleDrag(mouseX, diff)){
            return;
        }

        oldX = mouseX;
        root.swipeContinues(diff);
    }

    function resetGesture() {
        originX = 0; oldX = 0;
        gestureStarted =  false;
    }

    function haldleDrag(xPos,xPosDiff){
        if(xPosDiff < 0) {
            if( Math.abs(originX-xPos)  > swipeOffset ){
                root.swipeEnded(xPosDiff);
                resetGesture();
                return true;
            }
        } else {
            if( Math.abs(originX-xPos) >  swipeOffset ){
                root.swipeEnded(xPosDiff);
                resetGesture();
                return true;
            }
        }
        return false;
    }

    function handleFlick(xPosDiff){
        var now = new Date();
        var timeDiff = now - gestureStartTime;

        //high velocity and large diff between start end point
        if(timeDiff < 40 && Math.abs(xPosDiff) > 10 ){
            if(xPosDiff < 0) {
                root.swipeEnded(xPosDiff);
                resetGesture();
                return true;
            } else {
                root.swipeEnded(xPosDiff);
                resetGesture();
                return true;
            }
        }
        return false;
    }
}
So, this was SwipeHandler which can detect if swipe is generated or not. To demonstrate its use, I created a small View Management component, that create's three views. On swipe, view changes form one to another base on direction of swipe movement. Here is code for the same.
import QtQuick 2.0

Rectangle {
    id: root
    width: 200
    height: 300

    property var delegate: comp;

    property var centralView;
    property var nextView;
    property var prevView;

    focus: true

    Component.onCompleted: {
        var colors = ["red","blue","green"];
        var objs = [];
        for(var i =0; i < 3; ++i){
            var obj = comp.createObject(root);
            obj.text = i+1;
            obj.color = colors[i];
            objs.push(obj);
        }

        centralView = objs[0]
        nextView = objs[1]
        prevView = objs[2]

        setViewPos();
    }

    function setViewPos(oldX){
        centralView.animate(50,0);
        nextView.animate(50,root.width);
        prevView.animate(50,-root.width);

        centralView.z = 1;
        nextView.z = 0;
        prevView.z = 0;
    }

    Keys.onRightPressed: {
        var tempView = centralView;
        centralView = prevView;
        prevView = nextView;
        nextView = tempView;

        centralView.animate(150,0);
        nextView.animate(150,root.width);
        prevView.x = -width
    }

    Keys.onLeftPressed: {
        var tempView = centralView;
        centralView = nextView;
        nextView = prevView;
        prevView = tempView;

        centralView.animate(150,0);
        prevView.animate(150,-root.width);
        nextView.x = width
    }

    SwipeArea{
        onSwipeEnded: {
            if(diff === 0) {
                root.setViewPos();
                return;
            }

            var tempView = centralView;
            if(diff < 0) {
                centralView = prevView;
                prevView = nextView;
                nextView = tempView;
            } else {
                centralView = nextView;
                nextView = prevView;
                prevView = tempView;
            }
            root.setViewPos();
        }

        onSwipeContinues: {
            centralView.x = centralView.x - diff;
            if(diff < 0) {
                prevView.x = prevView.x  + Math.abs(diff*1.6);
                prevView.z = 1
                centralView.z = 0;
            } else {
                nextView.x = nextView.x - Math.abs(diff*1.6) ;
                nextView.z = 1
                centralView.z = 0;
            }
        }
    }

    Component{
        id: comp
        Rectangle{
            id: rect
            property alias text: label.text

            width: parent.width; height: parent.height
            Text{
                id: label; anchors.centerIn: parent
            }

            function animate(duration, to){
                anim.to = to; anim.duration = duration
                anim.running = true
            }

            PropertyAnimation{
                id: anim; target:rect; property: "x";duration: 50
            }
        }
    }
}