如何在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
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]
}

在这里,我们可以看到在文字中,我们可以使用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模式。

整个项目的代码在:git clone https://gitcafe.com/ubuntu/expandinglist.git

时间: 2024-07-29 04:35:32

如何在QML中设计一个expandable ListView的相关文章

如何在QML应用中启动Scope

在这篇文章中,我们将介绍如何在QML应用中调用Scope,并把搜索的关键词传进去.这对有些QML应用需要用到Scope的情况非常有用.更多关于url-dispatcher的知识,请在文章"使用URL dispatcher的范例"看到. Scope ID 首先我们来讲一下什么是Scope ID.我们打开我们创建的任何一个Scope的manifest.json文件: { "architecture": "@[email protected]", &q

如何在html添加一个搜索框和一个按钮?

<INPUT TYPE="text" id="k"><INPUT TYPE="button" VALUE="ok" ONCLICK="xx()">   <SCRIPT LANGUAGE="JavaScript">   <!--   function xx(){ var k=document.getElementById("k"

如何在QML中使用ListView并导航到其它页面中

我们知道ListView在QML应用中扮演非常重要的角色.看看我们的很多的应用都是在使用ListView.那么当我们点击ListView中的item并导航到另外一个页面呢?其实这样的方法有很多.在这篇文章中,我们来介绍其中的几种.开发者可以参照其中的设计,或自己想出更好的设计. 1)使用PageStack来完成 在我们的RssReader中的例子中,我们使用了PageStack来完成我们的导航.我们可以把我们的每个页面都做成我们的Page.当我们的页面被点击后,我们把新的Page压入栈中.在返回

如何在QML应用中实现一个Splash画面

在QML应用中,我们经常要用到一个SplashScreen的画面来渲染我们的应用.那么我们怎么在自己的应用中做一个Splash Screen呢? 首先我们来设计一个自己的SplashScreen的QML模块: SplashScreen.qml import QtQuick 2.0 Item { id: splash anchors.fill: parent property int timeoutInterval: 2000 signal timeout Image { id: splashIm

如何在QML应用中得到一个Item的所有属性,信号及方法

Item是QML语言中最基本的元素.有时为了方便,我们可以列出它里面的所有的属性,信号及方法.我们可以通过这个方法来修改我们的属性等.在QML语言中,所有的可视的控件都是继承于Item的. 下面我们来通过一个例子来展示如何这么做.我们可以设计一个简单的QML应用如下: import QtQuick 2.0 import Ubuntu.Components 1.1 /*! \brief MainView with a Label and Button elements. */ MainView {

如何在QML应用中设计自己的Dialog

对话框Dialog的设计在许多的QML应用是经常用到的.许多新的开发者刚开始接触QML,有时找不到头绪.也许是由于QML的设计太过灵活,所以实现的方法有非常多.这里介绍几种简单的方式. 1)使用Ubuntu SDK提供的标准API 我们可以使用Ubuntu SDK提供的标准Dialog接口.使用的方法非常简单: import QtQuick 2.4 import Ubuntu.Components 1.2 import Ubuntu.Components.Popups 1.0 Item { wi

如何在QML应用中创建一个Context Menu

我们在很多的系统中看见可以在屏幕的一个地方长按,然后就可以根据当前显示的上下文弹出一个菜单.菜单中可以有一些选项,比如删除,修改该项.这种一般在ListView或GridView中常见.今天,我们就在这个例程中详细介绍如何实现这个功能. 对ListView来说,我们只需要对它的delegate做一些修改: Component { id: listDelegate ListItem { id: delegateItem width: listView.width; height: units.gu

如何在QML应用中使用Javascript解析JSON

很多QML应需要访问web services.我们可以通过Javascript的方法来解析得到我们所需要的JSON数据,并把它们展示出来.在今天的例子中,我们将展示如何实现它? 我们可以创建一个最基本的"QML App with Simple UI (qmlproject)",并取名我们的应用为"baidutranslator".我们将使用的API为: http://openapi.baidu.com/public/2.0/bmt/translate?client_

如何在QML应用中动态修改ListModel中的数据并存储它为JSON格式

我们知道JSON数据格式被广泛使用在很多的应用中,它可以帮我们保存我们应用的设置数据等.在QML中的ListView或GridView中,我们使用ListModel来显示它里面的数据.这个数据可以来源于xml或JSON.在ListView或GridView中,我们也可以动态修改ListModel中的数据.那么我们将如何保存这个数据呢?本篇文章也同样适用于xml格式的保存.这个练习就留个开发者自己了.当然保存ListModel中的数据也可以使用到SQLite数据库.这个因人而已! 为了方便我们的设