Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. QML and Qt Quick
  4. Slideshow style item navigation with zooming in of current item
QtWS25 Last Chance

Slideshow style item navigation with zooming in of current item

Scheduled Pinned Locked Moved Solved QML and Qt Quick
slideshowanimationparentimx6embedded
8 Posts 2 Posters 2.8k Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • D Offline
    D Offline
    dv__
    wrote on last edited by dv__
    #1

    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.

    1 Reply Last reply
    0
    • SeeLookS Offline
      SeeLookS Offline
      SeeLook
      wrote on last edited by SeeLook
      #2

      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.

      D 1 Reply Last reply
      0
      • SeeLookS SeeLook

        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.

        D Offline
        D Offline
        dv__
        wrote on last edited by dv__
        #3

        @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?

        SeeLookS 1 Reply Last reply
        0
        • D dv__

          @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?

          SeeLookS Offline
          SeeLookS Offline
          SeeLook
          wrote on last edited by
          #4

          @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.

          D 1 Reply Last reply
          0
          • SeeLookS SeeLook

            @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.

            D Offline
            D Offline
            dv__
            wrote on last edited by dv__
            #5

            @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
            				}
            			}
            		}
            	}
            }
            
            1 Reply Last reply
            0
            • D Offline
              D Offline
              dv__
              wrote on last edited by
              #6

              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:

              1. disconnect() a non-connected function - is this ok?
              2. 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.
              3. 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();
              			}
              		}
              	}
              }
              
              SeeLookS 1 Reply Last reply
              1
              • D dv__

                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:

                1. disconnect() a non-connected function - is this ok?
                2. 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.
                3. 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();
                			}
                		}
                	}
                }
                
                SeeLookS Offline
                SeeLookS Offline
                SeeLook
                wrote on last edited by
                #7

                @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
                  }
                
                1 Reply Last reply
                0
                • D Offline
                  D Offline
                  dv__
                  wrote on last edited by
                  #8

                  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.

                  1 Reply Last reply
                  0

                  • Login

                  • Login or register to search.
                  • First post
                    Last post
                  0
                  • Categories
                  • Recent
                  • Tags
                  • Popular
                  • Users
                  • Groups
                  • Search
                  • Get Qt Extensions
                  • Unsolved