Qml 实现瀑布流布局

【写在前面】

最近在刷掘金的时候看到一篇关于瀑布流布局的文章,然鹅他们的实现都是前端的那套,就想着 Qml 有没有类似实现。

结果百度了一圈也没有( T_T Qml 凉了凉了 ),于是,我按照自己理解,简单实现了一个 Qml 版的瀑布流布局。

关于瀑布流:

瀑布流布局(Waterfall Layout),也被称为瀑布式布局或多栏自适应布局,是一种网页布局技术,它允许内容以多列的形式显示,类似于瀑布一样从上到下流动。这种布局方式特别适合于展示图片或卡片式内容,如图片库、新闻摘要、商品列表等。

瀑布流布局的特点包括:

  1. 多列显示:内容被分割成多列,每列可以独立滚动,使得页面可以展示更多的信息。
  2. 动态宽度:每列的宽度通常是固定的,而内容块(如图片或卡片)的宽度可以是动态的,以适应不同的屏幕大小。
  3. 不等高:内容块的高度可以不同,这样可以使布局看起来更加自然和有吸引力。
  4. 响应式:布局可以根据用户的屏幕尺寸自动调整,以提供最佳的浏览体验。
  5. 灵活性:内容块可以自由地在列之间流动,不需要严格的对齐。

【正文开始】

一个经典的瀑布流布局来自小红书:

而我们实现的 Qml 版效果图如下:

现在开始讲解思路:

首先考虑屏幕宽度,竖屏两列,横屏可以三列或者更多,应当根据宽度动态改变,然后便可以计算出列宽:

width: (flickable.width - flickable.spacing) / flickable.column

因此,其实未知的仅有卡片高度:

如图所示,卡片高度由三部分组成:【封面图片高度】+【标题高度】+【卡片信息高度】

height: coverRealHeight + titleHeight + infoHeight

现在有了宽高,接下来只要计算出 位置 (x, y) 即可:

javascript 复制代码
    if (flickable.currentColumn == flickable.column) {
        flickable.currentColumn = 0;
        flickable.currentX = 0;
        for (let i = 0; i < flickable.column; i++) {
            flickable.currentY[i] += flickable.prevHeight[i];
        }
    }

    x = flickable.currentX;
    y = flickable.currentY[flickable.currentColumn];

    flickable.prevHeight[flickable.currentColumn] = Math.round(height + flickable.spacing);

    print(flickable.currentColumn, flickable.currentX, flickable.prevHeight, flickable.currentY);

    flickable.currentX += coverRealWidth + flickable.spacing;

    flickable.currentColumn++;

    let max = 0;
    for (let j = 0; j < flickable.column; j++) {
        max = Math.max(flickable.prevHeight[j] + flickable.currentY[j]);
    }

    flickable.contentHeight = max;
    

x 坐标计算思路是:从左往右依次增加一个卡片宽度,到达本行最后一个卡片时置零即可。

y 坐标计算思路是:记录下本行卡片高度数组 prevHeight[column],到达本行最后一个卡片时计算下行卡片 y 坐标数组 currentY[column],而首行则为 0。

至此,Rect (x, y, width, height) 全部已知,我们可以直接利用 Repeater 轻松实例化出来:

