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 | |
21 | QT_BEGIN_NAMESPACE |
22 | |
23 | |
24 | QSmoothedAnimationTimer::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 | |
31 | QSmoothedAnimationTimer::~QSmoothedAnimationTimer() |
32 | { |
33 | } |
34 | |
35 | void QSmoothedAnimationTimer::stopAnimation() |
36 | { |
37 | m_animation->stop(); |
38 | } |
39 | |
40 | QSmoothedAnimation::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 | |
50 | QSmoothedAnimation::~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 | |
72 | void QSmoothedAnimation::restart() |
73 | { |
74 | initialVelocity = trackVelocity; |
75 | if (isRunning()) |
76 | init(); |
77 | else |
78 | start(); |
79 | } |
80 | |
81 | void 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 | |
95 | void QSmoothedAnimation::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State /*oldState*/) |
96 | { |
97 | if (newState == QAbstractAnimationJob::Running) |
98 | init(); |
99 | } |
100 | |
101 | void QSmoothedAnimation::delayedStop() |
102 | { |
103 | if (!delayedStopTimer->isActive()) |
104 | delayedStopTimer->start(); |
105 | } |
106 | |
107 | int QSmoothedAnimation::duration() const |
108 | { |
109 | return -1; |
110 | } |
111 | |
112 | bool 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 | |
182 | qreal 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 | |
206 | void 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 | |
225 | void 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 | |
278 | void 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 | |
333 | QQuickSmoothedAnimation::QQuickSmoothedAnimation(QObject *parent) |
334 | : QQuickNumberAnimation(*(new QQuickSmoothedAnimationPrivate), parent) |
335 | { |
336 | } |
337 | |
338 | QQuickSmoothedAnimationPrivate::QQuickSmoothedAnimationPrivate() |
339 | : anim(new QSmoothedAnimation) |
340 | { |
341 | } |
342 | |
343 | QQuickSmoothedAnimationPrivate::~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 | |
352 | void 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 | |
363 | QAbstractAnimationJob* 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 | */ |
431 | QQuickSmoothedAnimation::ReversingMode QQuickSmoothedAnimation::reversingMode() const |
432 | { |
433 | Q_D(const QQuickSmoothedAnimation); |
434 | return (QQuickSmoothedAnimation::ReversingMode) d->anim->reversingMode; |
435 | } |
436 | |
437 | void 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 | */ |
458 | int QQuickSmoothedAnimation::duration() const |
459 | { |
460 | Q_D(const QQuickSmoothedAnimation); |
461 | return d->anim->userDuration; |
462 | } |
463 | |
464 | void 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 | |
475 | qreal 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 | */ |
493 | void 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 | */ |
514 | int QQuickSmoothedAnimation::maximumEasingTime() const |
515 | { |
516 | Q_D(const QQuickSmoothedAnimation); |
517 | return d->anim->maximumEasingTime; |
518 | } |
519 | |
520 | void 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 | |
530 | QT_END_NAMESPACE |
531 | |
532 | #include "moc_qquicksmoothedanimation_p_p.cpp" |
533 | |
534 | #include "moc_qquicksmoothedanimation_p.cpp" |
535 | |