1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquicksmoothedanimation_p.h"
5#include "qquicksmoothedanimation_p_p.h"
6
7#include "qquickanimation_p_p.h"
8#include "private/qcontinuinganimationgroupjob_p.h"
9
10#include <qmath.h>
11#include <qqmlproperty.h>
12#include <private/qqmlproperty_p.h>
13
14#include <private/qqmlglobal_p.h>
15
16#include <QtCore/qdebug.h>
17
18
19#define DELAY_STOP_TIMER_INTERVAL 32
20
21QT_BEGIN_NAMESPACE
22
23
24QSmoothedAnimationTimer::QSmoothedAnimationTimer(QSmoothedAnimation *animation, QObject *parent)
25 : QTimer(parent)
26 , m_animation(animation)
27{
28 connect(sender: this, SIGNAL(timeout()), receiver: this, SLOT(stopAnimation()));
29}
30
31QSmoothedAnimationTimer::~QSmoothedAnimationTimer()
32{
33}
34
35void QSmoothedAnimationTimer::stopAnimation()
36{
37 m_animation->stop();
38}
39
40QSmoothedAnimation::QSmoothedAnimation(QQuickSmoothedAnimationPrivate *priv)
41 : QAbstractAnimationJob(), to(0), velocity(200), userDuration(-1), maximumEasingTime(-1),
42 reversingMode(QQuickSmoothedAnimation::Eased), initialVelocity(0),
43 trackVelocity(0), initialValue(0), invert(false), finalDuration(-1), lastTime(0),
44 skipUpdate(false), delayedStopTimer(new QSmoothedAnimationTimer(this)), animationTemplate(priv)
45{
46 delayedStopTimer->setInterval(DELAY_STOP_TIMER_INTERVAL);
47 delayedStopTimer->setSingleShot(true);
48}
49
50QSmoothedAnimation::~QSmoothedAnimation()
51{
52 delete delayedStopTimer;
53 if (animationTemplate) {
54 if (target.object()) {
55 QHash<QQmlProperty, QSmoothedAnimation* >::iterator it =
56 animationTemplate->activeAnimations.find(key: target);
57 if (it != animationTemplate->activeAnimations.end() && it.value() == this)
58 animationTemplate->activeAnimations.erase(it);
59 } else {
60 //target is no longer valid, need to search linearly
61 QHash<QQmlProperty, QSmoothedAnimation* >::iterator it;
62 for (it = animationTemplate->activeAnimations.begin(); it != animationTemplate->activeAnimations.end(); ++it) {
63 if (it.value() == this) {
64 animationTemplate->activeAnimations.erase(it);
65 break;
66 }
67 }
68 }
69 }
70}
71
72void QSmoothedAnimation::restart()
73{
74 initialVelocity = trackVelocity;
75 if (isRunning())
76 init();
77 else
78 start();
79}
80
81void QSmoothedAnimation::prepareForRestart()
82{
83 initialVelocity = trackVelocity;
84 if (isRunning()) {
85 //we are joining a new wrapper group while running, our times need to be restarted
86 skipUpdate = true;
87 init();
88 lastTime = 0;
89 } else {
90 skipUpdate = false;
91 //we'll be started when the group starts, which will force an init()
92 }
93}
94
95void QSmoothedAnimation::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State /*oldState*/)
96{
97 if (newState == QAbstractAnimationJob::Running)
98 init();
99}
100
101void QSmoothedAnimation::delayedStop()
102{
103 if (!delayedStopTimer->isActive())
104 delayedStopTimer->start();
105}
106
107int QSmoothedAnimation::duration() const
108{
109 return -1;
110}
111
112bool QSmoothedAnimation::recalc()
113{
114 s = to - initialValue;
115 vi = initialVelocity;
116
117 s = (invert? -1.0: 1.0) * s;
118
119 if (userDuration >= 0 && velocity > 0) {
120 tf = s / velocity;
121 if (tf > (userDuration / 1000.)) tf = (userDuration / 1000.);
122 } else if (userDuration >= 0) {
123 tf = userDuration / 1000.;
124 } else if (velocity > 0) {
125 tf = s / velocity;
126 } else {
127 return false;
128 }
129
130 finalDuration = qCeil(v: tf * 1000.0);
131
132 if (maximumEasingTime == 0) {
133 a = 0;
134 d = 0;
135 tp = 0;
136 td = tf;
137 vp = velocity;
138 sp = 0;
139 sd = s;
140 } else if (maximumEasingTime != -1 && tf > (maximumEasingTime / 1000.)) {
141 qreal met = maximumEasingTime / 1000.;
142 /* tp| |td
143 * vp_ _______
144 * / \
145 * vi_ / \
146 * \
147 * \ _ 0
148 * |ta| |ta|
149 */
150 qreal ta = met / 2.;
151 a = (s - (vi * tf - 0.5 * vi * ta)) / (tf * ta - ta * ta);
152
153 vp = vi + a * ta;
154 d = vp / ta;
155 tp = ta;
156 sp = vi * ta + 0.5 * a * tp * tp;
157 sd = sp + vp * (tf - 2 * ta);
158 td = tf - ta;
159 } else {
160 qreal c1 = 0.25 * tf * tf;
161 qreal c2 = 0.5 * vi * tf - s;
162 qreal c3 = -0.25 * vi * vi;
163
164 qreal a1 = (-c2 + qSqrt(v: c2 * c2 - 4 * c1 * c3)) / (2. * c1);
165
166 qreal tp1 = 0.5 * tf - 0.5 * vi / a1;
167 qreal vp1 = a1 * tp1 + vi;
168
169 qreal sp1 = 0.5 * a1 * tp1 * tp1 + vi * tp1;
170
171 a = a1;
172 d = a1;
173 tp = tp1;
174 td = tp1;
175 vp = vp1;
176 sp = sp1;
177 sd = sp1;
178 }
179 return true;
180}
181
182qreal QSmoothedAnimation::easeFollow(qreal time_seconds)
183{
184 qreal value;
185 if (time_seconds < tp) {
186 trackVelocity = vi + time_seconds * a;
187 value = 0.5 * a * time_seconds * time_seconds + vi * time_seconds;
188 } else if (time_seconds < td) {
189 time_seconds -= tp;
190 trackVelocity = vp;
191 value = sp + time_seconds * vp;
192 } else if (time_seconds < tf) {
193 time_seconds -= td;
194 trackVelocity = vp - time_seconds * a;
195 value = sd - 0.5 * d * time_seconds * time_seconds + vp * time_seconds;
196 } else {
197 trackVelocity = 0;
198 value = s;
199 delayedStop();
200 }
201
202 // to normalize 's' between [0..1], divide 'value' by 's'
203 return value;
204}
205
206void QSmoothedAnimation::updateCurrentTime(int t)
207{
208 if (skipUpdate) {
209 skipUpdate = false;
210 return;
211 }
212
213 if (!isRunning() && !isPaused()) // This can happen if init() stops the animation in some cases
214 return;
215
216 qreal time_seconds = qreal(t - lastTime) / 1000.;
217
218 qreal value = easeFollow(time_seconds);
219 value *= (invert? -1.0: 1.0);
220 QQmlPropertyPrivate::write(that: target, initialValue + value,
221 QQmlPropertyData::BypassInterceptor
222 | QQmlPropertyData::DontRemoveBinding);
223}
224
225void QSmoothedAnimation::init()
226{
227 if (velocity == 0) {
228 stop();
229 return;
230 }
231
232 if (delayedStopTimer->isActive())
233 delayedStopTimer->stop();
234
235 initialValue = target.read().toReal();
236 lastTime = this->currentTime();
237
238 if (to == initialValue) {
239 stop();
240 return;
241 }
242
243 bool hasReversed = trackVelocity != 0. &&
244 ((!invert) == ((initialValue - to) > 0));
245
246 if (hasReversed) {
247 switch (reversingMode) {
248 default:
249 case QQuickSmoothedAnimation::Eased:
250 initialVelocity = -trackVelocity;
251 break;
252 case QQuickSmoothedAnimation::Sync:
253 QQmlPropertyPrivate::write(that: target, to,
254 QQmlPropertyData::BypassInterceptor
255 | QQmlPropertyData::DontRemoveBinding);
256 trackVelocity = 0;
257 stop();
258 return;
259 case QQuickSmoothedAnimation::Immediate:
260 initialVelocity = 0;
261 break;
262 }
263 }
264
265 trackVelocity = initialVelocity;
266
267 invert = (to < initialValue);
268
269 if (!recalc()) {
270 QQmlPropertyPrivate::write(that: target, to,
271 QQmlPropertyData::BypassInterceptor
272 | QQmlPropertyData::DontRemoveBinding);
273 stop();
274 return;
275 }
276}
277
278void QSmoothedAnimation::debugAnimation(QDebug d) const
279{
280 d << "SmoothedAnimationJob(" << Qt::hex << (const void *) this << Qt::dec << ")" << "duration:" << userDuration
281 << "velocity:" << velocity << "target:" << target.object() << "property:" << target.name()
282 << "to:" << to << "current velocity:" << trackVelocity;
283}
284
285/*!
286 \qmltype SmoothedAnimation
287 \instantiates QQuickSmoothedAnimation
288 \inqmlmodule QtQuick
289 \ingroup qtquick-transitions-animations
290 \inherits NumberAnimation
291 \brief Allows a property to smoothly track a value.
292
293 A SmoothedAnimation animates a property's value to a set target value
294 using an ease in/out quad easing curve. When the target value changes,
295 the easing curves used to animate between the old and new target values
296 are smoothly spliced together to create a smooth movement to the new
297 target value that maintains the current velocity.
298
299 The follow example shows one \l Rectangle tracking the position of another
300 using SmoothedAnimation. The green rectangle's \c x and \c y values are
301 bound to those of the red rectangle. Whenever these values change, the
302 green rectangle smoothly animates to its new position:
303
304 \snippet qml/smoothedanimation.qml 0
305
306 A SmoothedAnimation can be configured by setting the \l velocity at which the
307 animation should occur, or the \l duration that the animation should take.
308 If both the \l velocity and \l duration are specified, the one that results in
309 the quickest animation is chosen for each change in the target value.
310
311 For example, animating from 0 to 800 will take 4 seconds if a velocity
312 of 200 is set, will take 8 seconds with a duration of 8000 set, and will
313 take 4 seconds with both a velocity of 200 and a duration of 8000 set.
314 Animating from 0 to 20000 will take 10 seconds if a velocity of 200 is set,
315 will take 8 seconds with a duration of 8000 set, and will take 8 seconds
316 with both a velocity of 200 and a duration of 8000 set.
317
318 The default velocity of SmoothedAnimation is 200 units/second. Note that if the range of the
319 value being animated is small, then the velocity will need to be adjusted
320 appropriately. For example, the opacity of an item ranges from 0 - 1.0.
321 To enable a smooth animation in this range the velocity will need to be
322 set to a value such as 0.5 units/second. Animating from 0 to 1.0 with a velocity
323 of 0.5 will take 2000 ms to complete.
324
325 Like any other animation type, a SmoothedAnimation can be applied in a
326 number of ways, including transitions, behaviors and property value
327 sources. The \l {Animation and Transitions in Qt Quick} documentation shows a
328 variety of methods for creating animations.
329
330 \sa SpringAnimation, NumberAnimation, {Animation and Transitions in Qt Quick}, {Qt Quick Examples - Animation}
331*/
332
333QQuickSmoothedAnimation::QQuickSmoothedAnimation(QObject *parent)
334: QQuickNumberAnimation(*(new QQuickSmoothedAnimationPrivate), parent)
335{
336}
337
338QQuickSmoothedAnimationPrivate::QQuickSmoothedAnimationPrivate()
339 : anim(new QSmoothedAnimation)
340{
341}
342
343QQuickSmoothedAnimationPrivate::~QQuickSmoothedAnimationPrivate()
344{
345 typedef QHash<QQmlProperty, QSmoothedAnimation* >::iterator ActiveAnimationsHashIt;
346
347 delete anim;
348 for (ActiveAnimationsHashIt it = activeAnimations.begin(), end = activeAnimations.end(); it != end; ++it)
349 it.value()->clearTemplate();
350}
351
352void QQuickSmoothedAnimationPrivate::updateRunningAnimations()
353{
354 for (QSmoothedAnimation *ease : std::as_const(t&: activeAnimations)) {
355 ease->maximumEasingTime = anim->maximumEasingTime;
356 ease->reversingMode = anim->reversingMode;
357 ease->velocity = anim->velocity;
358 ease->userDuration = anim->userDuration;
359 ease->init();
360 }
361}
362
363QAbstractAnimationJob* QQuickSmoothedAnimation::transition(QQuickStateActions &actions,
364 QQmlProperties &modified,
365 TransitionDirection direction,
366 QObject *defaultTarget)
367{
368 Q_UNUSED(direction);
369 Q_D(QQuickSmoothedAnimation);
370
371 const QQuickStateActions dataActions = QQuickPropertyAnimation::createTransitionActions(actions, modified, defaultTarget);
372
373 QContinuingAnimationGroupJob *wrapperGroup = new QContinuingAnimationGroupJob();
374
375 if (!dataActions.isEmpty()) {
376 QSet<QAbstractAnimationJob*> anims;
377 for (int i = 0; i < dataActions.size(); i++) {
378 QSmoothedAnimation *ease;
379 bool isActive;
380 if (!d->activeAnimations.contains(key: dataActions[i].property)) {
381 ease = new QSmoothedAnimation(d);
382 d->activeAnimations.insert(key: dataActions[i].property, value: ease);
383 ease->target = dataActions[i].property;
384 isActive = false;
385 } else {
386 ease = d->activeAnimations.value(key: dataActions[i].property);
387 isActive = true;
388 }
389 wrapperGroup->appendAnimation(animation: initInstance(animation: ease));
390
391 ease->to = dataActions[i].toValue.toReal();
392
393 // copying public members from main value holder animation
394 ease->maximumEasingTime = d->anim->maximumEasingTime;
395 ease->reversingMode = d->anim->reversingMode;
396 ease->velocity = d->anim->velocity;
397 ease->userDuration = d->anim->userDuration;
398
399 ease->initialVelocity = ease->trackVelocity;
400
401 if (isActive)
402 ease->prepareForRestart();
403 anims.insert(value: ease);
404 }
405
406 const auto copy = d->activeAnimations;
407 for (QSmoothedAnimation *ease : copy) {
408 if (!anims.contains(value: ease)) {
409 ease->clearTemplate();
410 d->activeAnimations.remove(key: ease->target);
411 }
412 }
413 }
414 return wrapperGroup;
415}
416
417/*!
418 \qmlproperty enumeration QtQuick::SmoothedAnimation::reversingMode
419
420 Sets how the SmoothedAnimation behaves if an animation direction is reversed.
421
422 Possible values are:
423
424 \value SmoothedAnimation.Eased
425 (default) the animation will smoothly decelerate, and then reverse direction
426 \value SmoothedAnimation.Immediate
427 the animation will immediately begin accelerating in the reverse direction, beginning with a velocity of 0
428 \value SmoothedAnimation.Sync
429 the property is immediately set to the target value
430*/
431QQuickSmoothedAnimation::ReversingMode QQuickSmoothedAnimation::reversingMode() const
432{
433 Q_D(const QQuickSmoothedAnimation);
434 return (QQuickSmoothedAnimation::ReversingMode) d->anim->reversingMode;
435}
436
437void QQuickSmoothedAnimation::setReversingMode(ReversingMode m)
438{
439 Q_D(QQuickSmoothedAnimation);
440 if (d->anim->reversingMode == m)
441 return;
442
443 d->anim->reversingMode = m;
444 emit reversingModeChanged();
445 d->updateRunningAnimations();
446}
447
448/*!
449 \qmlproperty int QtQuick::SmoothedAnimation::duration
450
451 This property holds the animation duration, in msecs, used when tracking the source.
452
453 Setting this to -1 (the default) disables the duration value.
454
455 If the velocity value and the duration value are both enabled, then the animation will
456 use whichever gives the shorter duration.
457*/
458int QQuickSmoothedAnimation::duration() const
459{
460 Q_D(const QQuickSmoothedAnimation);
461 return d->anim->userDuration;
462}
463
464void QQuickSmoothedAnimation::setDuration(int duration)
465{
466 Q_D(QQuickSmoothedAnimation);
467 if (duration != -1)
468 QQuickNumberAnimation::setDuration(duration);
469 if(duration == d->anim->userDuration)
470 return;
471 d->anim->userDuration = duration;
472 d->updateRunningAnimations();
473}
474
475qreal QQuickSmoothedAnimation::velocity() const
476{
477 Q_D(const QQuickSmoothedAnimation);
478 return d->anim->velocity;
479}
480
481/*!
482 \qmlproperty real QtQuick::SmoothedAnimation::velocity
483
484 This property holds the average velocity allowed when tracking the 'to' value.
485
486 The default velocity of SmoothedAnimation is 200 units/second.
487
488 Setting this to -1 disables the velocity value.
489
490 If the velocity value and the duration value are both enabled, then the animation will
491 use whichever gives the shorter duration.
492*/
493void QQuickSmoothedAnimation::setVelocity(qreal v)
494{
495 Q_D(QQuickSmoothedAnimation);
496 if (d->anim->velocity == v)
497 return;
498
499 d->anim->velocity = v;
500 emit velocityChanged();
501 d->updateRunningAnimations();
502}
503
504/*!
505 \qmlproperty int QtQuick::SmoothedAnimation::maximumEasingTime
506
507 This property specifies the maximum time, in msecs, any "eases" during the follow should take.
508 Setting this property causes the velocity to "level out" after at a time. Setting
509 a negative value reverts to the normal mode of easing over the entire animation
510 duration.
511
512 The default value is -1.
513*/
514int QQuickSmoothedAnimation::maximumEasingTime() const
515{
516 Q_D(const QQuickSmoothedAnimation);
517 return d->anim->maximumEasingTime;
518}
519
520void QQuickSmoothedAnimation::setMaximumEasingTime(int v)
521{
522 Q_D(QQuickSmoothedAnimation);
523 if(v == d->anim->maximumEasingTime)
524 return;
525 d->anim->maximumEasingTime = v;
526 emit maximumEasingTimeChanged();
527 d->updateRunningAnimations();
528}
529
530QT_END_NAMESPACE
531
532#include "moc_qquicksmoothedanimation_p_p.cpp"
533
534#include "moc_qquicksmoothedanimation_p.cpp"
535

source code of qtdeclarative/src/quick/util/qquicksmoothedanimation.cpp