ListViews...within a ListView

I've been trying to figure out how to put ListViews within a ListView for certain use-cases that warrant the UI to be able to swipe through multple screens, either horizontally or vertically.  While a ListView on it's own is usefuly for showing various different items in a nice layout, there are many instances where it may be benificial to incorporate another ListView within a ListView.  I personally incorporated into my BetaZone app, Kloudmix, which has a different UI for Keyboard devices and portrait devices for userpages and profiles.  The main use-case that most will probably be familiar with is the featured carousel, where a screen has a vertical listview with one item being a horizontal listview that let's the user swipe through various featured items.  Another use-case may be user profiles, allowing users to swipe through different panes or containers of information.  Long story short, I spent some time tweaking around with it and reading a support forum post to create this nice and simple tutorial and sample app for you!

Before we get started here are the files we will be making use of:

  • main.qml (each device resolution's folder)

Screenshot

 

 

 

main.qml

Here is where we setup the initial Listview that will provide the general direction you want users to interact with (i.e. will you be swiping horizontally for different listviews or panes, or will you be swiping vertically between the different panes?). You can adjust this by altering the stacklistlayout orientation to TopToBottom instead of LeftToRight. Also notice the dataModel in the ListView is an ArrayDataModel, which will allow us to append a certain amount of items to it which will later allow us to determine which lists and containers show up in each of those appended items **One note of caution first.  You MUST specify the width of each listItemComponent depending on the device and resolution so that each of the items you swipe to turn out ok on the screen, you cannot simply use infinity or horizontal fill to fill the screen since these are ListItemComponents**.

 
NavigationPane {
    id: nav
    Page {
        Container {
            ListView {
                dataModel: ArrayDataModel {
                    id: dm
                }
                layout: StackListLayout {
                    orientation: LayoutOrientation.LeftToRight
                }
                scrollIndicatorMode: ScrollIndicatorMode.None
                id: lister
                flickMode: FlickMode.SingleItem
            }
        }

    }
    attachedObjects: [
        ComponentDefinition {
            id: itemPageDefinition
            source: "ItemPage.qml"
        }
    ]
    onPopTransitionEnded: {
        page.destroy();
    }
}

Next we added an onCreationCompleted to the main NavigationPane that will append three items to our ListView. Once these items are appended we will setup a function that places one of three ListItemComponents that are found within our ListView into each appended item depending on the items position in the arrayDataModel. For this sample and for the sake of simplicity we only have three items and will place each of our three ListItemComponents based on whichever item is in slot "0", "1", and "2" of the arryDataModel:

 
NavigationPane {
    id: nav
    Page {
        Container {
            ListView {
                dataModel: ArrayDataModel {
                    id: dm
                }
                function getNav(){
                    return nav
                }
                scrollIndicatorMode: ScrollIndicatorMode.None
                id: lister
                flickMode: FlickMode.SingleItem
                listItemComponents: [
                    ListItemComponent {
                        type: "first"
                    },
                    ListItemComponent {
                        type: "second"
                    },
                    ListItemComponent {
                        type: "third"
                    }
                ]
                function itemType(data, indexPath) {
                    if (indexPath == 0) {
                        return 'first'
                    } else if (indexPath == 1) {
                        return 'second'
                    } else {
                        return 'third'
                    }
                }
            }
        }

    }
    attachedObjects: [
        ComponentDefinition {
            id: itemPageDefinition
            source: "ItemPage.qml"
        }
    ]
    onPopTransitionEnded: {
        page.destroy();
    }
    onCreationCompleted: {
        dm.append([ "one", "two", "three" ])
    }
}

Now that we've setup our main ListView to select the various ListItemComponents based on the appended items and their position, we can adjust our ListItemComponents and add the ListViews where we want them. Keep in mind not every ListItemComponent needs to have a listView so you can play around with it. For this sample I made the first ListItemComponent just a simple container with a Label, and the other two ListItemComponents have ListViews:

 
NavigationPane {
    id: nav
    Page {
        Container {
            ListView {
                dataModel: ArrayDataModel {
                    id: dm
                }
                layout: StackListLayout {
                    orientation: LayoutOrientation.LeftToRight
                }
                scrollIndicatorMode: ScrollIndicatorMode.None
                id: lister
                flickMode: FlickMode.SingleItem
                listItemComponents: [
                    ListItemComponent {
                        type: "first"
                        Container {
                            layout: DockLayout {

                            }
                            preferredHeight: 1280
                            preferredWidth: 768
                            leftPadding: 10
                            topPadding: leftPadding
                            bottomPadding: leftPadding
                            rightPadding: leftPadding
                            Label {
                                verticalAlignment: VerticalAlignment.Center
                                horizontalAlignment: HorizontalAlignment.Center
                                multiline: true
                                text: "This is the first Item!...Pretty lonely :( so move over to continue ----->"
                            }
                        }

                    },
                    ListItemComponent {
                        type: "second"
                        Container {
                            layout: DockLayout {

                            }
                            preferredHeight: 1280
                            preferredWidth: 768
                            topPadding: 10
                            id: second
                            ListView {
                                dataModel: XmlDataModel {
                                    source: "data.xml"
                                }
                            }
                        }
                    },
                    ListItemComponent {
                        type: "third"
                        Container {
                            layout: DockLayout {

                            }
                            preferredHeight: 1280
                            preferredWidth: 768
                            topPadding: 10
                            id: third
                            ListView {
                                dataModel: XmlDataModel {
                                    source: "datatwo.xml"
                                }
                            }
                        }
                    }
                ]
                function itemType(data, indexPath) {
                    if (indexPath == 0) {
                        return 'first'
                    } else if (indexPath == 1) {
                        return 'second'
                    } else {
                        return 'third'
                    }
                }
            }
        }

    }
    attachedObjects: [
        ComponentDefinition {
            id: itemPageDefinition
            source: "ItemPage.qml"
        }
    ]
    onPopTransitionEnded: {
        page.destroy();
    }
    onCreationCompleted: {
        dm.append([ "one", "two", "three" ])
    }
}

Finally, we want to be able to interact with our added ListViews so that something happens when a user clicks on an item. Because of how ListViews are in Cascades it requires a bit more effort than simply referencing an ID and pushing a page, you must setup a function at the top of the main ListView and access items outside of the ListView through this:

 
NavigationPane {
    id: nav
    Page {
        Container {
            ListView {
                dataModel: ArrayDataModel {
                    id: dm
                }
                layout: StackListLayout {
                    orientation: LayoutOrientation.LeftToRight
                }
                function getItemPage(){
                    return itemPageDefinition
                }
                function getNav(){
                    return nav
                }
                scrollIndicatorMode: ScrollIndicatorMode.None
                id: lister
                flickMode: FlickMode.SingleItem
                listItemComponents: [
                    ListItemComponent {
                        type: "first"
                        Container {
                            layout: DockLayout {

                            }
                            preferredHeight: 1280
                            preferredWidth: 768
                            leftPadding: 10
                            topPadding: leftPadding
                            bottomPadding: leftPadding
                            rightPadding: leftPadding
                            Label {
                                verticalAlignment: VerticalAlignment.Center
                                horizontalAlignment: HorizontalAlignment.Center
                                multiline: true
                                text: "This is the first Item!...Pretty lonely :( so move over to continue ----->"
                            }
                        }

                    },
                    ListItemComponent {
                        type: "second"
                        Container {
                            layout: DockLayout {

                            }
                            preferredHeight: 1280
                            preferredWidth: 768
                            topPadding: 10
                            id: second
                            ListView {
                                dataModel: XmlDataModel {
                                    source: "data.xml"
                                }
                                onTriggered: {

                                    if (indexPath.length > 1) {
                                        var chosenItem = dataModel.data(indexPath);
                                        var contentpage = second.ListItem.view.getItemPage().createObject();
                                        
                                        contentpage.itemPageTitle = chosenItem.name
                                        second.ListItem.view.getNav().push(contentpage);
                                    }
                                }
                            }
                        }
                    },
                    ListItemComponent {
                        type: "third"
                        Container {
                            layout: DockLayout {

                            }
                            preferredHeight: 1280
                            preferredWidth: 768
                            topPadding: 10
                            id: third
                            ListView {
                                dataModel: XmlDataModel {
                                    source: "datatwo.xml"
                                }
                                onTriggered: {

                                    if (indexPath.length > 1) {
                                        var chosenItem = dataModel.data(indexPath);
                                        var contentpage = third.ListItem.view.getItemPage().createObject();

                                        contentpage.itemPageTitle = chosenItem.name
                                        third.ListItem.view.getNav().push(contentpage);
                                    }
                                }
                            }
                        }
                    }
                ]
                function itemType(data, indexPath) {
                    if (indexPath == 0) {
                        return 'first'
                    } else if (indexPath == 1) {
                        return 'second'
                    } else {
                        return 'third'
                    }
                }
            }
        }

    }
    attachedObjects: [
        ComponentDefinition {
            id: itemPageDefinition
            source: "ItemPage.qml"
        }
    ]
    onPopTransitionEnded: {
        page.destroy();
    }
    onCreationCompleted: {
        dm.append([ "one", "two", "three" ])
    }
}

Now whenever a user clicks on a page the getNav() and getItemPage() functions get called and reference the itemPageDefinition and the NavigationPane for pushing.

This is a really simple example of using ListViews within a ListView and is open to getting adjusted to your liking. The associated sample app source code and bar can be found below.

Brandon Orr

Transportation Planner (University of Waterloo Graduate) & Blackberry 10 Developer (PinGuin App) part of the open source BB team.

Website: appworld.blackberry.com/webstore/vendor/65375/
More in this category: « Simple Active Frames

Leave your comments

Post comment as a guest

0

People in this conversation

  • Guest (Dau Duc)

    Vietnam

    Thanks Brandon Orr. Very useful article.:p

    Like 0 Short URL:
  • Jakarta, Jakarta, Indonesia

    Hi Brandon Orr. How do push ItemPage1.qml from Item 1-1, push ItemPage2.qml from Item 1-2, ItemPage3.qml from Item 1-3,........??? Thanks:p

    Like 0 Short URL:
  • @chandatmadixehoi Depends on what you want to do, if you simply just want a similar page to show information for each item you would use a property to show the values you want represented in the pushed page and it would just show the information for each item as you tap them. If you want a specific page to be pushed depending on which item is clicked you need to use an IF statement to distinguish the items based on IndexPath of the listView or based on some other identifier so that you can tell the app to push another page other than ItemPage.qml

    Like 0 Short URL:
  • Jakarta, Jakarta, Indonesia

    I still do not understand. You can guide more detailed okay? I need examples. Thank you

    Like 0 Short URL:
  • Guest (Tom)

    How would you handle an orientation change on devices like the Z10 and Z30? With the code sample above, the preferred width and height don't adjust to the new screen orientation

    Like 0 Short URL:
  • @Tom I'm still trying to figure out how to account for rotation in apps. I'll let you know when I figure it out!

    Like 0 Short URL:
  • import bb.cascades 1.0

    // A Custom Title bar with a differnt look then the prepackaged one.
    TitleBar {
    id: customTitleBar
    property alias barTitle: titleLabel.text
    property int layoutHeight: 0
    property int layoutWidth: 0

    kind: TitleBarKind.FreeForm

    // This is a custom title bar so we put the content (a text)
    // and an image) in a FreeFormTitleBarKindProperties.
    kindProperties: FreeFormTitleBarKindProperties {
    Container {
    leftPadding: 25

    background: titlePaint.imagePaint
    attachedObjects: [
    ImagePaintDefinition {
    id: titlePaint
    imageSource: "asset:///images/custom_title.png"
    },
    LayoutUpdateHandler {
    id: layoutHandler
    onLayoutFrameChanged: {
    // We store the height and width of the title bar as it is layouted.
    customTitleBar.layoutHeight = layoutFrame.height;
    customTitleBar.layoutWidth = layoutFrame.width;
    }
    }
    ]

    layout: StackLayout {
    orientation: LayoutOrientation.LeftToRight
    }

    Label {
    id: titleLabel
    verticalAlignment: VerticalAlignment.Center

    textStyle {
    color: Color.White
    base: SystemDefaults.TextStyles.TitleText
    fontWeight: FontWeight.W500
    }

    layoutProperties: StackLayoutProperties {
    spaceQuota: 1
    }
    }

    }
    }
    }

    Like 0 Short URL:
Subscribe to the official OSBB BBM Channel!

osbbchannelQR

C00013E89

Back to top