3

This is the pull to refresh icon used to refresh views in Android.

enter image description here

I've been trying to bring that to qml but it is not so easy. There are so many transitions that it quickly becomes very complex.

How difficult this should be to recreated in QML? Is using canvas the better solution?

As i have first seen, the swipe brings down the arrow in a different pace of the swipe, while the arrow rotates. If this arrow comes from a canvas how can it relate to outside events, that is the swipe?

Guilherme Souza
  • 344
  • 4
  • 14
  • This is actually fairly easy to implement. If you have difficulties envisioning the implementation, I'd say you have a bit more to learn before you jump into using it. – dtech Jan 15 '18 at 20:42
  • Ok. I'm not asking for code and i don't even like it. Just need some general explanation on the implementation... some direction. I'm very newbie at the moment and you know its very hard to learn when you don't know what is the tool you need to do smth. Answers from more experienced devs here help a lot. Btw, thanks for the advice. – Guilherme Souza Jan 16 '18 at 00:16
  • Just place `Image` over a list or whatever using z-positioning and anchoring. Place `MouseArea` inside to get onClick event and so refresh a list. – folibis Jan 16 '18 at 06:51
  • The OP is asking about an animation like that : https://storage.googleapis.com/material-design/publish/material_v_12/assets/0B6Okdz75tqQsRWlsOUFOeG96RnM/components-progressactivity-behavior-loading-swipedown-noload-xhdpi-005.webm , while it's doable in QML, it's certainly not trivial. Checking http://doc.qt.io/qt-5/qml-qtquick-flickable.html#verticalOvershoot-prop would be a good starting point. As for the rendering of the arrow, either Canvas or the new Shape module in Qt 5.10 seems good. – GrecKo Jan 16 '18 at 09:40
  • Thank you @GrecKo. Unfortunately my version of Qt is 5.7, but `Flickable.verticalOverShoot` was introduced in v. 5.9. I'm really considering the upgrade. By now, despite setting `boundsBehavior` to `stopAtBounds`, i have no way to check what is the `ListView`'s implicit vertical position, since it will always get bounded in the top. `contentY` becomes useless likewise. – Guilherme Souza Jan 16 '18 at 14:02
  • If you can't use 5.10, handling the mouse events from c++ seems to be the only reasonable solution to me. – GrecKo Jan 16 '18 at 14:54
  • C++ seems kind of a complicated hassle when it is much more easily achievable using only QML. Also consider that flickable and list view in their C++ form are part of the private api and not really intended to be extended or modified or even used directly from C++. Just use a mouse area on top of the inactivated view and use that to drive the view as already suggested. – dtech Jan 18 '18 at 09:46

4 Answers4

5

I used something like this:

 //
 //  Slot called when the flick has started
 //
 onFlickStarted: {
     refreshFlik = atYBeginning
 }

 //
 //  Slot called when the flick has finished
 //
 onFlickEnded: {
     if ( atYBeginning && refreshFlik )
     {
         refresh()
     }
 }

It seems to work as expected and it is easy to implement

Patryk Kubiak
  • 71
  • 1
  • 5
1

The problem is that Flickable and the derived ListView don't really provide any over-drag or over-shoot information in the cases where the visual behavior is disabled.

If dragging the visual over the beginning is not a problem for you, you can simply use the negated value of contentY which goes into the negative if the view is dragged before its beginning.

The only solution I can think of to not have any visual over-dragging but still get the over-drag information in order to drive your refresher is to set the view interactive property to false, and put another mouse area on top of that, and redirect drags and flicks manually to the now non-interactive view.

That last part might sound complex, but it isn't that complex, and I happen to know for a fact that it works well, because I have already used this approach and the source code is already here on SO.

So once you have access to the mouse area that controls the view, you can track how much you are in the negative, and use that information to drive the logic and animation of the refresher.

The notable difference between the implementation in the linked answer and what you need is that the linked answer has the mouse area in each delegate, due to the requirements of the specific problem I wanted to solve. You don't need that, you only need one single mouse area that covers the view.

