I've come across the same issue raised in the following question:
How does one create a staggered grid view in QML?
I am looking to implement either a Flow
GridLayout
or GridView
in a Masonry style, see attached pictures of current output and desired.
CURRENT:
DESIRED:
I am not looking to use an external library for this; instead work purely with QML and JavaScript.
I have tried things such as a Flow within a Flickable:
Flickable
{
anchors.fill: parent
anchors.margins: 5
contentHeight: flow.height
Flow {
id: flow
width: parent.width
height: 800
spacing: 10
flow: Flow.TopToBottom
Repeater{
model: [80,60,120,75,90,55,140,50,70,90,80,60,120,75,90]
Rectangle {
height: modelData
width: 100
border.color: "black"
}
}
}
}
Also with my GridLayout working with the preferredHeight
and fillHeight
:
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: Layout.columnSpan
Layout.preferredHeight: Layout.rowSpan
However both present the problem of the rowHeight being set to the tallest object per row.
MY QUESTION IS
How can I create unequal cell size in a grid where the rows are not locked to the tallest object for a masonry view?
The only 'hacky' solution that's come to mind is now have two lists, one whose model is every odd index, the other every even index attached to the same flickable so they scroll as one object, but surely there must be a better way to achieve a masonry view with no whitespace? I have tested this and it does work; (see below) but again, is there a better way to implement?
function isEven(n) {
return n % 2 == 0;
}
function isOdd(n) {
return Math.abs(n % 2) == 1;
}
JsonListModel {
id: jsonModel; source: userFeed; keyField: "id"
fields: ["id", "owner", "downloadUrl","profile_Pic_URL", "tag", "timestamp","post_description", "team", "liked_by", "location"]
}
SortFilterProxyModel {
id: sortedModelOdd;
Component.onCompleted: {app.userFeedChanged();sourceModel = jsonModel}
filters: [
ExpressionFilter {expression: model.tag === exploreFilter},
ExpressionFilter {expression: isOdd(index)}
]
sorters: RoleSorter {roleName: "timestamp"; ascendingOrder: false}
}
SortFilterProxyModel {
id: sortedModelEven;
Component.onCompleted: {app.userFeedChanged();sourceModel = jsonModel}
filters: [
ExpressionFilter {expression: model.tag === exploreFilter},
ExpressionFilter {expression: isEven(index)}
]
sorters: RoleSorter {roleName: "timestamp"; ascendingOrder: false}
}
// i create 2 versions of the same model by using my expressionFilter for odds/evens
ScrollView {
anchors.fill: parent
Row {
anchors.fill: parent
spacing: dp(5)
anchors.margins: dp(5)
AppListView {
id: evenModelView
model: sortedModelEven; emptyText.text: qsTr("No posts yet!"); scale: 0.96
width: parent.width/2; spacing: dp(5); scrollIndicatorVisible: false;
delegate: AppImage {width: parent.width; fillMode: Image.PreserveAspectFit; source: model.downloadUrl}
}
AppListView {
id: oddModelView
model: sortedModelOdd; emptyText.text: qsTr("No posts yet!"); scale: 0.96
width: parent.width/2; spacing: dp(5);scrollIndicatorVisible: false;
delegate: AppImage {width: parent.width; fillMode: Image.PreserveAspectFit; source: model.downloadUrl}
}
}
}
I have found a solution (which I touched on in my question - and refined it) so will post as answer but still happy to hear if anyone achieves this result using actual Layouts ect.
JsonListModel {
id: jsonModel; source: jsonArray; keyField: "id"
fields: ["id", "owner", "downloadUrl","profile_Pic_URL", "tag", "timestamp","post_description", "team", "liked_by", "location"]
}
SortFilterProxyModel {
id: sortedModelOdd; Component.onCompleted: {sourceModel = jsonModel}
filters: [ExpressionFilter {expression: model.tag === exploreFilter; enabled: exploreFilter !== "Everything"}, ExpressionFilter {expression: isOdd(index)}]
sorters: RoleSorter {roleName: "timestamp"; ascendingOrder: false}
}
SortFilterProxyModel {
id: sortedModelEven; Component.onCompleted: {sourceModel = jsonModel}
filters: [ExpressionFilter {expression: model.tag === exploreFilter; enabled: exploreFilter !== "Everything"}, ExpressionFilter {expression: isEven(index)}]
sorters: RoleSorter {roleName: "timestamp"; ascendingOrder: false}
}
AppFlickable {
id: flickable
anchors.fill: parent; contentHeight: scrollRow.height + dp(Theme.navigationBar.height)*2; anchors.topMargin: scrollModel.height
Row {
id: scrollRow; width: parent.width
Column {
id: col; spacing: dp(5); width: parent.width/2
Behavior on y {
NumberAnimation {properties: "y"; duration: 1000 }
}
Repeater {
id: evenModelView; model: sortedModelEven;
delegate: AppCard {
id: evenImage; width: (parent.width)-dp(2); margin: dp(5); paper.radius: dp(5); scale: 0.96;
media: AppImage {
width: parent.width; fillMode: Image.PreserveAspectFit; source: model.downloadUrl; autoTransform: true
MouseArea {anchors.fill: parent; onPressAndHold: PictureViewer.show(homePage, model.downloadUrl); onReleased: PictureViewer.close(); onClicked: {exploreStack.push(viewPostComp, {postID: model.id})}}
}
content: AppText {
width: parent.width; padding: dp(15); maximumLineCount: 2; elide: Text.ElideRight; wrapMode: Text.Wrap; text: model.owner.username
MouseArea {anchors.fill: parent; onClicked: exploreStack.push(otherUserComp, {userID: model.owner.id})}
}
actions: Row {
IconButton {
icon: {
var likedList = Object.keys(model.liked_by.list)
likedList.indexOf(userData.id) ? IconType.heart : IconType.hearto}
onClicked: {likedList.indexOf(userData.id) ? dataModel.likePost(model.id, false, model.liked_by.count, model.owner.id) : dataModel.likePost(model.id, true, model.liked_by.count, model.owner.id)}
}
}
}
}
}
Column {
id: oddCol; spacing: dp(5); width: parent.width/2
Repeater {
id: oddModelView; model: sortedModelOdd;
delegate: AppCard {
id: oddImage; width: (parent.width)-dp(2); margin: dp(5); paper.radius: dp(5); scale: 0.96;
media: AppImage {
width: parent.width; fillMode: Image.PreserveAspectFit; source: model.downloadUrl; autoTransform: true
MouseArea {anchors.fill: parent; onPressAndHold: PictureViewer.show(homePage, model.downloadUrl); onReleased: PictureViewer.close(); onClicked: {exploreStack.push(viewPostComp, {postID: model.id})}}
}
content: AppText{
width: parent.width; padding: dp(15); maximumLineCount: 2; elide: Text.ElideRight; wrapMode: Text.Wrap; text: model.owner.username;
MouseArea {anchors.fill: parent; onClicked: exploreStack.push(otherUserComp, {userID: model.owner.id})}
}
actions: Row {
IconButton {
icon: {
var likedList = Object.keys(model.liked_by.list)
likedList.indexOf(userData.id) ? IconType.heart : IconType.hearto}
onClicked: {likedList.indexOf(userData.id) ? dataModel.likePost(model.id, false, model.liked_by.count, model.owner.id) : dataModel.likePost(model.id, true, model.liked_by.count, model.owner.id)}
}
}
}
}
}
}
}
Notes:
I used a ScrollView
initially instead of a Flickable
, however this caused serious performance issues.
I placed this inside of a Loader
which did resolve the issue somewhat as it wasn't tearing into performance at runtime, I then changed to a Flickable
, which has caused 0 issues thus far (yet to test with a large model).
Each AppListView
needs it's interactive disabled otherwise sometimes scrolling can catch onto only one of the columns and put them out of sync.