javascript 复制代码
Repeater {
    id: repeater
    model: ListModel {
        id: listModel
        Component.onCompleted: {
            flickable.loadMore();
        }
    }
    delegate: Rectangle {
        id: rootItem
        width: (flickable.width - flickable.spacing) / flickable.column
        height: coverRealHeight + titleHeight + infoHeight
        radius: 4
        clip: true

        property real aspectRatio: coverWidth / coverHeight
        property real coverRealWidth: width
        property real coverRealHeight: width / aspectRatio
        property real titleWidth: width
        property real titleHeight: titleText.height
        property real infoWidth: width
        property real infoHeight: 50

        Component.onCompleted: {
            if (flickable.currentColumn == flickable.column) {
                flickable.currentColumn = 0;
                flickable.currentX = 0;
                for (let i = 0; i < flickable.column; i++) {
                    flickable.currentY[i] += flickable.prevHeight[i];
                }
            }

            x = flickable.currentX;
            y = flickable.currentY[flickable.currentColumn];

            flickable.prevHeight[flickable.currentColumn] = Math.round(height + flickable.spacing);

            print(flickable.currentColumn, flickable.currentX, flickable.prevHeight, flickable.currentY);

            flickable.currentX += coverRealWidth + flickable.spacing;

            flickable.currentColumn++;

            let max = 0;
            for (let j = 0; j < flickable.column; j++) {
                max = Math.max(flickable.prevHeight[j] + flickable.currentY[j]);
            }

            flickable.contentHeight = max;
        }

        Column {
            Item {
                id: coverPort
                width: coverRealWidth
                height: coverRealHeight

                Image {
                    anchors.fill: parent
                    anchors.topMargin: rootItem.radius
                    source: cover
                }
            }

            Item {
                id: titlePort
                width: titleWidth
                height: titleText.height

                Text {
                    id: titleText
                    width: parent.width
                    wrapMode: Text.WrapAnywhere
                    text: title
                    font.family: "微软雅黑"
                    font.pointSize: 14
                }
            }

            Item {
                id: infoPort
                width: infoWidth
                height: infoHeight

                RowLayout {
                    anchors.fill: parent

                    CircularImage {
                        id: head
                        Layout.preferredWidth: parent.height - 5
                        Layout.preferredHeight: parent.height - 5
                        Layout.leftMargin: 5
                        Layout.alignment: Qt.AlignVCenter
                        source: "file:/C:/Users/mps95/Desktop/head.jpg"
                    }

                    Text {
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                        text: "用户" + user
                        font.pointSize: 14
                        verticalAlignment: Text.AlignVCenter
                        elide: Text.ElideRight
                    }

                    Text {
                        Layout.preferredWidth: 100
                        Layout.preferredHeight: parent.height
                        Layout.rightMargin: 5
                        text: (like ? "🩷" : "🤍") + " " + Math.round(Math.random() * 1000)
                        font.pointSize: 14
                        horizontalAlignment: Text.AlignRight
                        verticalAlignment: Text.AlignVCenter
                        property int like: Math.round(Math.random())
                    }
                }
            }
        }
    }
}

loadMore() 是向后台请求更多的卡片数据,这部分需要根据实际需求进行改造,我这里就简单生成了一些模拟数据:

javascript 复制代码
function loadMore() {
    //这部分来自后台请求, 必须知道封面宽高
    let titleList = [
            "单行标题: 测试测试测试测试",
            "双行标题: 测试测试测试测试测试测测试测试测试测试测试测试",
            "三行标题: 测试测试测试测试测试测测试测试测试测试测试测试测试测试测试测试测试测试测试"
        ];
    for (let i = 0; i < 10; i++) {
        let userId = Math.round(Math.random() * 100000);
        let type = Math.round(Math.random());  //0 image / 1 video
        let cover = "file:/C:/Users/mps95/Desktop/素材/动漫图片/img2" + i + ".jpg"; //封面, 无论视频还是图片都需要有
        let url = cover;
        if (type == 1) {
            //url = "file:/test.mp4";
        }

        let object = {
            type: type,
            cover: cover,
            user: userId,
            url: url,
            title: titleList[Math.round(Math.random() * 2)],
            coverWidth: 300,
            coverHeight: (type + 2) * 100 + Math.round(Math.random() * 3) * 80
        };

        jsonData.push(object);
        listModel.append(object);
    }
}

【结语】

最后:项目链接(多多star呀..⭐_⭐):

Github 的 WaterfallFlow 瀑布流视图(并且可以自适应),类似小红书

注意: 测试用的图片没有包含在内,请改为自己的测试集。

相关推荐
锦亦之22332 小时前
QT+OSG+OSG-earth如何在窗口显示一个地球
开发语言·qt
柳鲲鹏5 小时前
编译成功!QT/6.7.2/Creator编译Windows64 MySQL驱动(MinGW版)
开发语言·qt·mysql
三玖诶5 小时前
如何在 Qt 的 QListWidget 中逐行添加和显示数据
开发语言·qt
阳光开朗_大男孩儿11 小时前
DBUS属性原理
linux·服务器·前端·数据库·qt
Alphapeople12 小时前
Qt Modbus
开发语言·qt
竹林海中敲代码12 小时前
Qt Creator 集成开发环境 常见问题
qt·qt工具常见问题
竹林海中敲代码15 小时前
Qt安卓开发连接手机调试(红米K60为例)
android·qt·智能手机
长沙红胖子Qt16 小时前
关于 Qt运行加载内存较大崩溃添加扩大运行内存 的解决方法
开发语言·qt·qt扩大运行内存
gopher951117 小时前
qt相关面试题
开发语言·qt·面试
三玖诶1 天前
在 Qt 中使用 QLabel 设置 GIF 动态背景
开发语言·qt·命令模式