dtech
  • 47,916
  • 17
  • 112
  • 190
  • It does have overshoot though : http://doc.qt.io/qt-5/qml-qtquick-flickable.html#verticalOvershoot-prop – GrecKo Jan 16 '18 at 09:45
  • "The overshoot distance is reported even when boundsMovement is Flickable.StopAtBounds." – GrecKo Jan 16 '18 at 10:33
  • OK, contrary to what the documentation says, overshoot is **not** being reported with `StopAtBounds`. At least not in 5.9.1 which I am using. It is reported when `OvershootBounds` but only for flicks, not for dragging, and it is reported for `DragOverBounds` but not for flicking and the content doesn't stay on the bounds, and of course, it is reported for `DragAndOvershootBounds`, but `StopAtBounds` is the one that is needed and the overshoot value doesn't chance with it, it is always 0. Looks like it has stuff to do with `boundsMovement` which is a 5.10 thing. – dtech Jan 16 '18 at 10:54
  • Indeed, it was added in 5.10 https://bugreports.qt.io/browse/QTBUG-38515 – GrecKo Jan 16 '18 at 12:10
  • For anyone using 5.10+ it would provide an easy way to get at 90% of the desired behavior. For me it is not an option because of regressions. And of course, you cannot possibly get the exact behavior without having explicit control over the user interaction. – dtech Jan 16 '18 at 12:24
1

I did like this recently.

Basically I use the position of a ScrollBar and if it goes negative I show a spinner and refresh. So I don't need to mess with the flick stuff.

import QtQuick.Controls 6.0
import QtQuick 6.0

ListView {

    ScrollBar.vertical: ScrollBar {
        id: scrollbar
    }
    property bool negativescroll: scrollbar.position < 0
    onNegativescrollChanged: {
        if (spinner.visible) {
            refresh()
        }
        spinner.visible = !spinner.visible
    }

    BusyIndicator {
        anchors.top: parent.top
        anchors.horizontalCenter: parent.horizontalCenter
        visible: false
        running: visible
        id: spinner
    }


    width: 180; height: 200

    model: model
    delegate: Text {
        text: name + ": " + number
    }
    ListModel {
        id: model
        ListElement {
            name: "Bill Smith"
            number: "555 3264"
        }
        ListElement {
            name: "John Brown"
            number: "555 8426"
        }
        ListElement {
            name: "Sam Wise"
            number: "555 0473"
        }
    }
}
LtWorf
  • 7,286
  • 6
  • 31
  • 45
0

I came to a simpler solution based on dtech's experience involving multiple Flickable elements, which basically consists on filling the Flickable with a MouseArea, setting its boundsBehavior property to Flickable.StopAtBounds, and from there, if it's at the top, do things based on mouseY values.

The better approximation i could get is in the following code. A possible drawback is that diagonal swiping also counts as a refresh intention. It could be improved with GestureArea, but i'm too lazy to get my hands on this at the moment.

import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Window 2.2

ApplicationWindow {
    property real mm: Screen.pixelDensity
    property real margins: 2 * mm
    id: mainWindow
    visible: true
    width: 60 * mm
    height: 120 * mm
    title: qsTr("Hello World")

    ListModel {
        id: myModel
        Component.onCompleted: {
            for(var i = 0; i <= 100; ++i) {
                myModel.append({num: i})
            }
        }
    }
    ListView {
        id: view
        boundsBehavior: Flickable.StopAtBounds
        interactive: true
        anchors.fill: parent
        model: myModel
        spacing: 4
        delegate: Rectangle {
            width: parent.width
            height: 25 * mm
            border.color: 'red'
            Text {
                id: name
                text: num
                anchors.centerIn: parent
            }
        }
        Rectangle {
            signal follow
            id: swatch
            width: 15 * mm
            height: width
            radius: width / 2
            color: 'lightgray'
            anchors.horizontalCenter: parent.horizontalCenter
            y: - height
        }
        MouseArea {
            property int mouseYSart
            property int biggerMouseY
            anchors.fill: view
            onPressed: {
                mouseYSart = mouseY
                biggerMouseY = 0
            }
            onMouseYChanged: {
                if(view.contentY == 0) {
                    var currentMouseY = mouseY
                    if(currentMouseY > biggerMouseY) {
                        biggerMouseY = currentMouseY
                        swatch.y += 1
                    }
                    if(currentMouseY < biggerMouseY) {
                        biggerMouseY = currentMouseY
                        swatch.y -= 1
                    }
                }
            }
            onReleased: swatch.y = - swatch.height
        }
    }
}
Guilherme Souza
  • 344
  • 4
  • 14