如何在QML中设计一个expandable ListView

在前面的文章“如何在QML中使用ListView并导航到其它页面中”中,我们已经介绍了各种在ListView中导航到其它页面的方法。在这篇文章中,我来介绍如何建立一个expandable的ListView。通过这样的方法,ListView可以不用导航到其它的页面中,但是它可以通过状态的控制占据整个页面,而得到显示。


首先我们可以使用Ubuntu SDK来创建一个最简单的“QML App with Simple UI (qmlproject)”项目。我们的Main.qml非常简单:

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 {}
        }
    }
}


就像上面的代码显示的那样,我们需要一个model。为此,我们创建了如下的RecipesModel.qml文件:

RecipesModel.qml


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>"
    }
}



在这里,我们可以看到在文字中,我们可以使用 html格式来格式化我们的文字。这对我们多样的显示是非常有用的。


我们最关键的设计在于RecipesDelegate.qml文件:

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]
}


在这个delegate里,它有两个状态:

  • 默认的List模式。在这种模式下,它只显示一个图片及title
  • 详细模式。在这种模式下,除了显示上面的图片和title以外,还显示model中的ingredients及method
在详细模式下的状态为:

       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;
                }
            }

可以帮我们把当前的项移到ListView的窗口中。

我们运行我们的应用:

     

在上面的第二个图中,点击“Close”按钮,就可以回到List模式。

整个项目的代码在: https://github.com/liu-xiao-guo/expandinglist