Friday, January 4, 2013

Creating custom listview with BB10 Cascades API

As I indicated in my previous post, I am trying to port my Audiobook Reader application to BB10. I needed to make few changes to use BB10 Cascades API.

Previously I posted about how to create custom dialog box using Cascades QML API.

In this post I will talk about how to create custom list view.

For most case StandardListItem should be enough for use. Its looks like below.


But I wanted to learn creating custom component and also wanted to add some dynamic behavior to my list item.

I wanted to add Delete button on my list item. By default delete button is invisible, its get visible on long press event. It you tap on button the delete event is fired, touching anywhere else in item makes it invisible again. Also touching List item in normal state fire the selection event.

I was not sure how I can achieve this behavior with StandardListItem and I create my custom list item like below.

Normal condition, it looks like below.

On long press event on list item, it shows the delete button.

Now lets see how I created list item to satisfy above behavior.

First lets create list view, which uses the custom list item named ListDelegate and also have necessary function (selected and delete )to handle events from list item.
            ListView {
                id: listView
                dataModel: listModel.model
                listItemComponents: [
                    ListDelegate {
                    }
                ]

                //called by listdelegate manually
                function selected(index) {
                    console.log("###### Index selected:" + index);
                }

                //called by listdelegate manually
                function delete(index) {
                    console.log("###### Delete index:" + index);
                }
            }
Following is code for ListDelete component. I removed some code to keep code short and relevant.

First we need to set touchPropogationMode to Full on root container, so the all sub component get chance to handle their event. Then I added to sub container to main container to hold actual list item content and one to hold delete button and added gesture handler to both of them.

Unfortunately I was not able to find easy way to emit signal from ListItem to ListView, so I ended up calling ListView function manually to indicate select and delete event. By calling list view function
 root.ListItem.view.selected() and root.ListItem.view.delete()
.
To access current list items's index, we can use root.ListItem.indexPath property.
ListItemComponent {  
    
    Container{
        id: root
        layout: DockLayout {}
        touchPropagationMode: TouchPropagationMode.Full;
          
        Container{
            id: itemRoot
            layout: DockLayout {}
            preferredWidth: 768; preferredHeight: 120  
            
            gestureHandlers:[
                LongPressHandler {
                    onLongPressed: {
                        //make delete button visible
                        deleteBtn.opacity = 1;
                        itemRoot.opacity = 0.3 
                    }
                },
                TapHandler {
                    onTapped: {   
                        if( deleteBtn.opacity == 1 ) {   
                            //make delete button hide                
                            deleteBtn.opacity = 0;
                            itemRoot.opacity = 1; 
                        } else {
                            //fire selected event
                            root.ListItem.view.selected(root.ListItem.indexPath); 
                        }
                    }
                }
            ]  
                        
            Divider {
                verticalAlignment: VerticalAlignment.Bottom
                horizontalAlignment: HorizontalAlignment.Center  
            }
            
            Container {
                
                verticalAlignment: VerticalAlignment.Center
                layout:StackLayout {
                    orientation: LayoutOrientation.LeftToRight;                
                }
                                
                ImageView{
                    id: image
                    preferredHeight: 110; preferredWidth: 110
                    imageSource: ListItemData.image;
                    verticalAlignment: VerticalAlignment.Center
                }
                
                Container {             
                    layout:StackLayout {}
                    bottomPadding: 10
                    Label{
                        text: ListItemData.title;            
                    }
                    
                    Label{
                        text: ListItemData.subTitle;                            
                    } 
                }
            }
        
        } 
        
        Container {
            id: deleteBtn
            
            opacity: 0; preferredWidth: 150; preferredHeight: 100
            verticalAlignment: VerticalAlignment.Center
            horizontalAlignment: HorizontalAlignment.Right  
          
            ImageView{
                imageSource: "delete.png";
                verticalAlignment: VerticalAlignment.Center
                horizontalAlignment: HorizontalAlignment.Center
            } 
            
            gestureHandlers:[
                TapHandler {
                    onTapped: {                              
                        if( deleteBtn.opacity == 1 ) {                    
                            deleteBtn.opacity = 0;
                            itemRoot.opacity = 1;
                            root.ListItem.view.delete(root.ListItem.indexPath); 
                        } 
                    }
                }
            ]           
        }
    }
}
Thought it seems lot of code, its quite easy to create custom list item. Now lets see how we can provide data to ListView. For my case I created Data model in C++ and exported it to QML. You can export c++ Data model to QML using setContextProperty. listModel is instance of class derived from QObject.

  QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(&app);
  ListModel listModel;
 qml->setContextProperty("listModel", &listModel);
We are setting model to list view like dataModel: listModel.model, it means that listModel has property called model. Following is code from the same.

class ListModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bb::cascades::DataModel* model READ model);

public:
    void loadData();
    bb::cascades::DataModel* model(return mListModel;);
private:

    QMapListDataModel* mListModel;
};
And I am populating the ListModel like below. I am packaging my List ItemData inside QVariantMap, so that ListItem can easily access the data.

void ListModel::loadData()
{
    mListModel->clear();

    foreach( ... )
    {
  QVariantMap map;
 map["image"] = "image path";
 map["title"] = "title";
 map["subtitle"] = "subtitle";
 (*mListModel) << map;
   }
}
If you see ListItem's code, We can access the list item data like below.
                ImageView{
                    imageSource: ListItemData.image;
                }
                
                Container {             
                    Label{
                        text: ListItemData.title;
                    }            
                    Label{
                        text: ListItemData.subTitle;                            
                    } 
                }
Now finally its done. I hope it will be helpful to someone.

6 comments:

  1. Hi,

    I am developing an image viewer and went your way too. QMapListModel and so on with a custom delegate for my ListView.

    Issue is, the ListView shows all the items, but only loads the first image I add and shows the rest as black squares.

    Any idea why ?
    The code doesn't seem to be different from yours. Have you encountered this issue before?

    ReplyDelete
    Replies
    1. I think I faced some data model related issue when i was setting type attribute in wrong way. I choose to remove type attribute and then it was working fine. But else that this I did not faced any issue, for me image loads fine for each list item.

      ListItemComponent {
      type: "header"
      ...
      }

      Delete
    2. I see there is an image in your list view, how do I make a context menu to change that image. Thanks

      Delete
    3. I guess you are talking about thumbnail image in list delegate. You will have to change data model to change the thumbnail image in list delegate.

      Delete
  2. Hi Kunal, I need to implement a dynamic listview. Like, I ll get a list from server as json or XMLsoap . I need to parse it and put it in the list and show it. I am looking for some help on this.

    ReplyDelete
    Replies
    1. may be this link can help you,
      Its example where it uses the JSON data file as model

      http://blackberry.github.io/Cascades-Samples/rssnews.html

      Delete