Forums

OverviewV-Play 2 Support (Qt 5) › AppListView.restoreScrollPosition() not working.

Viewing 8 posts - 1 through 8 (of 8 total)
  • Author
    Posts
  • #19199

    Arne

    Hi,

    I use an AppListView with a C++ Model. Everything is working as expected, except the restoreScrollPosition(). I store the last scroll position before updating the model with:

    rootTP.timelinePos = listView.getScrollPosition();
    
    // debug output
    qml: store pos
    qml: {"index":10,"pos":{"x":0,"y":228.0000198694679}}

    After the model changed I send a signal in my c++ backend, onModelChanged() to restore the position:

    listView.restoreScrollPosition(timelinePos);
    
    // debug output
    qml: restore pos
    qml: {"index":10,"pos":{"x":0,"y":228.0000198694679}}

    So I am absolutely sure that the position information is not overwritten during the model change.

    But it does not work. Then I tried to add a custom button which restores the scroll position on click, to be sure that it has nothing to do with timing or signal problems, but it does not work either.

    Has somebody a hint or maybe a solution? :)

    Here is my Page.qml (I stripped code for better overview):

    import VPlay 2.0
    import VPlayApps 1.0
    import QtQuick 2.0
    import QtQuick.Controls 1.4
    import QtQuick.Layouts 1.3
    import VPlayPlugins 1.0
    import "../R24"
    import "../util"
    import ruhr24.backend 1.0
    
    Page {
        id: rootTP
        property var timelinePos
    
        Dialog {
            // [...]
        }
    
        // The c++ backend
        Backend {
            id: backend
            language: Qt.locale().name.substring(0, 2) || "de"
    
            onErrorMessage: {
                messageDialog.title = errorString;
                messageDialog.open();
            }
    
            // THIS GETS FIRED WHEN BACKEND LOADING IS STARTED OR STOPPED
            // PARAMETER bool status 
            onIsLoadingChanged: {
                if(status){
                    rootTP.timelinePos = listView.getScrollPosition();
                    console.log("store pos");
                    console.log(JSON.stringify(rootTP.timelinePos));
                    // play loading animation
                    customBar.play();
                } else {
                    // stop loading animation
                    customBar.stop();
                }
            }
    
            onConfigChanged: {
                // if there is a banner, set it up
                rootTP.setBanner();
                // reload menu
                rootTP.setMenu();
            }
    
            // THIS GETS FIRED WHEN THE MODEL CHANGED
            onModelChanged: {
                listView.restoreScrollPosition(timelinePos);
                console.log("restore");
                console.log(JSON.stringify(rootTP.timelinePos));
    
                // update emojis
                var modelLen = getTimelineModel().length || 0;
                for(var i2 = 0; i2 < modelLen; i2++){
                    var _set = database.getValue( getTimelineModel()[i2].post_id );
                    if( typeof _set !== 'undefined' && typeof _set.id !== 'undefined' ){
                        getTimelineModel()[i2].post_most_reactions = _set.reactions;
                        getTimelineModel()[i2].post_total_reactions = _set.total;
                    }
                }
            }
        }
    
        PageNavBarBackground {
            id: customBar
            onLogoClick: {
            	// this works
                listView.positionViewAtBeginning();
            }
        }
    
        // left menu, language switch, categories etc
        AppDrawer {
            // [...]
        } // - end AppDrawer
    
        // Main Navigation Drawer Button
        leftBarItem: IconButtonBarItem {
            // [...]
        }
    
        // HERE I TRIED TO HANDLE THE SCROLL POSITION MANUALLY
        rightBarItem: IconButtonBarItem {
            icon: IconType.refresh
            onClicked: {
            	backend.nextPage()
            	listView.restoreScrollPosition(timelinePos)
        	}
        }
    
        // One Signal Push Notifications
        OneSignal {
            // [...]
        }
    
        AdMobBanner {
            // [...]
        }
    
        // store data between states,
        // but clear everything at restart
        Storage {
            // [...]
        }
    
        // HERE IS THE LISTVIEW
        AppListView {
            id: listView
            anchors.top: customBar.bottom
            anchors.bottom: parent.bottom
            model: backend.model
            delegate: ArticleRow {
                id: timelineRow
                onSelected: {
                    navigationStack.push(articlePageComponent, {
                                             post_url: backend.filterUrl( timelineRow.post_external_url, timelineRow.post_url )
                                         })
                }
                onEmojiUpdate: { // onEmojiUpdate(int post_id, int total, var most_reactions)
                    // store the emoji state into temporary database
                    var _obj = {id: post_id, total: total, reactions: most_reactions};
                    database.setValue(_obj.id, _obj)
                }
            }
            backgroundColor: "#000000"
            scrollIndicatorVisible: false
    
            // Load newer elements by pulling the list down
            PullToRefreshHandler {
                id: refreshHandler
                contentColor: Theme.navigationBar.titleColor
                onRefresh: {
                    backend.firstPage();
                }
            } // - end PullToRefreshHandler
    
            // Load more elements if this item becomes visible
    //        footer: VisibilityRefreshHandler {
    //            id: footerRefreshHandler
    //            onRefresh: {
    //                backend.nextPage();
    //            }
    //        } // - end VisibilityRefreshHandler
    
            Component.onCompleted: {
                // init backend and fetch configuration from api
                backend.init();
                // init model
                backend.firstPage();
            }
        } // - AppListView end
    
        // get config object
        function getConfig(){
            return (typeof backend.config !== 'undefined' ? backend.config : ({}) );
        }
    
        // get the timeline model
        function getTimelineModel(){
            return (typeof backend.model !== 'undefined' ? backend.model : []);
        }
    
        // get the timeline language
        function getLanguage(){
            return backend.language;
        }
    
        // set the timeline language
        function setLanguage(code){
            backend.language = code;
        }
    
        // get the current category
        function getCategory(){
            return backend.category;
        }
    
        // set the current category
        function setCategory(cat){
            backend.category = cat;
        }
    
        // set menu from api cfg.menu object
        function setMenu() {
            if( typeof getConfig() !== 'undefined' && typeof getConfig().main_menu !== 'undefined' ){
                var mainMenu = getConfig().main_menu;
                var menu = [];
                for(var i = 0; i < mainMenu.length; i++){
                    menu[i] = {};
    
                    // remap with fontawesome character for icon
                    Object.keys(mainMenu[i]).forEach(function(key, index){
                        if(key == 'icon' && typeof mainMenu[i][key] !== 'undefined'){
                            menu[i][key] = IconType[ mainMenu[i][key] ];
                        } else {
                            menu[i][key] = mainMenu[i][key];
                        }
                    }, mainMenu[i]);
                }
    
                // assign menu to listView in the menuDrawer
                menuListView.model = menu;
            }
        }
    
        // show custom banner
        function setBanner(){
            // set timeline banner from api app config
            // - default values
            customBar.bannerUrl = "";
            customBar.bannerPartnerUrl = "";
            // - overwrite values
            if(typeof getConfig().timeline_banner_url !== 'undefined'){
                if( getConfig().timeline_banner_url.length > 0){
                    // set banner image url
                    customBar.bannerUrl = getConfig().timeline_banner_url;
    
                    // set external url
                    if( typeof cfg.timeline_banner_partner_url !== 'undefined' ){
                        customBar.bannerPartnerUrl = getConfig().timeline_banner_partner_url;
                    }
                } // - end if length
            } // - end if typeof
        }
    }
    

     

    #19208

    Alex
    V-Play Team

    Hi Arne,

    indeed there seems to be in issue, it looks like the getScrollPosition is not working correctly, we need to investigate this closer. Would it be sufficient for you to just use the contentY property, like this:

    import VPlayApps 1.0
    
    App {
      NavigationStack {
    
        Page {
          id: page
          title: "Add List Items"
    
          // the data model for the list
          property var dataModel: [
            { text: "Item 1" },
            { text: "Item 2" },
            { text: "Item 3" }
          ]
    
          // button to add an item
          AppButton {
            id: button
            anchors.horizontalCenter: parent.horizontalCenter
            text: "Add Row"
            onClicked: {
              // create and add new item
              var itemNr = page.dataModel.length + 1
              var newItem = { text: "Item "+itemNr }
              page.dataModel.push(newItem)
    
              // signal change in data model to trigger UI update (list view)
              var pos = listView.contentY//listView.getScrollPosition() //retrieve scroll position data
              page.dataModelChanged()
              //console.debug("restore scroll position: " + pos.pos.y)
              //listView.restoreScrollPosition(pos) //scrolls to the previous position
              listView.contentY = pos
            }
          }
    
          // list view
          AppListView {
            id: listView
            anchors.top: button.bottom
            anchors.bottom: parent.bottom
            width: parent.width
    
            model: page.dataModel
            delegate: SimpleRow {}
          }
        }
      }
    }
    

    Cheers,
    Alex

    #19210

    Arne

    Hi Alex,

    thanks for the reply. I tried your hotfix, but it doesn’t work.

    Also I changed the delegate to something simple, to test if it has something to do with the delegate item. But there is no difference.

    Then I tried to use the function ListView.positionViewAtIndex(int) but it doesn’t work either.

    Now I will wait for your further investigation.

    Cheers,

    Arne

    #19241

    Arne

    Hi Alex,

    because we want to launch our app soon, but are on hold because of this issue, is it possible to say how much time you need to fix this? Like 1 month or so?

    Cheers,

    Arne

    #19255

    Günther
    V-Play Team

    Hi Arne!

    I created a small runnable version of your code-snippet (with dummy data Item instead of your C++ backend, and SimpleRow items for the list).
    The following example is runnable for me (with latest V-Play version), the list does not lose its position:

    import VPlayApps 1.0
    import QtQuick 2.0
    import VPlay 2.0
    import VPlayApps 1.0
    import QtQuick 2.0
    import QtQuick.Controls 1.4
    import QtQuick.Layouts 1.3
    import VPlayPlugins 1.0
    
    
    App {
      onInitTheme: Theme.platform = "android"
    
    
      // adds tab navigation
      Navigation {
    
        // first tab
        NavigationItem {
          icon: IconType.home
          title: "Main"
    
          // main page
          NavigationStack {
    
            initialPage: testPage
    
          }
        }
      }
    
      Component {
        id: testPage
    
        Page {
            id: rootTP
            property var timelinePos
    
            Dialog {
                // [...]
            }
    
            // The c++ backend
            Item {
                id: backend
    
                property var model: []
                property bool isLoading: false
    
                function firstPage() {
                  // start loading and reset model
                  // NOTE: if the model is changed / reset befor isLoading gets true, the wrong position will be stored
                  isLoading = true
                  model = []
    
                  // data loaded -> fill model
                  for(var i = 0; i< 25; i++)
                    model.push({ text: "First Page Item: "+i})
                  modelChanged()
                  isLoading = false
                }
    
                function nextPage() {
                  isLoading = true
                  model = []
                  for(var i = 0; i< 99; i++)
                    model.push({ text: "Next Page Item: "+i})
                  modelChanged()
                  isLoading = false
                }
    
                // THIS GETS FIRED WHEN BACKEND LOADING IS STARTED OR STOPPED
                // PARAMETER bool status
                onIsLoadingChanged: {
                    if(isLoading){
                        rootTP.timelinePos = listView.getScrollPosition();
                        console.log("store pos");
                        console.log(JSON.stringify(rootTP.timelinePos));
                    }
                }
    
                // THIS GETS FIRED WHEN THE MODEL CHANGED
                onModelChanged: {
    //                listView.restoreScrollPosition(timelinePos);
                    console.log("restore");
                    console.log(JSON.stringify(rootTP.timelinePos));
                }
            }
    
            // HERE I TRIED TO HANDLE THE SCROLL POSITION MANUALLY
            rightBarItem: IconButtonBarItem {
                icon: IconType.refresh
                onClicked: {
                  backend.nextPage()
                  listView.restoreScrollPosition(timelinePos)
              }
            }
    
            // HERE IS THE LISTVIEW
            AppListView {
                id: listView
                anchors.top: parent.top
                anchors.bottom: parent.bottom
                model: backend.model
                delegate: SimpleRow { text: modelData.text }
                backgroundColor: "#000000"
                scrollIndicatorVisible: false
    
                // Load newer elements by pulling the list down
                PullToRefreshHandler {
                    id: refreshHandler
                    contentColor: Theme.navigationBar.titleColor
                    onRefresh: {
                        backend.firstPage();
                    }
                } // - end PullToRefreshHandler
    
    
                Component.onCompleted: {
                    // init model
                    backend.firstPage();
                }
            } // - AppListView end
        }
      }
    }
    

    Does this example also work for you with latest V-Play 2.16.1? (I tested on macOS and iOS)

    Best,
    Günther

    #19256

    Günther
    V-Play Team

    If you still have issues, please send over a minimum example project which shows the issue to support@v-play.net. Please also let me know which platforms / devices you are experiencing the issue on.

    Best,
    Günther

    #19371

    Arne

    Hi Günther,

    thanks for your time. I can confirm that your code is working but only with a SimpleRow delegate.

    So it seems that the problem relies inside my custom delegate. I will investigate further.

    Best,

    Arne

    #19374

    Arne

    Finally I found the mistake. It’s not a bug, I think.

    I have made a custom delegate item like this:

    Column {
    	Image {
    		id: thumbnail
    		// sometimes this is 0
    		sourceSize.width: parent.width
    		// sometimes this is 0 too, mostly if the post_thumbnail_size is empty
            sourceSize.height: {
                // callback to scale the image height proportionally
                var scaleX = rowContentCols.width / post_thumbnail_size.w;
                var sourceHeight = Math.round(scaleX * post_thumbnail_size.h);
    
                return sourceHeight;
            }
    
            Label { id: date }
    	}
    
    	Label { id: title }
    	RowLayout { id: footer }
    }
    

    Because the image has a dynamic height and sometimes the post_thumbnail is 0, the whole element collapse to height 0. On that point, the ListView gets confused and does not calculate the correct element height.

    So I added a signal handler like this:

    Column {
    	Image {
    		id: thumbnail
    		// sometimes this is 0
    		sourceSize.width: parent.width
    		// sometimes this is 0 too, mostly if the post_thumbnail_size is empty
            sourceSize.height: {
                // callback to scale the image height proportionally
                var scaleX = rowContentCols.width / post_thumbnail_size.w;
                var sourceHeight = Math.round(scaleX * post_thumbnail_size.h);
    
                return sourceHeight;
            }
            // handle the possible 0 values
            onSourceSizeChanged: {
                if( sourceSize.width == 0 || sourceSize.height == 0 ){
                    if( post_thumbnail_size.width > sourceSize.width ){
                        sourceSize.width = post_thumbnail_size.width;
                    }
                    if( post_thumbnail_size.height > sourceSize.height ){
                        sourceSize.height = post_thumbnail_size.height;
                    }
    
                    if( sourceSize.width == 0 && sourceSize.height == 0 ){
                        sourceSize.width = cardRect.width;
                        sourceSize.height = cardRect.height;
                    }
                }
            }
    
            Label { id: date }
    	}
    
    	Label { id: title }
    	RowLayout { id: footer }
    }

    Also I have implemented the changes from Günthers example.

    Now my AppListView looks like this and it’s working:

    Backend {
    	id: backend
    	onIsLoadingChanged: {
    		if(isLoading){
    			// store timeline position
                storeTimelinePosition();
    		}
    	}
    }
    
    AppListView {
        id: listView
        backgroundColor: "#000000"
        scrollIndicatorVisible: false
        model: backend.model
        delegate: SimpleRow {
            id: dataRow
            implicitHeight: 400
            state: (modelData.type == "adv" ? "ADV" : "POST")
            states: [
                State {
                    name: "POST"
                    PropertyChanges { target: timelineRow; visible: true }
                    PropertyChanges { target: adRow; visible: false }
                    PropertyChanges { target: dataRow; height: Math.abs(timelineRow.implicitHeight) }
                },
                State {
                    name: "ADV"
                    PropertyChanges { target: timelineRow; visible: false }
                    PropertyChanges { target: adRow; visible: true }
                    PropertyChanges { target: dataRow; height: Math.abs(backend.config.advertising.container_height) }
                }
            ]
    
            // TimelineRow Signal Connections
            Connections {
                target: timelineRow
                enabled: timelineRow.visible
                onHeightChanged: {
                    dataRow.height = Math.round( Math.max(dataRow.implicitHeight, timelineRow.height) )
                }
                onSelected: navigationStack.push(articlePageComponent, { post_url: backend.filterUrl( timelineRow.post_external_url, timelineRow.post_url ) })
                onEmojiUpdate: { // onEmojiUpdate(int post_id, int total, var most_reactions)
                    // store the emoji state into temporary database
                    var _obj = {id: post_id, total: total, reactions: most_reactions};
                    database.setValue(_obj.id, _obj)
                }
            } // - TimelineRow Connections
    
            // AdRow Signal Connections
            Connections {
                target: adRow
                enabled: adRow.visible
                onHeightChanged: {
                    if(adRow.visible){
                        dataRow.implicitHeight = Math.abs( adRow.height );
                    }
                }
            } // - AdRow Connections
    
            // Article
            ArticleRow {
                id: timelineRow
                visible: false
            } // - ArticleRow
    
            // Advertising
            AdRow {
                id: adRow
                visible: false
            } // - AdRow
    
        } // - SimpleRow
        // Load newer elements by pulling the list down
        PullToRefreshHandler {
            id: refreshHandler
            contentColor: Theme.navigationBar.titleColor
            onRefresh: {
                backend.firstPage();
            }
        } // - end PullToRefreshHandler
    
        // Load more elements if this item becomes visible
        footer: VisibilityRefreshHandler {
            id: footerRefreshHandler
            onRefresh: {
                backend.nextPage();
                restoreTimelinePosition();
            }
        } // - end VisibilityRefreshHandler
    
        Component.onCompleted: {
            // init backend and fetch configuration from api
            backend.init();
            // init model
            backend.firstPage();
        }
    } // - AppListView end

    The more I get used to QML the more I like it :)

    Best, Arne

Viewing 8 posts - 1 through 8 (of 8 total)

RSS feed for this thread

You must be logged in to reply to this topic.

Voted #1 for:

  • Easiest to learn
  • Most time saving
  • Best support

Develop Cross-Platform Apps and Games 50% Faster!

  • Voted the best supported, most time-saving and easiest to learn cross-platform development tool
  • Based on the Qt framework, with native performance and appearance on all platforms including iOS and Android
  • Offers a variety of plugins to monetize, analyze and engage users
FREE!
create apps
create games
cross platform
native performance
3rd party services
game network
multiplayer
level editor
easiest to learn
biggest time saving
best support