在前面的文章“如何在QML中使用ListView并导航到其它页面中”中,我们已经介绍了各种在ListView中导航到其它页面的方法。在这篇文章中,我来介绍如何建立一个expandable的ListView。通过这样的方法,ListView可以不用导航到其它的页面中,但是它可以通过状态的控制占据整个页面,而得到显示。
首先我们可以使用Ubuntu SDK来创建一个最简单的“QML App with Simple UI (qmlproject)”项目。我们的Main.qml非常简单:
import QtQuick 2.4 import Ubuntu.Components 1.2 /*! \brief MainView with a Label and Button elements. */ MainView { // objectName for functional testing purposes (autopilot-qt5) objectName: "mainView" // Note! applicationName needs to match the "name" field of the click manifest applicationName: "expandinglist.liu-xiao-guo" /* This property enables the application to change orientation when the device is rotated. The default is false. */ //automaticOrientation: true // Removes the old toolbar and enables new features of the new header. // useDeprecatedToolbar: false width: units.gu(60) height: units.gu(85) Page { id: mainpage title: i18n.tr("expandinglist") flickable: null ListView { id: listView anchors.fill: parent clip: true model: RecipesModel {} delegate: RecipesDelegate {} } } }
import QtQuick 2.0 ListModel { ListElement { title: "Pancakes" picture: "content/pics/pancakes.jpg" ingredients: "<html> <ul> <li> 1 cup (150g) self-raising flour <li> 1 tbs caster sugar <li> 3/4 cup (185ml) milk <li> 1 egg </ul> </html>" method: "<html> <ol> <li> Sift flour and sugar together into a bowl. Add a pinch of salt. <li> Beat milk and egg together, then add to dry ingredients. Beat until smooth. <li> Pour mixture into a pan on medium heat and cook until bubbles appear on the surface. <li> Turn over and cook other side until golden. </ol> </html>" } ListElement { title: "Fruit Salad" picture: "content/pics/fruit-salad.jpg" ingredients: "* Seasonal Fruit" method: "* Chop fruit and place in a bowl." } ListElement { title: "Vegetable Soup" picture: "content/pics/vegetable-soup.jpg" ingredients: "<html> <ul> <li> 1 onion <li> 1 turnip <li> 1 potato <li> 1 carrot <li> 1 head of celery <li> 1 1/2 litres of water </ul> </html>" method: "<html> <ol> <li> Chop vegetables. <li> Boil in water until vegetables soften. <li> Season with salt and pepper to taste. </ol> </html>" } ListElement { title: "Hamburger" picture: "content/pics/hamburger.jpg" ingredients: "<html> <ul> <li> 500g minced beef <li> Seasoning <li> lettuce, tomato, onion, cheese <li> 1 hamburger bun for each burger </ul> </html>" method: "<html> <ol> <li> Mix the beef, together with seasoning, in a food processor. <li> Shape the beef into burgers. <li> Grill the burgers for about 5 mins on each side (until cooked through) <li> Serve each burger on a bun with ketchup, cheese, lettuce, tomato and onion. </ol> </html>" } ListElement { title: "Lemonade" picture: "content/pics/lemonade.jpg" ingredients: "<html> <ul> <li> 1 cup Lemon Juice <li> 1 cup Sugar <li> 6 Cups of Water (2 cups warm water, 4 cups cold water) </ul> </html>" method: "<html> <ol> <li> Pour 2 cups of warm water into a pitcher and stir in sugar until it dissolves. <li> Pour in lemon juice, stir again, and add 4 cups of cold water. <li> Chill or serve over ice cubes. </ol> </html>" } }
我们最关键的设计在于RecipesDelegate.qml文件:
import QtQuick 2.0 import Ubuntu.Components 1.2 // Delegate for the recipes. This delegate has two modes: // 1. List mode (default), which just shows the picture and title of the recipe. // 2. Details mode, which also shows the ingredients and method. //Component { // id: recipeDelegate //! [0] Item { id: recipe // Create a property to contain the visibility of the details. // We can bind multiple element's opacity to this one property, // rather than having a "PropertyChanges" line for each element we // want to fade. property real detailsOpacity : 0 //! [0] width: ListView.view.width height: units.gu(10) // A simple rounded rectangle for the background Rectangle { id: background x: 2; y: 2; width: parent.width - x*2; height: parent.height - y*2 color: "ivory" border.color: "orange" radius: 5 } // This mouse region covers the entire delegate. // When clicked it changes mode to 'Details'. If we are already // in Details mode, then no change will happen. //! [1] MouseArea { anchors.fill: parent onClicked: { console.log("recipe.y: " + recipe.y ); console.log("origin.y: " + listView.originY ); recipe.state = 'Details'; } } // Lay out the page: picture, title and ingredients at the top, and method at the // bottom. Note that elements that should not be visible in the list // mode have their opacity set to recipe.detailsOpacity. Row { id: topLayout x: 10; y: 10; height: recipeImage.height; width: parent.width spacing: 10 Image { id: recipeImage width: units.gu(8); height: units.gu(8) source: picture } //! [1] Column { width: background.width - recipeImage.width - 20; height: recipeImage.height spacing: 5 Text { text: title font.bold: true; font.pointSize: units.gu(2) } SmallText { text: "Ingredients" font.bold: true opacity: recipe.detailsOpacity } SmallText { text: ingredients wrapMode: Text.WordWrap width: parent.width opacity: recipe.detailsOpacity } } } //! [2] Item { id: details x: 10; width: parent.width - 20 anchors { top: topLayout.bottom; topMargin: 10; bottom: parent.bottom; bottomMargin: 10 } opacity: recipe.detailsOpacity //! [2] SmallText { id: methodTitle anchors.top: parent.top text: "Method" font.pointSize: 12; font.bold: true } Flickable { id: flick width: parent.width anchors { top: methodTitle.bottom; bottom: parent.bottom } contentHeight: methodText.height clip: true Text { id: methodText; text: method; wrapMode: Text.WordWrap; width: details.width } } Image { anchors { right: flick.right; top: flick.top } source: "content/pics/moreUp.png" opacity: flick.atYBeginning ? 0 : 1 } Image { anchors { right: flick.right; bottom: flick.bottom } source: "content/pics/moreDown.png" opacity: flick.atYEnd ? 0 : 1 } //! [3] } // A button to close the detailed view, i.e. set the state back to default (''). TextButton { y: 10 anchors { right: background.right; rightMargin: 10 } opacity: recipe.detailsOpacity text: "Close" onClicked: recipe.state = ''; } states: State { name: "Details" PropertyChanges { target: background; color: "white" } PropertyChanges { target: recipeImage; width: 130; height: 130 } // Make picture bigger PropertyChanges { target: recipe; detailsOpacity: 1; x: 0 } // Make details visible PropertyChanges { target: recipe; height: listView.height } // Fill the entire list area with the detailed view // Move the list so that this item is at the top. PropertyChanges { target: recipe.ListView.view; explicit: true; contentY: { console.log("listView.contentY: " + listView.contentY); return recipe.y + listView.contentY; } } // Disallow flicking while we're in detailed view PropertyChanges { target: recipe.ListView.view; interactive: false } } transitions: Transition { // Make the state changes smooth ParallelAnimation { ColorAnimation { property: "color"; duration: 500 } NumberAnimation { duration: 300; properties: "detailsOpacity,x,contentY,height,width" } } } // } //! [3] }
states: State { name: "Details" PropertyChanges { target: background; color: "white" } PropertyChanges { target: recipeImage; width: 130; height: 130 } // Make picture bigger PropertyChanges { target: recipe; detailsOpacity: 1; x: 0 } // Make details visible PropertyChanges { target: recipe; height: listView.height } // Fill the entire list area with the detailed view // Move the list so that this item is at the top. PropertyChanges { target: recipe.ListView.view; explicit: true; contentY: { console.log("listView.contentY: " + listView.contentY); return recipe.y + listView.contentY; } } // Disallow flicking while we're in detailed view PropertyChanges { target: recipe.ListView.view; interactive: false } }
PropertyChanges { target: recipe.ListView.view; explicit: true; contentY: { console.log("listView.contentY: " + listView.contentY); return recipe.y + listView.contentY; } }