I have a SwipeView with a number of pages. It's designed for a small round screen, 2.1" in diameter, with the idea being swipe left or right between pages, and each page has one or a few UI elements to interact with. Many of those pages contain a Dial and little/nothing else. Maybe a button at the bottom center.
With touch (the intended input device), a horizontal drag within the Dial is most often interpreted as navigating the SwipeView underneath. Very rarely does it recognize that I wish to adjust the Dial.
With a mouse, the Dial seems to pretty reliably grab the drag gesture so that even a horizontal drag (e.g. from the top or bottom of the dial) does not inadvertently navigate between pages in the SwipeView.
See this video for a comparison of the touch behavior vs. the mouse behavior (I wasn't able to upload it directly in the StackOverflow editor, so I hope the link to imgur is acceptable)
Is there a way to give the Dial higher priority over interpreting touch/swipe gestures so that the user is not likely to accidentally swipe between pages? (More like the mouse behavior)
I can work around it by starting the gesture vertically and then moving to the desired position, but I would prefer not to have to tell my user that, and that seems to be their most frequent hangup in this app.
The dials are 70% of the size of the window itself so there's plenty of room outside of the dials for the user to swipe the SwipeView.
I have tried setting SwipeView interactive: !(dial1.pressed || dial2.pressed)
etc. but this does not work. It would seem the .pressed
property of the dial does not take effect until the dial definitively gets selected for handling the input. I'd prefer it to behave more like it does with the mouse, in this particular instance.
In considering a SwipeView that is purely interactive: false
--so that the Dial is guaranteed to pick up the gestures--I would have to add buttons to each screen to navigate between pages, and that will take away from the room I have for other UI elements. The Dial pages are the only ones giving me trouble. Other pages already have 1-3 buttons in places where there is little risk of a conflicting gesture. If I have to add a button to either side of the page, that starts to squish things down to a point where it becomes hard to hit on the intended screen size.
The only other alternative I can think of at the moment is an edit / done button (like a modal dialog?), but that adds some friction I'd rather avoid.
This code is the minimal reproducible example as seen in the video. I last tried in Qt 6.3.0 on Ubuntu 20.04 with a USB/HDMI touchscreen.
import QtQuick
import QtQuick.Controls.Basic
Window {
width: 480
height: 480
visible: true
title: "SwipeView Dial Test"
SwipeView {
anchors.fill: parent
Item {
Dial {
id: dial1
anchors.centerIn: parent
width: parent.width * 0.7
height: parent.height * 0.7
}
}
Item {
Dial {
id: dial2
anchors.centerIn: parent
width: parent.width * 0.7
height: parent.height * 0.7
}
}
}
}
It really feels like there's an issue with the Dial
's drag handler. It feels like not all of the control is covered by a drag handler so the events are being picked up by the SwipeView
at unexpected times.
I've created a workaround by patching Dial
with DialPatched.qml
.
In my implementation, I created a mock Rectangle
that sits over the Dial
. The mock rectangle will now receive drag events via its MouseArea
which has preventStealing: true
. We observe changes in mouse position and reverse engineer a value via Math.atan2()
to convert vectors into angles and compute a new value for the dial.
To see the mock Rectangle I colored it in a nearly transparent "red" so you can see both the Rectangle and the Dial underneath. If you're going to go with this approach, I recommend turning the opacity to 0 or swapping out the Rectangle with a placeholder Item.
Also, I did a minor correction to your width/height calculation.
import QtQuick
import QtQuick.Controls
Page {
SwipeView {
anchors.fill: parent
Item {
DialPatched {
id: dial1
anchors.centerIn: parent
debug: debugCheckBox.checked
width: Math.min(parent.width,parent.height) * 0.7
height: width
}
}
Item {
DialPatched {
id: dial2
anchors.centerIn: parent
debug: debugCheckBox.checked
width: Math.min(parent.width,parent.height) * 0.7
height: width
}
}
}
CheckBox { id: debugCheckBox; text: "Debug"; checked: true }
}
// DialPatched.qml
import QtQuick
import QtQuick.Controls
Dial {
id: dial
property bool debug: true
Item {
anchors.fill: parent
Rectangle {
width: parent.width
height: parent.height
color: debug ? "red" : "transparent"
opacity: debug ? 0.05 : 0
property int startX: 0
property int startY: 0
onXChanged: Qt.callLater(mouseMove, startX+x, startY+y)
onYChanged: Qt.callLater(mouseMove, startX+x, startY+y)
MouseArea {
anchors.fill: parent
drag.target: parent
preventStealing: true
onPressed: function (mouse) {
parent.startX = mouse.x;
parent.startY = mouse.y;
parent.Drag.active = true;
}
onReleased: {
parent.Drag.active = false;
parent.x = 0;
parent.y = 0;
}
}
function mouseMove(mx,my) {
if (Drag.active) {
let ang = Math.atan2(height/2 - my, mx - width/2) * 180 / Math.PI;
ang = (360 + 270 - Math.round(ang)) % 360;
dial.value = (ang - 40) / 280 * (dial.to - dial.from) + dial.from;
}
}
}
}
}
You can Try it Online!