Created
January 11, 2021 10:31
-
-
Save raven-worx/28a737f7333480cfac418b42480093fc to your computer and use it in GitHub Desktop.
QML GridFlow (Masonry) Layout type
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "GridFlow.hpp" | |
#include <QQmlInfo> | |
#include <QTime> | |
#include <QQmlProperty> | |
#include <QCoreApplication> | |
GridFlow::GridFlow(QQuickItem *parentItem) | |
: QQuickItem(parentItem), m_HorizontalSpacing(5), m_VerticalSpacing(5), m_Margin(5) | |
, m_RowHeight(50), m_SpanningEnabled(true), m_KeepChildOrder(false), m_ColumnWidth(0), m_ColumnCount(4) | |
{ | |
qsrand(QTime::currentTime().msec()); // TODO: use QRandomGenerator (since Qt 5.10) | |
connect(this, &QQuickItem::widthChanged, this, &GridFlow::scheduleLayout); | |
connect(this, &QQuickItem::heightChanged, this, &GridFlow::scheduleLayout); | |
this->attachToParent( parentItem ); | |
} | |
GridFlow::~GridFlow() | |
{ | |
} | |
GridFlowAttached* GridFlow::qmlAttachedProperties(QObject* attachee) | |
{ | |
return new GridFlowAttached(attachee); | |
} | |
void GridFlow::doLayout() | |
{ | |
if( !this->isVisible() || !this->parentItem() ) | |
return; | |
const QList<QQuickItem*> items = this->childItems(); | |
if( items.isEmpty() ) | |
return; | |
m_ColumnHeights = QVector<qreal>( m_ColumnCount, m_Margin ); | |
m_ColumnWidth = ((this->width() - 2*m_Margin) / m_ColumnCount) - m_HorizontalSpacing; | |
m_ColumnWidth += (m_HorizontalSpacing / m_ColumnCount); | |
for( int i = 0; i < items.count(); ++i ) | |
{ | |
QQuickItem* item = items.at(i); | |
if( !item->isVisible() ) | |
continue; | |
GridFlowAttached* attachedLayoutItem = qobject_cast<GridFlowAttached*>( qmlAttachedPropertiesObject<GridFlow>(item,true) ); | |
this->connectLayoutItem( attachedLayoutItem, true ); | |
const int column = m_KeepChildOrder ? ((m_ColumnCount+i) % m_ColumnCount) : this->getMinColumnIndex(); | |
int rowspan = 0; | |
if( m_SpanningEnabled ) | |
{ | |
const qreal spanningChance = attachedLayoutItem->m_SpanningChance; | |
if( qrand() <= (spanningChance*RAND_MAX) ) | |
{ | |
if (column - 1 > 0 && m_ColumnHeights.at(column - 1) <= m_ColumnHeights.at(column)) | |
rowspan = -1; | |
else if (column + 1 < m_ColumnHeights.count() && m_ColumnHeights.at(column + 1) <= m_ColumnHeights.at(column)) | |
rowspan = 1; | |
} | |
} | |
QSizeF itemSize = this->layoutItemSize( item, attachedLayoutItem, 1+qAbs(rowspan) ); | |
if( itemSize.width() <= 0 || itemSize.height() <= 0 ) | |
continue; | |
itemSize.rheight() -= std::fmod( (m_ColumnHeights.at(column) + itemSize.height() + m_VerticalSpacing), m_RowHeight ); | |
QPointF pos = QPointF( | |
m_Margin + (m_ColumnWidth + m_HorizontalSpacing) * (column + (rowspan == -1 ? -1 : 0)), | |
m_ColumnHeights.at(column) | |
); | |
// apply property values and trigger possible connected animations | |
QQmlProperty(item, "x").write( QVariant::fromValue<qreal>(pos.x()) ); | |
QQmlProperty(item, "y").write( QVariant::fromValue<qreal>(pos.y()) ); | |
QQmlProperty(item, "width").write( QVariant::fromValue<qreal>(itemSize.width()) ); | |
QQmlProperty(item, "height").write( QVariant::fromValue<qreal>(itemSize.height()) ); | |
qreal next_height = m_ColumnHeights.at(column) + itemSize.height() + m_VerticalSpacing; | |
m_ColumnHeights[column + rowspan] = m_ColumnHeights[column] = next_height; | |
} | |
qreal implicitHeight = 0.0; | |
foreach( qreal h, m_ColumnHeights ) | |
implicitHeight = qMax(implicitHeight,h); | |
implicitHeight += m_Margin; | |
QQmlProperty(this, "implicitHeight").write( QVariant::fromValue<qreal>(implicitHeight) ); | |
} | |
void GridFlow::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) | |
{ | |
switch( change ) | |
{ | |
case QQuickItem::ItemVisibleHasChanged: | |
{ | |
if( !value.boolValue ) | |
break; | |
} | |
// fall through! | |
case QQuickItem::ItemChildAddedChange: | |
case QQuickItem::ItemChildRemovedChange: | |
{ | |
this->scheduleLayout(); | |
GridFlowAttached* attachedLayoutItem = qobject_cast<GridFlowAttached*>( qmlAttachedPropertiesObject<GridFlow>(value.item,false) ); | |
this->connectLayoutItem( attachedLayoutItem, change == QQuickItem::ItemChildAddedChange ); | |
} | |
break; | |
case QQuickItem::ItemParentHasChanged: | |
{ | |
this->attachToParent( value.item ); | |
} | |
break; | |
default: | |
break; | |
} | |
QQuickItem::itemChange(change,value); | |
} | |
void GridFlow::scheduleLayout() | |
{ | |
QCoreApplication::postEvent(this, new QEvent(QEvent::LayoutRequest)); // make use of Qt's event-compression | |
} | |
void GridFlow::attachToParent(QQuickItem *parentItem) | |
{ | |
if( !parentItem ) | |
return; | |
QObject* anchors = this->property("anchors").value<QObject*>(); //private type QQuickAnchors | |
QQmlProperty(anchors, "fill").write( QVariant::fromValue<QQuickItem*>(parentItem) ); | |
} | |
qreal GridFlow::horizontalSpacing() const | |
{ | |
return m_HorizontalSpacing; | |
} | |
void GridFlow::setHorizontalSpacing(qreal spacing) | |
{ | |
spacing = qMax(0.0, spacing); | |
if( m_HorizontalSpacing != spacing ) | |
{ | |
m_HorizontalSpacing = spacing; | |
Q_EMIT horizontalSpacingChanged(spacing); | |
this->scheduleLayout(); | |
} | |
} | |
qreal GridFlow::verticalSpacing() const | |
{ | |
return m_VerticalSpacing; | |
} | |
void GridFlow::setMargin(qreal margin) | |
{ | |
margin = qMax(0.0, margin); | |
if( m_Margin != margin ) | |
{ | |
m_Margin = margin; | |
Q_EMIT marginChanged(margin); | |
this->scheduleLayout(); | |
} | |
} | |
qreal GridFlow::margin() const | |
{ | |
return m_Margin; | |
} | |
void GridFlow::setRowHeight(qreal height) | |
{ | |
height = qMax(0.0, height); | |
if( m_RowHeight != height ) | |
{ | |
m_RowHeight = height; | |
Q_EMIT rowHeightChanged(height); | |
this->scheduleLayout(); | |
} | |
} | |
qreal GridFlow::rowHeight() const | |
{ | |
return m_RowHeight; | |
} | |
void GridFlow::setVerticalSpacing(qreal spacing) | |
{ | |
spacing = qMax(0.0, spacing); | |
if( m_VerticalSpacing != spacing ) | |
{ | |
m_VerticalSpacing = spacing; | |
Q_EMIT verticalSpacingChanged(spacing); | |
this->scheduleLayout(); | |
} | |
} | |
int GridFlow::columnCount() const | |
{ | |
return m_ColumnCount; | |
} | |
void GridFlow::setColumnCount(int count) | |
{ | |
count = qMax(count,1); | |
if( m_ColumnCount != count ) | |
{ | |
m_ColumnCount = count; | |
this->scheduleLayout(); | |
Q_EMIT columnCountChanged(count); | |
} | |
} | |
void GridFlow::setSpanningEnabled(bool enabled) | |
{ | |
if( m_SpanningEnabled != enabled ) | |
{ | |
m_SpanningEnabled = enabled; | |
this->scheduleLayout(); | |
Q_EMIT spanningEnabledChanged(enabled); | |
} | |
} | |
bool GridFlow::isSpanningEnabled() const | |
{ | |
return m_SpanningEnabled; | |
} | |
void GridFlow::setKeepChildOrder(bool keepChildOrder) | |
{ | |
if( m_KeepChildOrder != keepChildOrder ) | |
{ | |
m_KeepChildOrder = keepChildOrder; | |
this->scheduleLayout(); | |
Q_EMIT keepChildOrderChanged(keepChildOrder); | |
} | |
} | |
bool GridFlow::keepChildOrder() const | |
{ | |
return m_KeepChildOrder; | |
} | |
#ifdef max | |
#undef max | |
#endif | |
int GridFlow::getMinColumnIndex() | |
{ | |
qreal minHeight = std::numeric_limits<qreal>::max(); | |
int minIdx = m_ColumnHeights.count() ? 0 : -1; | |
for( int i = 0; i < m_ColumnHeights.count(); ++i ) | |
{ | |
qreal h = m_ColumnHeights.at(i); | |
if( h < minHeight) | |
{ | |
minHeight = h; | |
minIdx = i; | |
} | |
} | |
return minIdx; | |
} | |
QSizeF GridFlow::layoutItemSize(QQuickItem* item, GridFlowAttached* layoutItem, int rowspan) const | |
{ | |
QSizeF initSize = this->getInitialItemSize(item); | |
if( !initSize.isValid() ) | |
return QSizeF(); | |
qreal w = m_ColumnWidth * rowspan; | |
if (rowspan > 1) | |
w += m_VerticalSpacing; | |
qreal h = (initSize.height() * w) / initSize.width(); | |
qreal hh = qMax( layoutItem->m_MinimumHeight, h ); | |
if( layoutItem->m_MaximumHeight > 0 ) | |
hh = qMin( layoutItem->m_MaximumHeight, h ); | |
qreal ww = (w * hh) / h; | |
return QSizeF(ww,hh); | |
} | |
QSizeF GridFlow::getInitialItemSize(QQuickItem* item) const | |
{ | |
Q_ASSERT( item ); | |
QSizeF size; | |
if( !item ) | |
return size; | |
if( QObject* attached = qmlAttachedPropertiesObject<GridFlow>(item,false) ) | |
{ | |
size = attached->property("sizeHint").toSizeF(); | |
} | |
if( !size.isValid() ) | |
{ | |
if( item->implicitWidth() > 0 ) | |
size.rwidth() = item->implicitWidth(); | |
else | |
size.rwidth() = m_ColumnWidth; | |
if( item->implicitHeight() > 0 ) | |
size.rheight() = item->implicitHeight(); | |
else | |
size.rheight() = m_ColumnWidth; | |
} | |
return size; | |
} | |
void GridFlow::connectLayoutItem(GridFlowAttached *item, bool connect) | |
{ | |
if( !item ) | |
return; | |
if( connect ) | |
{ | |
QObject::connect( item, &GridFlowAttached::sizeHintChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection ); | |
QObject::connect( item, &GridFlowAttached::spanningChanceChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection ); | |
QObject::connect( item, &GridFlowAttached::minimumHeightChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection ); | |
QObject::connect( item, &GridFlowAttached::maximumHeightChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection ); | |
QObject::connect( item, &GridFlowAttached::visibilityChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection ); | |
} | |
else | |
{ | |
QObject::disconnect( item, &GridFlowAttached::sizeHintChanged, this, &GridFlow::scheduleLayout ); | |
QObject::disconnect( item, &GridFlowAttached::spanningChanceChanged, this, &GridFlow::scheduleLayout ); | |
QObject::disconnect( item, &GridFlowAttached::minimumHeightChanged, this, &GridFlow::scheduleLayout ); | |
QObject::disconnect( item, &GridFlowAttached::maximumHeightChanged, this, &GridFlow::scheduleLayout ); | |
QObject::disconnect( item, &GridFlowAttached::visibilityChanged, this, &GridFlow::scheduleLayout ); | |
} | |
} | |
bool GridFlow::event(QEvent* event) | |
{ | |
switch( event->type() ) | |
{ | |
case QEvent::LayoutRequest: | |
{ | |
this->doLayout(); | |
return true; | |
} | |
break; | |
default: | |
break; | |
} | |
return QQuickItem::event(event); | |
} | |
//------------------------------------------------------ | |
GridFlowAttached::GridFlowAttached(QObject* attachee) | |
: QObject(attachee), m_SpanningChance(0.5), m_MinimumHeight(0.0), m_MaximumHeight(0.0) | |
{ | |
if( QQuickItem* item = qobject_cast<QQuickItem*>(attachee) ) | |
{ | |
m_SizeHint = QSizeF(item->width(),item->height()); | |
connect( item, &QQuickItem::visibleChanged, this, &GridFlowAttached::visibilityChanged ); | |
} | |
} | |
GridFlowAttached::~GridFlowAttached() | |
{ | |
} | |
void GridFlowAttached::setSizeHint(const QSizeF &size) | |
{ | |
if( !size.isValid() ) | |
{ | |
QtQml::qmlInfo(this) << "Cannot set invalid size: " << size; | |
return; | |
} | |
if( m_SizeHint != size ) | |
{ | |
m_SizeHint = size; | |
Q_EMIT sizeHintChanged(size); | |
} | |
} | |
void GridFlowAttached::setSpanningChance(qreal chance) | |
{ | |
if( 0.0 <= chance && chance <= 1.0 && m_SpanningChance != chance ) | |
{ | |
m_SpanningChance = chance; | |
Q_EMIT spanningChanceChanged(chance); | |
} | |
} | |
void GridFlowAttached::setMinimumHeight(qreal height) | |
{ | |
height = qMax( 0.0, height ); | |
if( m_MinimumHeight != height ) | |
{ | |
m_MinimumHeight = height; | |
Q_EMIT minimumHeightChanged( height ); | |
} | |
} | |
void GridFlowAttached::setMaximumHeight(qreal height) | |
{ | |
height = qMax( 0.0, height ); | |
if( m_MaximumHeight != height ) | |
{ | |
m_MaximumHeight = height; | |
Q_EMIT maximumHeightChanged( height ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef GRIDFLOW_HPP | |
#define GRIDFLOW_HPP | |
#include <QQuickItem> | |
class GridFlow; | |
class GridFlowAttached : public QObject | |
{ | |
Q_OBJECT | |
Q_PROPERTY( QSizeF sizeHint MEMBER m_SizeHint WRITE setSizeHint NOTIFY sizeHintChanged ) | |
Q_PROPERTY( qreal spanningChance MEMBER m_SpanningChance WRITE setSpanningChance NOTIFY spanningChanceChanged ) | |
Q_PROPERTY( qreal minimumHeight MEMBER m_MinimumHeight WRITE setMinimumHeight NOTIFY minimumHeightChanged ) | |
Q_PROPERTY( qreal maximumHeight MEMBER m_MaximumHeight WRITE setMaximumHeight NOTIFY maximumHeightChanged ) | |
friend class GridFlow; | |
public: | |
void setSizeHint( const QSizeF & size ); | |
void setSpanningChance( qreal chance ); | |
void setMinimumHeight( qreal height ); | |
void setMaximumHeight( qreal height ); | |
Q_SIGNALS: | |
void visibilityChanged(); | |
void sizeHintChanged(const QSizeF & sizeHint); | |
void spanningChanceChanged(qreal chance); | |
void minimumHeightChanged(qreal height); | |
void maximumHeightChanged(qreal height); | |
protected: | |
explicit GridFlowAttached( QObject* attachee ); | |
~GridFlowAttached(); | |
QSizeF m_SizeHint; | |
qreal m_SpanningChance; | |
qreal m_MinimumHeight; | |
qreal m_MaximumHeight; | |
}; | |
QML_DECLARE_TYPE(GridFlowAttached) | |
//-------------------------------------------------------------- | |
class GridFlow : public QQuickItem | |
{ | |
Q_OBJECT | |
QRW_ANDROID_CLASSINFO | |
Q_PROPERTY( int columnCount READ columnCount WRITE setColumnCount NOTIFY columnCountChanged ) | |
Q_PROPERTY( qreal horizontalSpacing READ horizontalSpacing WRITE setHorizontalSpacing NOTIFY horizontalSpacingChanged ) | |
Q_PROPERTY( qreal verticalSpacing READ verticalSpacing WRITE setHorizontalSpacing NOTIFY verticalSpacingChanged ) | |
Q_PROPERTY( bool spanningEnabled READ isSpanningEnabled WRITE setSpanningEnabled NOTIFY spanningEnabledChanged ) | |
Q_PROPERTY( qreal margin READ margin WRITE setMargin NOTIFY marginChanged ) | |
Q_PROPERTY( qreal rowHeight READ rowHeight WRITE setRowHeight NOTIFY rowHeightChanged ) | |
Q_PROPERTY( bool keepChildOrder READ keepChildOrder WRITE setKeepChildOrder NOTIFY keepChildOrderChanged ) | |
public: | |
explicit GridFlow(QQuickItem *parentItem = Q_NULLPTR); | |
~GridFlow(); | |
static GridFlowAttached* qmlAttachedProperties(QObject* attachee); | |
void setHorizontalSpacing(qreal spacing); | |
qreal horizontalSpacing() const; | |
void setVerticalSpacing(qreal spacing); | |
qreal verticalSpacing() const; | |
void setMargin(qreal margin); | |
qreal margin() const; | |
void setRowHeight(qreal height); | |
qreal rowHeight() const; | |
void setColumnCount(int); | |
int columnCount() const; | |
void setSpanningEnabled(bool enabled); | |
bool isSpanningEnabled() const; | |
void setKeepChildOrder(bool keepChildOrder); | |
bool keepChildOrder() const; | |
Q_SIGNALS: | |
void columnCountChanged(int count); | |
void horizontalSpacingChanged(qreal spacing); | |
void verticalSpacingChanged(qreal spacing); | |
void marginChanged(qreal margin); | |
void rowHeightChanged(qreal height); | |
void spanningEnabledChanged(bool enabled); | |
void keepChildOrderChanged(bool keepChildOrder); | |
protected Q_SLOTS: | |
void doLayout(); | |
private: | |
int getMinColumnIndex(); | |
QSizeF layoutItemSize(QQuickItem* item, GridFlowAttached* layoutItem, int rowspan) const; | |
QSizeF getInitialItemSize(QQuickItem* item) const; | |
void connectLayoutItem( GridFlowAttached* item, bool connect ); | |
protected: | |
virtual bool event(QEvent* event) Q_DECL_OVERRIDE; | |
virtual void itemChange(ItemChange change, const ItemChangeData &value) Q_DECL_OVERRIDE; | |
void scheduleLayout(); | |
void attachToParent(QQuickItem* parentItem); | |
qreal m_HorizontalSpacing; | |
qreal m_VerticalSpacing; | |
qreal m_Margin; | |
qreal m_RowHeight; | |
bool m_SpanningEnabled; | |
bool m_KeepChildOrder; | |
qreal m_ColumnWidth; | |
int m_ColumnCount; | |
QVector<qreal> m_ColumnHeights; | |
}; | |
QML_DECLARE_TYPE(GridFlow) | |
QML_DECLARE_TYPEINFO(GridFlow, QML_HAS_ATTACHED_PROPERTIES) | |
#endif // GRIDFLOW_HPP |
what exactly is your issue?
Register this class as a QML type (qmlRegisterType() method)
Example usage in QML can be found here: https://forum.qt.io/topic/81267/qrwandroid-qml-plugin/6
I want to use this with a model, following the example you linked, all the delegates appear to be in the same row:
I'm expecting it to look something like this:
This is my code:
GridFlow {
Layout.fillWidth: true
Layout.fillHeight: true
columnCount: 3
horizontalSpacing: Kirigami.Units.gridUnit
verticalSpacing: Kirigami.Units.gridUnit * 50
spanningEnabled: true
margin: Kirigami.Units.gridUnit
rowHeight: Kirigami.Units.gridUnit * 100
keepChildOrder: true
Repeater {
model: fontsModel
FontDelegate {
GridFlow.sizeHint: Qt.size(implicitWidth, implicitHeight)
GridFlow.spanningChance: 0.2
GridFlow.minimumHeight: Kirigami.Units.gridUnit * 5
GridFlow.maximumHeight: implicitHeight
}
}
}
Another thing is that it segfaults everytime I close a page in the application, not sure how they're related though...
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey, could you help getting this to work on my application?