aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMitch Curtis <mitch.curtis@qt.io>2022-04-08 17:45:41 +0800
committerMitch Curtis <mitch.curtis@qt.io>2022-04-28 12:12:50 +0800
commit44dd79eedae174af3474f6317327a09af0de177a (patch)
tree93377fb97dba547d2dfaa9fef4bfff5acbf1b05e
parent7b827225f2a5a28fea42d7e0c2113182f510ed3e (diff)
Allow modifying from, to, duration and easing properties during animation
The main motivation for this is convenience, but it also allows controls to respond to resizing for items that have animations without resorting to JavaScript to call restart(). [ChangeLog][QtQuick] It is now possible to set the from, to, duration, and easing properties of a top-level animation while it is running. The animation will take the changes into account on the next loop. Change-Id: I2f560ee713666e67b7162d95ff28621fcf3b2545 Fixes: QTBUG-38932 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
-rw-r--r--src/qml/animations/qabstractanimationjob.cpp7
-rw-r--r--src/quick/util/qquickanimation.cpp46
-rw-r--r--src/quick/util/qquickanimation_p_p.h5
-rw-r--r--tests/auto/quick/qquickanimations/data/changePropertiesDuringAnimation.qml56
-rw-r--r--tests/auto/quick/qquickanimations/tst_qquickanimations.cpp109
5 files changed, 219 insertions, 4 deletions
diff --git a/src/qml/animations/qabstractanimationjob.cpp b/src/qml/animations/qabstractanimationjob.cpp
index 05cf786e7d..b547129d33 100644
--- a/src/qml/animations/qabstractanimationjob.cpp
+++ b/src/qml/animations/qabstractanimationjob.cpp
@@ -514,8 +514,11 @@ void QAbstractAnimationJob::setCurrentTime(int msecs)
RETURN_IF_DELETED(updateCurrentTime(m_currentTime));
- if (m_currentLoop != oldLoop)
- currentLoopChanged();
+ if (m_currentLoop != oldLoop) {
+ // CurrentLoop listeners may restart the job if e.g. from has changed. Stopping a job will
+ // destroy it, so account for that here.
+ RETURN_IF_DELETED(currentLoopChanged());
+ }
// All animations are responsible for stopping the animation when their
// own end state is reached; in this case the animation is time driven,
diff --git a/src/quick/util/qquickanimation.cpp b/src/quick/util/qquickanimation.cpp
index 0a5e7f2b3e..9962313f26 100644
--- a/src/quick/util/qquickanimation.cpp
+++ b/src/quick/util/qquickanimation.cpp
@@ -173,7 +173,8 @@ void QQuickAbstractAnimationPrivate::commence()
if (animationInstance) {
if (q->threadingModel() == QQuickAbstractAnimation::RenderThread)
animationInstance = new QQuickAnimatorProxyJob(animationInstance, q);
- animationInstance->addAnimationChangeListener(this, QAbstractAnimationJob::Completion);
+ animationInstance->addAnimationChangeListener(this,
+ QAbstractAnimationJob::Completion | QAbstractAnimationJob::CurrentLoop);
emit q->started();
animationInstance->start();
}
@@ -1950,6 +1951,35 @@ QAbstractAnimationJob* QQuickParallelAnimation::transition(QQuickStateActions &a
return initInstance(ag);
}
+void QQuickPropertyAnimationPrivate::animationCurrentLoopChanged(QAbstractAnimationJob *)
+{
+ Q_Q(QQuickPropertyAnimation);
+ // We listen to current loop changes in order to restart the animation if e.g. from, to, etc.
+ // are modified while the animation is running.
+ // Restarting is a bit drastic but there is a lot of stuff that commence() (and therefore
+ // QQuickPropertyAnimation::transition() and QQuickPropertyAnimation::createTransitionActions())
+ // does, so we want to avoid trying to take a shortcut and just restart the whole thing.
+ if (ourPropertiesDirty) {
+ ourPropertiesDirty = false;
+
+ // We use animationInstance everywhere for simplicity - if we defined the job parameter
+ // it would be deleted as soon as we call stop().
+ Q_ASSERT(animationInstance);
+ const int currentLoop = animationInstance->currentLoop();
+
+ QSignalBlocker signalBlocker(q);
+ q->stop();
+ q->start();
+
+ Q_ASSERT(animationInstance);
+ // We multiply it ourselves here instead of just saving currentTime(), because otherwise
+ // it seems to accumulate, and changing our properties while the animation is running
+ // can result in the animation starting mid-way through a loop, which is not we want;
+ // we want it to start from the beginning.
+ animationInstance->setCurrentTime(currentLoop * animationInstance->duration());
+ }
+}
+
//convert a variant from string type to another animatable type
void QQuickPropertyAnimationPrivate::convertVariant(QVariant &variant, QMetaType type)
{
@@ -2089,6 +2119,12 @@ void QQuickBulkValueAnimator::debugAnimation(QDebug d) const
Note that PropertyAnimation inherits the abstract \l Animation type.
This includes additional properties and methods for controlling the animation.
+ \section1 Modifying Properties Duration Animations
+
+ Since Qt 6.4, it is possible to set the \l from, \l to, \l duration, and
+ \l easing properties on a top-level animation while it is running. The
+ animation will take the changes into account on the next loop.
+
\sa {Animation and Transitions in Qt Quick}, {Qt Quick Examples - Animation}
*/
@@ -2129,6 +2165,8 @@ void QQuickPropertyAnimation::setDuration(int duration)
if (d->duration == duration)
return;
d->duration = duration;
+ if (d->componentComplete && d->running)
+ d->ourPropertiesDirty = true;
emit durationChanged(duration);
}
@@ -2156,6 +2194,8 @@ void QQuickPropertyAnimation::setFrom(const QVariant &f)
return;
d->from = f;
d->fromIsDefined = f.isValid();
+ if (d->componentComplete && d->running)
+ d->ourPropertiesDirty = true;
emit fromChanged();
}
@@ -2183,6 +2223,8 @@ void QQuickPropertyAnimation::setTo(const QVariant &t)
return;
d->to = t;
d->toIsDefined = t.isValid();
+ if (d->componentComplete && d->running)
+ d->ourPropertiesDirty = true;
emit toChanged();
}
@@ -2413,6 +2455,8 @@ void QQuickPropertyAnimation::setEasing(const QEasingCurve &e)
return;
d->easing = e;
+ if (d->componentComplete && d->running)
+ d->ourPropertiesDirty = true;
emit easingChanged(e);
}
diff --git a/src/quick/util/qquickanimation_p_p.h b/src/quick/util/qquickanimation_p_p.h
index d8d2c9aa33..c82cffff9e 100644
--- a/src/quick/util/qquickanimation_p_p.h
+++ b/src/quick/util/qquickanimation_p_p.h
@@ -270,9 +270,11 @@ class QQuickPropertyAnimationPrivate : public QQuickAbstractAnimationPrivate
Q_DECLARE_PUBLIC(QQuickPropertyAnimation)
public:
QQuickPropertyAnimationPrivate()
- : QQuickAbstractAnimationPrivate(), target(nullptr), fromIsDefined(false), toIsDefined(false),
+ : QQuickAbstractAnimationPrivate(), target(nullptr), fromIsDefined(false), toIsDefined(false), ourPropertiesDirty(false),
defaultToInterpolatorType(0), interpolatorType(0), interpolator(nullptr), duration(250), actions(nullptr) {}
+ void animationCurrentLoopChanged(QAbstractAnimationJob *job) override;
+
QVariant from;
QVariant to;
@@ -285,6 +287,7 @@ public:
bool fromIsDefined:1;
bool toIsDefined:1;
+ bool ourPropertiesDirty : 1;
bool defaultToInterpolatorType:1;
int interpolatorType;
QVariantAnimation::Interpolator interpolator;
diff --git a/tests/auto/quick/qquickanimations/data/changePropertiesDuringAnimation.qml b/tests/auto/quick/qquickanimations/data/changePropertiesDuringAnimation.qml
new file mode 100644
index 0000000000..2e19cb2692
--- /dev/null
+++ b/tests/auto/quick/qquickanimations/data/changePropertiesDuringAnimation.qml
@@ -0,0 +1,56 @@
+/****************************************************************************
+**
+** Copyright (C) 2022 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the test suite of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick
+import QtQuick.Window 2.2
+
+Rectangle {
+ id: root
+ width: 200
+ height: 200
+ visible: true
+
+ property alias rect: rect
+ property alias numberAnimation: numberAnimation
+ property alias loops: numberAnimation.loops
+
+ Rectangle {
+ id: rect
+ width: 100
+ height: 10
+ color: "tomato"
+
+ NumberAnimation on x {
+ id: numberAnimation
+ from: -rect.width
+ to: root.width
+ duration: 60
+ easing.type: Easing.Linear
+ }
+ }
+}
diff --git a/tests/auto/quick/qquickanimations/tst_qquickanimations.cpp b/tests/auto/quick/qquickanimations/tst_qquickanimations.cpp
index 033d86f3da..10c8fb3487 100644
--- a/tests/auto/quick/qquickanimations/tst_qquickanimations.cpp
+++ b/tests/auto/quick/qquickanimations/tst_qquickanimations.cpp
@@ -117,6 +117,8 @@ private slots:
void opacityAnimationFromZero();
void alwaysRunToEndInSequentialAnimationBug();
void cleanupWhenRenderThreadStops();
+ void changePropertiesDuringAnimation_data();
+ void changePropertiesDuringAnimation();
};
#define QTIMED_COMPARE(lhs, rhs) do { \
@@ -2011,6 +2013,113 @@ void tst_qquickanimations::cleanupWhenRenderThreadStops()
QVERIFY(QTest::qWaitForWindowExposed(&view));
}
+// This will be called each frame and should return true for the test to pass.
+typedef std::function<bool(QQuickItem *, QString &)> PropertyValidatorFunc;
+Q_DECLARE_METATYPE(PropertyValidatorFunc)
+
+void tst_qquickanimations::changePropertiesDuringAnimation_data()
+{
+ QTest::addColumn<int>("loops");
+ QTest::addColumn<QString>("propertyName");
+ QTest::addColumn<qreal>("newValue");
+ QTest::addColumn<PropertyValidatorFunc>("propertyValidatorFunc");
+
+ // Use a value large enough to ensure that the animation is running for the duration of
+ // the test. We test both infinite and non-infinite loop counts.
+ const int largeLoopCount = 100;
+
+ const auto fromValidator = PropertyValidatorFunc([](QQuickItem *rect, QString &failureMessage){
+ if (rect->x() >= 0)
+ return true;
+ QDebug(&failureMessage) << "Expected x of rect to never go below new \"from\" value of 0, but it's" << rect->x();
+ return false;
+ });
+ QTest::newRow("from") << largeLoopCount << "from" << 0.0 << fromValidator;
+ QTest::newRow("from,infinite") << int(QQuickAbstractAnimation::Infinite) << "from" << 0.0 << fromValidator;
+
+ const auto toValidator = PropertyValidatorFunc([](QQuickItem *rect, QString &failureMessage){
+ if (rect->x() <= 100)
+ return true;
+ QDebug(&failureMessage) << "Expected x of rect to never go above new \"to\" value of 100, but it's" << rect->x();
+ return false;
+ });
+ QTest::newRow("to") << largeLoopCount << "to" << 100.0 << toValidator;
+ QTest::newRow("to,infinite") << int(QQuickAbstractAnimation::Infinite) << "to" << 100.0 << toValidator;
+
+ // Duration and easing.type would be difficult/flaky to test in CI so they're left out here.
+}
+
+// Tests that changing a NumberAnimation's properties while it's running will result
+// in those changes being picked up on the next loop. This is new behavior introduced
+// in Qt 6.4.
+void tst_qquickanimations::changePropertiesDuringAnimation()
+{
+ QFETCH(int, loops);
+ QFETCH(QString, propertyName);
+ QFETCH(qreal, newValue);
+ QFETCH(PropertyValidatorFunc, propertyValidatorFunc);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("changePropertiesDuringAnimation.qml"));
+ QScopedPointer<QQuickItem> rootItem(qobject_cast<QQuickItem*>(component.createWithInitialProperties({{ "loops", loops }})));
+ QVERIFY2(!component.isError(), qPrintable(component.errorString()));
+
+ auto numberAnimation = rootItem->property("numberAnimation").value<QQuickNumberAnimation*>();
+ QVERIFY(numberAnimation);
+ QCOMPARE(numberAnimation->from(), -100);
+ QCOMPARE(numberAnimation->to(), rootItem->width());
+
+ // Start the animation.
+ numberAnimation->start();
+ QVERIFY(numberAnimation->isRunning());
+
+ int loopCountBeforeModification = 0;
+ // Ensure that it's past the first loop so that we can check that it resumes
+ // from that loop after "restarting".
+ QTRY_VERIFY(numberAnimation->qtAnimation()->currentLoop() >= 1);
+ loopCountBeforeModification = numberAnimation->qtAnimation()->currentLoop();
+
+ QSignalSpy startedSpy(numberAnimation, SIGNAL(started()));
+ QVERIFY(startedSpy.isValid());
+ QSignalSpy stoppedSpy(numberAnimation, SIGNAL(stopped()));
+ QVERIFY(stoppedSpy.isValid());
+
+ // Modify the property.
+ // QQuickPropertyAnimation has a setProperty function of its own, and we don't want to call it, hence the cast.
+ QVERIFY(static_cast<QObject*>(numberAnimation)->setProperty(propertyName.toLatin1().constData(), QVariant(newValue)));
+
+ // Make sure we've reached the end of the animation.
+ auto rect = rootItem->property("rect").value<QQuickItem*>();
+ QVERIFY(rect);
+ // Ensure that we've passed the loop on which we modified the property, while also checking
+ // that currentLoop never gets reset to 0. We can't just use QTRY_VERIFY
+ // for this, because it could start at 0 and then pass loopCountBeforeModification;
+ // we need to ensure that it never goes below loopCountBeforeModification.
+ while (numberAnimation->qtAnimation()->currentLoop() < loopCountBeforeModification + 1) {
+ QVERIFY2(numberAnimation->qtAnimation()->currentLoop() >= loopCountBeforeModification,
+ qPrintable(QString::fromLatin1("Expected currentLoop to be larger than %1, but it's %2")
+ .arg(loopCountBeforeModification).arg(numberAnimation->qtAnimation()->currentLoop())));
+ QTest::qWait(0);
+ }
+
+ // Now that we know the modification should have been taken into account,
+ // check that the animated property never gets set to a value that we wouldn't expect after the change.
+ const int previousLoop = numberAnimation->qtAnimation()->currentLoop();
+ QString failureMessage;
+ while (numberAnimation->qtAnimation()->currentLoop() < previousLoop + 1) {
+ if (!propertyValidatorFunc(rect, failureMessage))
+ QFAIL(qPrintable(failureMessage));
+ QTest::qWait(0);
+ }
+
+ // The started and stopped signals should not be emitted when adapting to changes
+ // mid-animation.
+ if (loops != QQuickAbstractAnimation::Infinite)
+ QVERIFY(numberAnimation->qtAnimation()->currentLoop() < numberAnimation->loops());
+ QCOMPARE(startedSpy.count(), 0);
+ QCOMPARE(stoppedSpy.count(), 0);
+}
+
QTEST_MAIN(tst_qquickanimations)
#include "tst_qquickanimations.moc"