Slideshow style item navigation with zooming in of current item
-
This is a followup to an earlier (solved) question of mine. Since then, the requirements have changed somewhat.
Now we need to create a user interface where items are arranged as a horizontal list at the top. It must be possible to scroll through them and select one as the current item. The current item should then move out of the list and be maximized in the middle, similar to what the slideshow.qml example in examples/quick/views/visualdatamodel/ does.
The horizontal list I can do with a ListView. It is the current item zooming that is a problem. I could "steal" that item from the ListView and reparent it to the underlying item. But the problem is that later I might select a different item, so the current item should move back in at the same location in the list it was earlier, so it has to remember its previous place somehow. I am not sure if ListView can handle that.
I was thinking of solving this by placing each item in a "container" item. So, the ListView delegate is then this container item, with the actual visible item inside. When maximizing the current item, I reparent the visible item, and the container stays in the ListView. To make it seem as if the item was moved out, I could then also set the width of the container item to 0.
Does this sound reasonable, or is there a simpler way? slideshow.qml does not use a ListView, this is why I am asking. Also, would a container item width of 0 potentially cause problems?
Also please note that the visible items are a custom item type that must not be created more often than necessary. This is because it does some custom OpenGL painting (similar to what the openglunderqml example does) to render objects that have a texture on them that is filled with video frames. (This is done with custom GStreamer code, not with QtMultimedia.) More importantly, this has to be able to run on embedded i.MX6 hardware, so it is crucial to not have more open video streams than necessary, so one data model item must not be represented by more than one delegate. This is why I prefer to shift around existing items instead of creating new ones.
-
You might try a
Tumbler
but horizontal.Here is a working example:
import QtQuick 2.9 import QtQuick.Controls 2.2 Tumbler { id: tumbler property real factor: 15 SystemPalette { id: activPal; colorGroup: SystemPalette.Active } width: 800 //parent.width height: factor * 10 visibleItemCount: Math.min(((width / (factor * 7)) / 2) * 2 - 1, 7) model: 15 delegate: Component { Column { spacing: factor / 4 opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) scale: 1.7 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) Text { font { pixelSize: factor * 3 } text: modelData + 1 anchors.horizontalCenter: parent.horizontalCenter color: tumbler.currentIndex === modelData ? activPal.highlightedText : activPal.text } Text { anchors.horizontalCenter: parent.horizontalCenter width: factor * 8 text: modelData * 1008 horizontalAlignment: Text.AlignHCenter color: activPal.text font { bold: tumbler.currentIndex === modelData; pixelSize: factor * 0.8 } } } } contentItem: PathView { id: pathView model: tumbler.model delegate: tumbler.delegate clip: true pathItemCount: tumbler.visibleItemCount + 1 preferredHighlightBegin: 0.5 preferredHighlightEnd: 0.5 dragMargin: width / 2 path: Path { startX: 0 startY: factor * 1.4 PathLine { x: pathView.width y: factor * 1.4 } } } Rectangle { z: -1; width: factor * 9; height: parent.height * 0.5 x: parent.width / 2 - width / 2; y: 2 color: activPal.highlight radius: width / 12 } }
I plucked it from my code, so there can be more stuff than necessary and a bit messy, but You may see 'in action' is this approach suits You.
-
@SeeLook Wow, this is perfect! I did not know the Tumbler exists. I'm still a beginner at QML. I just have to arrange the items a bit to be more vertically centered, but otherwise this is even better than my idea, because the selected element does not have to be zoomed in! (I guess I could still do that if I want a 100% maximized mode, but I can live without it for now, I think the tumbler will suffice.) Thanks!
EDIT: To make it even better, it would be good if I could click on an item and the tumbler would scroll to it, as an addition to the dragging motion. I figure that I'd have to add a custom onClick handler to the contentItem that sets accepted to false, and then, in a MouseArea in the delegate, I'd have to set the Tumbler's current index to the index of the clicked element?
-
@dv__
I tried to add 'custom' click but the tumbler stopped working then.
But I didn't put much efforts to search why. Scrolling only is sufficient for me for now.
... but if You will find how to make it either scrollable and clickable, please write. -
@SeeLook I got it to work like this. However, I'm now not so sure if the Tumbler is really the way to go. What we are doing here is essentially bending the Tumbler . The actual items are still "vertical", just in the same row. The child items from these items are repositioned, and they are what we see. However, to really zoom in the current video, the adjacent items would have to be scaled down pretty hard so they don't consume too much width. Probably doable by adjusting the visibleItemCount formula, but so far I ended up with cases where the items overlap, and I can't afford that, because the 3D objects in my custom quickitems are being drawn directly with GL..
Window { id: window visible: true width: 800 height: 600 property var itemWidth: 200 property var itemHeight: 200 ListModel { id: nameModel ListElement { name: "Alice" } ListElement { name: "Bob" } ListElement { name: "Jane" } ListElement { name: "Peter" } ListElement { name: "James" } ListElement { name: "A" } ListElement { name: "B" } ListElement { name: "C" } ListElement { name: "D" } } Component { id: itemDelegate Item { opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) scale: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) * 0.7 Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter width: window.itemWidth height: window.itemHeight border.color: "red" border.width: 5 color: "green" Text { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter text: name } MouseArea { anchors.fill: parent onClicked: { console.log("New current index: " + index); tumbler.currentIndex = index } } } } } Tumbler { id: tumbler anchors.fill: parent model: nameModel visibleItemCount: Math.floor(width / window.itemWidth - 1) | 1 // bitwise OR to make sure the item count is always odd and at least 1 delegate: itemDelegate contentItem: PathView { model: tumbler.model delegate: tumbler.delegate clip: true pathItemCount: tumbler.visibleItemCount preferredHighlightBegin: 0.5 preferredHighlightEnd: 0.5 dragMargin: width / 2 path: Path { startX: 0 startY: tumbler.height / 2 PathLine { x: tumbler.width y: tumbler.height / 2 } } } } }
-
And here is a third version. This one allows for maximizing the current item. To make sure it isn't maximized while the tumbler's pathview is moving, I temporarily create a connection to onMovementEnded and tear it down once I maximized.
Unclear yet:
- disconnect() a non-connected function - is this ok?
- On touchscreens, does onMovementEnded() trigger when the items really don't move anymore? The documentation makes it sound as if it is triggered when the user no longer flicks the view, that is, releases the finger - but the items are still moving then.
- What if I delete the current item? What if it is currently maximized?
import QtQuick 2.0 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.3 import QtQuick.Window 2.0 Window { id: window visible: true width: 800 height: 600 property var itemWidth: 200 property var itemHeight: 200 ListModel { id: nameModel ListElement { name: "Alice" } ListElement { name: "Bob" } ListElement { name: "Jane" } ListElement { name: "Peter" } ListElement { name: "James" } ListElement { name: "A" } ListElement { name: "B" } ListElement { name: "C" } ListElement { name: "D" } } Component { id: itemDelegate Item { opacity: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) scale: 1.0 - Math.abs(Tumbler.displacement) / (Tumbler.tumbler.visibleItemCount / 2) * 0.7 z: (tumbler.model.count - Math.abs(index - tumbler.currentIndex)) / tumbler.model.count property alias maximized: itemDelegateRect.maximized property alias innerWidth: itemDelegateRect.width property alias innerHeight: itemDelegateRect.height Rectangle { id: itemDelegateRect anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter property bool maximized: false width: window.itemWidth height: window.itemHeight border.color: "red" border.width: 5 color: "green" Text { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter text: name } MouseArea { anchors.fill: parent onClicked: { console.log("New current index: " + index); tumbler.currentIndex = index } } Behavior on width { NumberAnimation { duration: 300; easing.type: Easing.InOutCubic } } Behavior on height { NumberAnimation { duration: 300; easing.type: Easing.InOutCubic } } } } } function toggleMaximize() { var item = tumbler.currentItem; if (item.maximized) { item.innerWidth = Qt.binding(function() { return window.itemWidth; }); item.innerHeight = Qt.binding(function() { return window.itemHeight; }); tumbler.contentItem.interactive = true; } else { item.innerWidth = Qt.binding(function() { return tumbler.width; }); item.innerHeight = Qt.binding(function() { return tumbler.height; }); tumbler.contentItem.interactive = false; } item.maximized = !(item.maximized); tumbler.contentItem.onMovementEnded.disconnect(toggleMaximize); } ColumnLayout { anchors.fill: parent Tumbler { id: tumbler model: nameModel visibleItemCount: Math.floor(width / window.itemWidth - 1) | 1 delegate: itemDelegate contentItem: PathView { model: tumbler.model delegate: tumbler.delegate clip: true pathItemCount: tumbler.visibleItemCount preferredHighlightBegin: 0.5 preferredHighlightEnd: 0.5 dragMargin: width / 2 interactive: true snapMode: PathView.SnapToItem path: Path { startX: 0 startY: tumbler.height / 2 PathLine { x: tumbler.width y: tumbler.height / 2 } } } Layout.fillWidth: true Layout.fillHeight: true } Button { id: maximizeButton text: "Toggle maximize" Layout.fillWidth: true Layout.fillHeight: false onClicked: { if (tumbler.contentItem.moving) tumbler.contentItem.onMovementEnded.connect(toggleMaximize); else toggleMaximize(); } } } }
-
@dv__ Thanks for sharing your experience. My 'bended' Tumbler also reacts for clicks now.
BTW, I think all QML controls are 'meant to be bend' , so as far as one doesn't break - it is allowed.In 'our' examples the crucial part is the
PathView
which does all tricks. It just uses model and delegate from the tumbler.I noticed that also in Your example hitting 'toggle maximize' button without touching tumbler before, causes maximizing wrong item (not this one in the middle). My workaround is to use a timer as follow:
Timer { // workaround to properly select 0 item, call it with delay running: true interval: 50 onTriggered: tumbler.currentIndex = 0 }
-
Hmm good catch!
Although it turns out that I can't flick the list, because the items require some custom mouse move handling ... which reduces it to a non-interactive PathView. So, solved I guess! Thanks for pointing out the Tumbler though, and I hope my examples prove to be useful to others.