Search code examples
androidqtqmlpinchzoom

QML PinchArea not working as expected with QML Flickable


So I was implementing a simple image viewer and wanted to implement pinch to zoom inside a Flickable. After looking at several example, I came across this and thought I could do something similar. However, when I tested the code (Qt 5.13 on Android) it doesn't work as expected. The problem is that when the image is zoomed, the PinchArea stops working and the user is unable to zoom out while the Flickable still allows to "flick" the image (at that zoom level). Here is the code, which I couldn't find any mistake with:

import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: qsTr("Flickable Image Zoom Example")

    Page {
        id: imagePage

        property string imgUrl: "qrc:/messi.jpg"
        property string strHpTitle: ""
        property string strThumbnailUrl: ""

        anchors.fill: parent
        Flickable {
            id: imageFlickable
            anchors.fill: parent
            contentWidth: imageContainer.width; contentHeight: imageContainer.height
            clip: true
            onHeightChanged: if (imagePreview.status === Image.Ready) imagePreview.fitToScreen();

            Item {
                id: imageContainer
                width: Math.max(imagePreview.width * imagePreview.scale, imageFlickable.width)
                height: Math.max(imagePreview.height * imagePreview.scale, imageFlickable.height)

                Image {
                    id: imagePreview

                    property real prevScale

                    function fitToScreen() {
                        scale = Math.min(imageFlickable.width / width, imageFlickable.height / height, 1)
                        pinchArea.minScale = scale
                        prevScale = scale
                    }

                    anchors.centerIn: parent
                    fillMode: Image.PreserveAspectFit
                    cache: false
                    asynchronous: true
                    source: imagePage.imgUrl
                    sourceSize.height: 1000;
                    smooth: !imageFlickable.moving

                    onStatusChanged: {
                        if (status == Image.Ready) {
                            fitToScreen()
                        }
                    }
                    onScaleChanged: {
                        if ((width * scale) > imageFlickable.width) {
                            var xoff = (imageFlickable.width / 2 + imageFlickable.contentX) * scale / prevScale;
                            imageFlickable.contentX = xoff - imageFlickable.width / 2
                        }
                        if ((height * scale) > imageFlickable.height) {
                            var yoff = (imageFlickable.height / 2 + imageFlickable.contentY) * scale / prevScale;
                            imageFlickable.contentY = yoff - imageFlickable.height / 2
                        }
                        prevScale = scale
                    }
                }
            }

            PinchArea {
                id: pinchArea

                property real minScale: 1.0
                property real maxScale: 3.0

                anchors.fill: parent
                pinch.target: imagePreview
                pinch.minimumScale: minScale
                pinch.maximumScale: maxScale
                pinch.dragAxis: Pinch.XandYAxis

                onPinchFinished: {
                    imageFlickable.returnToBounds()
                }
            }
        }
    }
}

Solution

  • Ok so I found the solution to this problem. The issue (I think) is due to a bug. For some strange reason, the Flickable in my code steals touch events from the PinchArea so the pinching touch events are ignored and passed onto the Flickable. In order to solve the problem, I found a workaround which consists in creating a MouseArea inside the PinchArea, this allows the PinchArea to receive the touch events so that the Flickable doesn't steal them. So with the workaround my code has the following structure:

    Flickable{
       PinchArea{
          MouseArea{ anchors.fill: parent} // this is the workaround
       }
       Item{}
    }
    

    I have reported the bug here and uploaded a small video showing the cause.