import QtQuick 2.9 import QtQuick.Controls 2.4 import QtGraphicalEffects 1.0 Item { id: control state: "idle" property var target // targeted ListView property bool autoConfigureTarget: true // should the target be automaticaly be configured? property int postRefreshDelay: 1000 // delay after reload funcion has finished property int preRefreshDelay: 1000 // delay before reload funcion is called property int refreshPosition: height * 1.2 // position of the item when refreshing property int dragOutPosition: height * 1.8 // maximum drag out property double dragRefreshPositionMultiplier: 0.5 // position of the item when starting to refresh property color backgroundColor: "white" // color for the pre-defined background property color pullIndicatorColor: "black" // color for the pre-defined pull indicator //property color busyIndicatorColor: "pink" // color for the pre-defined busy indicator readonly property double dragProgress: Math.min( userPosition / dragOutPosition, 1) property Component background: Item { RectangularGlow { anchors.fill: backgroundRe scale: 0.8 * backgroundRe.scale cornerRadius: backgroundRe.radius color: "black" glowRadius: 0.001 spread: 0.2 } Rectangle { id: backgroundRe anchors.fill: parent radius: width * 0.5 color: control.backgroundColor } } property Component busyIndicator: BusyIndicator { running: true } property Component pullIndicator: Canvas { property double drawProgress: control.dragProgress rotation: drawProgress > control.dragRefreshPositionMultiplier ? 180:0 onDrawProgressChanged: { requestPaint() } onPaint: { var ctx = getContext("2d"); var topMargin = height * 0.1 var bottomMargin = topMargin var rightMargin = 0 var leftMargin = 0 var arrowHeight = height - topMargin - bottomMargin var peakHeight = arrowHeight * 0.35 var peakWidth = peakHeight var lineWidth = 2 var progress = drawProgress * 1 / control.dragRefreshPositionMultiplier > 1 ? 1 : drawProgress * 1 / control.dragRefreshPositionMultiplier // modify all values to math the progress arrowHeight = arrowHeight * progress if(progress > 0.3){ peakHeight = peakHeight * (progress - 0.3) * 1/0.7 peakWidth = peakWidth * (progress - 0.3) * 1/0.7 } else { peakHeight = 0 peakWidth = 0 } // clear canvas ctx.reset() ctx.lineWidth = lineWidth; ctx.strokeStyle = control.pullIndicatorColor; // middle line ctx.moveTo(width/2, topMargin); ctx.lineTo(width/2, arrowHeight + topMargin); // right line ctx.moveTo(width/2 - lineWidth * 0.3, arrowHeight + topMargin); ctx.lineTo(width/2 + peakWidth,arrowHeight + topMargin - peakHeight); // left line ctx.moveTo(width/2 + lineWidth * 0.3, arrowHeight + topMargin); ctx.lineTo(width/2 - peakWidth,arrowHeight + topMargin - peakHeight); ctx.stroke(); } Behavior on rotation { NumberAnimation { duration: 100 } } } signal refreshRequested // internal properties property int minimumPosition: 0 property int maximumPosition: 0 property int userPosition: 0 property int position: Math.max( minimumPosition, Math.min(maximumPosition, userPosition)) height: 50 width: height Component.onCompleted: { if(control.autoConfigureTarget){ target.boundsBehavior = Flickable.DragOverBounds target.boundsMovement = Flickable.StopAtBounds } } function refresh() { control.refreshRequested() postRefreshTimer.start() } anchors { top: control.target.top horizontalCenter: control.target.horizontalCenter topMargin: control.position - height } Connections { target: control.target onDragEnded: { if(userPosition >= control.dragOutPosition * control.dragRefreshPositionMultiplier){ control.state = "refreshing" preRefreshTimer.start() } } } Loader { id: backgroundLd anchors.fill: parent sourceComponent: control.background } Loader { id: pullIndicatorLd anchors.centerIn: parent height: parent.height * 0.6 width: height rotation: 180 sourceComponent: control.pullIndicator } Loader { id: busyIndicatorLd anchors.centerIn: parent height: parent.height * 0.7 width: height opacity: 0 sourceComponent: control.busyIndicator } Timer { id: preRefreshTimer interval: control.preRefreshDelay <= 0 ? 1:control.preRefreshDelay running: false repeat: false onTriggered: { control.refresh() } } Timer { id: postRefreshTimer interval: control.postRefreshDelay <= 0 ? 1:control.postRefreshDelay running: false repeat: false onTriggered: { control.state = "hidden" } } Behavior on minimumPosition { enabled: !control.target.dragging && state !== "idle" NumberAnimation { duration: 100 } } states: [ State { name: "idle" PropertyChanges { target: control minimumPosition: userPosition > maximumPosition ? maximumPosition:userPosition userPosition: -1 / (Math.abs( target.verticalOvershoot * 0.001 + 0.003 ) + 1 / control.dragOutPosition * 0.001) + control.dragOutPosition // Math.abs( target.verticalOvershoot ) maximumPosition: control.dragOutPosition } PropertyChanges { target: pullIndicatorLd rotation: 0 } }, State { name: "refreshing" PropertyChanges { target: control minimumPosition: control.refreshPosition userPosition: 0 maximumPosition: control.refreshPosition } PropertyChanges { target: pullIndicatorLd opacity: 0 } PropertyChanges { target: busyIndicatorLd opacity: 1 } }, State { name: "hidden" PropertyChanges { target: control minimumPosition: control.refreshPosition userPosition: 0 maximumPosition: control.refreshPosition scale: 0 } PropertyChanges { target: pullIndicatorLd opacity: 0 } PropertyChanges { target: busyIndicatorLd opacity: 1 } } ] transitions: [ Transition { NumberAnimation { duration: 100 properties: "rotation, opacity" } }, Transition { from: "refreshing" to: "hidden" PauseAnimation { duration: 200 } NumberAnimation { duration: 200 properties: "scale" } onRunningChanged: { if(control.state === "hidden" && !running){ control.state = "idle" } } }, Transition { from: "hidden" to: "idle" } ] }