fix: layout delegates after configurable delay
Fixes glitches due to delegate sizes changing on creation Also use the loader state pattern to ensure size is set on same frame as active
This commit is contained in:
parent
e6c1248bef
commit
2efe1a93a6
5 changed files with 158 additions and 38 deletions
|
|
@ -155,13 +155,51 @@ StyledRect {
|
|||
}
|
||||
|
||||
component WrappedLoader: Loader {
|
||||
id: comp
|
||||
|
||||
required property bool shouldBeActive
|
||||
|
||||
opacity: shouldBeActive ? 1 : 0
|
||||
active: opacity > 0
|
||||
active: false
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
||||
states: State {
|
||||
name: "active"
|
||||
when: comp.shouldBeActive
|
||||
|
||||
PropertyChanges {
|
||||
comp.opacity: 1
|
||||
comp.active: true
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "active"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
}
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ LazyListView {
|
|||
implicitHeight: contentHeight
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
readyDelay: 1
|
||||
cacheBuffer: 400
|
||||
asynchronous: true
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ LazyListView {
|
|||
spacing: Math.round(Appearance.spacing.small / 2)
|
||||
asynchronous: true
|
||||
|
||||
readyDelay: 1
|
||||
cacheBuffer: 800
|
||||
removeDuration: Appearance.anim.durations.normal
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,17 @@ void LazyListViewAttached::setVisibleHeight(qreal height) {
|
|||
emit visibleHeightChanged();
|
||||
}
|
||||
|
||||
bool LazyListViewAttached::ready() const {
|
||||
return m_ready;
|
||||
}
|
||||
|
||||
void LazyListViewAttached::setReady(bool ready) {
|
||||
if (m_ready == ready)
|
||||
return;
|
||||
m_ready = ready;
|
||||
emit readyChanged();
|
||||
}
|
||||
|
||||
bool LazyListViewAttached::adding() const {
|
||||
return m_adding;
|
||||
}
|
||||
|
|
@ -268,7 +279,14 @@ qreal LazyListView::delegateVisibleHeight(QQuickItem* item) {
|
|||
return item->implicitHeight();
|
||||
}
|
||||
|
||||
// --- Remove Animation ---
|
||||
bool LazyListView::isDelegateReady(QQuickItem* item) {
|
||||
if (!item)
|
||||
return false;
|
||||
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false));
|
||||
return !att || att->ready();
|
||||
}
|
||||
|
||||
// --- Animation Durations ---
|
||||
|
||||
int LazyListView::removeDuration() const {
|
||||
return m_removeDuration;
|
||||
|
|
@ -281,6 +299,17 @@ void LazyListView::setRemoveDuration(int duration) {
|
|||
emit removeDurationChanged();
|
||||
}
|
||||
|
||||
int LazyListView::readyDelay() const {
|
||||
return m_readyDelay;
|
||||
}
|
||||
|
||||
void LazyListView::setReadyDelay(int delay) {
|
||||
if (m_readyDelay == delay)
|
||||
return;
|
||||
m_readyDelay = delay;
|
||||
emit readyDelayChanged();
|
||||
}
|
||||
|
||||
// --- State ---
|
||||
|
||||
int LazyListView::count() const {
|
||||
|
|
@ -315,12 +344,32 @@ void LazyListView::updatePolish() {
|
|||
if (!m_componentComplete || !m_model || !m_delegate)
|
||||
return;
|
||||
|
||||
// Flush pending inserts from the previous frame — make items visible
|
||||
// and clear the adding flag so enter animations begin.
|
||||
for (auto& entry : m_delegates) {
|
||||
if (!entry.pendingInsert || !entry.item)
|
||||
continue;
|
||||
entry.pendingInsert = false;
|
||||
entry.item->setVisible(true);
|
||||
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
|
||||
if (att) {
|
||||
att->setAdding(false);
|
||||
if (m_readyDelay > 0) {
|
||||
QTimer::singleShot(m_readyDelay, att, [att] {
|
||||
att->setReady(true);
|
||||
});
|
||||
} else {
|
||||
att->setReady(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
relayout();
|
||||
syncDelegates();
|
||||
|
||||
// Position delegates — QML Behavior on y handles the animation
|
||||
for (auto& entry : m_delegates) {
|
||||
if (!entry.item || entry.pendingRemoval)
|
||||
if (!entry.item || entry.pendingRemoval || entry.pendingInsert)
|
||||
continue;
|
||||
|
||||
const int idx = entry.modelIndex;
|
||||
|
|
@ -523,32 +572,15 @@ void LazyListView::syncDelegates() {
|
|||
// Batch create
|
||||
const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast<int>(toCreate.size());
|
||||
int created = 0;
|
||||
bool layoutChanged = false;
|
||||
for (int i : toCreate) {
|
||||
if (created >= createBudget)
|
||||
break;
|
||||
|
||||
auto entry = createDelegate(i);
|
||||
if (entry.item) {
|
||||
const qreal h = delegateHeight(entry.item);
|
||||
if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height + 1.0, h + 1.0)) {
|
||||
const qreal oldLayoutH = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight();
|
||||
if (m_layout[i].heightKnown)
|
||||
untrackHeight(m_layout[i].height);
|
||||
m_layout[i].height = h;
|
||||
m_layout[i].heightKnown = true;
|
||||
trackHeight(h);
|
||||
|
||||
// Compensate if tracked item materializes above viewport
|
||||
auto* att =
|
||||
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
|
||||
if (att && att->trackViewport()) {
|
||||
const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY;
|
||||
if (m_layout[i].targetY < vpTop)
|
||||
emit viewportAdjustNeeded(h - oldLayoutH);
|
||||
}
|
||||
layoutChanged = true;
|
||||
}
|
||||
// Height tracking and viewport compensation are deferred
|
||||
// until the delegate signals ready via readyChanged.
|
||||
entry.pendingInsert = true;
|
||||
entry.item->setY(m_layout[i].targetY - m_contentY);
|
||||
m_itemToIndex.insert(entry.item, i);
|
||||
m_delegates.insert(i, std::move(entry));
|
||||
|
|
@ -556,12 +588,10 @@ void LazyListView::syncDelegates() {
|
|||
}
|
||||
}
|
||||
|
||||
if (layoutChanged)
|
||||
relayout();
|
||||
|
||||
// If async and there's remaining work, schedule another pass
|
||||
if (m_asynchronous &&
|
||||
(destroyed < static_cast<int>(toRemove.size()) || created < static_cast<int>(toCreate.size())))
|
||||
// Pending inserts need to become visible on the next frame, and
|
||||
// async mode may have remaining create/destroy work.
|
||||
if (created > 0 || (m_asynchronous && (destroyed < static_cast<int>(toRemove.size()) ||
|
||||
created < static_cast<int>(toCreate.size()))))
|
||||
polish();
|
||||
}
|
||||
|
||||
|
|
@ -616,7 +646,7 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
|
|||
entry.item->setWidth(width());
|
||||
|
||||
// Set adding = true before completeCreate so bindings see it during initial evaluation.
|
||||
// Cleared after creation so the transition from true→false triggers QML Behaviors.
|
||||
// Cleared on the next frame in updatePolish when the item becomes visible.
|
||||
auto* addingAttached =
|
||||
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, true));
|
||||
if (addingAttached)
|
||||
|
|
@ -624,11 +654,14 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
|
|||
|
||||
m_delegate->completeCreate();
|
||||
|
||||
if (addingAttached)
|
||||
addingAttached->setAdding(false);
|
||||
// Keep adding=true and hide — flushed on the next frame in updatePolish
|
||||
entry.item->setVisible(false);
|
||||
|
||||
// Height-change handler — uses m_itemToIndex for O(1) lookup
|
||||
// Height-change handler — uses m_itemToIndex for O(1) lookup.
|
||||
// Ignored while the delegate is not yet ready.
|
||||
auto onHeightChanged = [this, item = entry.item] {
|
||||
if (!isDelegateReady(item))
|
||||
return;
|
||||
auto indexIt = m_itemToIndex.find(item);
|
||||
if (indexIt == m_itemToIndex.end())
|
||||
return;
|
||||
|
|
@ -678,6 +711,33 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) {
|
|||
connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] {
|
||||
polish();
|
||||
});
|
||||
connect(attached, &LazyListViewAttached::readyChanged, this, [this, item = entry.item] {
|
||||
auto indexIt = m_itemToIndex.find(item);
|
||||
if (indexIt == m_itemToIndex.end())
|
||||
return;
|
||||
const int idx = indexIt.value();
|
||||
if (idx >= static_cast<int>(m_layout.size()))
|
||||
return;
|
||||
auto* att = qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(item, false));
|
||||
if (!att || !att->ready())
|
||||
return;
|
||||
|
||||
const qreal h = delegateHeight(item);
|
||||
const qreal oldLayoutH = m_layout[idx].heightKnown ? m_layout[idx].height : effectiveEstimatedHeight();
|
||||
if (m_layout[idx].heightKnown)
|
||||
untrackHeight(m_layout[idx].height);
|
||||
m_layout[idx].height = h;
|
||||
m_layout[idx].heightKnown = true;
|
||||
trackHeight(h);
|
||||
|
||||
if (att->trackViewport() && !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) {
|
||||
const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY;
|
||||
if (m_layout[idx].targetY < vpTop)
|
||||
emit viewportAdjustNeeded(h - oldLayoutH);
|
||||
}
|
||||
|
||||
polish();
|
||||
});
|
||||
}
|
||||
|
||||
return entry;
|
||||
|
|
@ -818,6 +878,12 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first,
|
|||
m_itemToIndex.remove(entry.item);
|
||||
entry.pendingRemoval = true;
|
||||
|
||||
// Never made visible — skip remove animation
|
||||
if (entry.pendingInsert) {
|
||||
destroyDelegate(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_removeDuration > 0 && entry.item) {
|
||||
auto* attached =
|
||||
qobject_cast<LazyListViewAttached*>(qmlAttachedPropertiesObject<LazyListView>(entry.item, false));
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class LazyListViewAttached : public QObject {
|
|||
|
||||
Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged)
|
||||
Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged)
|
||||
Q_PROPERTY(bool ready READ ready NOTIFY readyChanged)
|
||||
Q_PROPERTY(bool adding READ adding NOTIFY addingChanged)
|
||||
Q_PROPERTY(bool removing READ removing NOTIFY removingChanged)
|
||||
Q_PROPERTY(bool trackViewport READ trackViewport WRITE setTrackViewport NOTIFY trackViewportChanged)
|
||||
|
|
@ -29,6 +30,9 @@ public:
|
|||
[[nodiscard]] qreal visibleHeight() const;
|
||||
void setVisibleHeight(qreal height);
|
||||
|
||||
[[nodiscard]] bool ready() const;
|
||||
void setReady(bool ready);
|
||||
|
||||
[[nodiscard]] bool adding() const;
|
||||
void setAdding(bool adding);
|
||||
|
||||
|
|
@ -41,6 +45,7 @@ public:
|
|||
signals:
|
||||
void preferredHeightChanged();
|
||||
void visibleHeightChanged();
|
||||
void readyChanged();
|
||||
void addingChanged();
|
||||
void removingChanged();
|
||||
void trackViewportChanged();
|
||||
|
|
@ -48,6 +53,7 @@ signals:
|
|||
private:
|
||||
qreal m_preferredHeight = -1;
|
||||
qreal m_visibleHeight = -1;
|
||||
bool m_ready = false;
|
||||
bool m_adding = false;
|
||||
bool m_removing = false;
|
||||
bool m_trackViewport = false;
|
||||
|
|
@ -79,8 +85,9 @@ class LazyListView : public QQuickItem {
|
|||
// Async
|
||||
Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged)
|
||||
|
||||
// Remove Animation
|
||||
// Animation Durations
|
||||
Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged)
|
||||
Q_PROPERTY(int readyDelay READ readyDelay WRITE setReadyDelay NOTIFY readyDelayChanged)
|
||||
|
||||
// State
|
||||
Q_PROPERTY(int count READ count NOTIFY countChanged)
|
||||
|
|
@ -126,10 +133,13 @@ public:
|
|||
[[nodiscard]] bool asynchronous() const;
|
||||
void setAsynchronous(bool async);
|
||||
|
||||
// Remove Animation
|
||||
// Animation Durations
|
||||
[[nodiscard]] int removeDuration() const;
|
||||
void setRemoveDuration(int duration);
|
||||
|
||||
[[nodiscard]] int readyDelay() const;
|
||||
void setReadyDelay(int delay);
|
||||
|
||||
// State
|
||||
[[nodiscard]] int count() const;
|
||||
signals:
|
||||
|
|
@ -145,6 +155,7 @@ signals:
|
|||
void estimatedHeightChanged();
|
||||
void asynchronousChanged();
|
||||
void removeDurationChanged();
|
||||
void readyDelayChanged();
|
||||
void countChanged();
|
||||
void viewportAdjustNeeded(qreal delta);
|
||||
|
||||
|
|
@ -164,6 +175,7 @@ private:
|
|||
int modelIndex = -1;
|
||||
QQuickItem* item = nullptr;
|
||||
bool pendingRemoval = false;
|
||||
bool pendingInsert = false;
|
||||
};
|
||||
|
||||
// Layout
|
||||
|
|
@ -173,6 +185,7 @@ private:
|
|||
[[nodiscard]] qreal effectiveEstimatedHeight() const;
|
||||
[[nodiscard]] static qreal delegateHeight(QQuickItem* item);
|
||||
[[nodiscard]] static qreal delegateVisibleHeight(QQuickItem* item);
|
||||
[[nodiscard]] static bool isDelegateReady(QQuickItem* item);
|
||||
void trackHeight(qreal height);
|
||||
void untrackHeight(qreal height);
|
||||
|
||||
|
|
@ -212,6 +225,7 @@ private:
|
|||
bool m_asynchronous = false;
|
||||
|
||||
int m_removeDuration = 300;
|
||||
int m_readyDelay = 0;
|
||||
|
||||
QVector<ItemRecord> m_layout;
|
||||
QHash<int, DelegateEntry> m_delegates;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue