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 "qquickspringanimation_p.h" |
5 | |
6 | #include "qquickanimation_p_p.h" |
7 | #include <private/qqmlproperty_p.h> |
8 | #include "private/qcontinuinganimationgroupjob_p.h" |
9 | |
10 | #include <QtCore/qdebug.h> |
11 | |
12 | #include <private/qobject_p.h> |
13 | |
14 | #include <cmath> |
15 | |
16 | #define DELAY_STOP_TIMER_INTERVAL 32 |
17 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | class QQuickSpringAnimationPrivate; |
21 | class Q_AUTOTEST_EXPORT QSpringAnimation : public QAbstractAnimationJob |
22 | { |
23 | Q_DISABLE_COPY(QSpringAnimation) |
24 | public: |
25 | QSpringAnimation(QQuickSpringAnimationPrivate * = nullptr); |
26 | |
27 | ~QSpringAnimation(); |
28 | int duration() const override; |
29 | void restart(); |
30 | void init(); |
31 | |
32 | qreal currentValue; |
33 | qreal to; |
34 | qreal velocity; |
35 | int startTime; |
36 | int dura; |
37 | int lastTime; |
38 | int stopTime; |
39 | enum Mode { |
40 | Track, |
41 | Velocity, |
42 | Spring |
43 | }; |
44 | Mode mode; |
45 | QQmlProperty target; |
46 | |
47 | qreal velocityms; |
48 | qreal maxVelocity; |
49 | qreal mass; |
50 | qreal spring; |
51 | qreal damping; |
52 | qreal epsilon; |
53 | qreal modulus; |
54 | |
55 | bool useMass : 1; |
56 | bool haveModulus : 1; |
57 | bool skipUpdate : 1; |
58 | typedef QHash<QQmlProperty, QSpringAnimation*> ActiveAnimationHash; |
59 | typedef ActiveAnimationHash::Iterator ActiveAnimationHashIt; |
60 | |
61 | void clearTemplate() { animationTemplate = nullptr; } |
62 | |
63 | protected: |
64 | void updateCurrentTime(int time) override; |
65 | void updateState(QAbstractAnimationJob::State, QAbstractAnimationJob::State) override; |
66 | void debugAnimation(QDebug d) const override; |
67 | |
68 | private: |
69 | QQuickSpringAnimationPrivate *animationTemplate; |
70 | }; |
71 | |
72 | class QQuickSpringAnimationPrivate : public QQuickPropertyAnimationPrivate |
73 | { |
74 | Q_DECLARE_PUBLIC(QQuickSpringAnimation) |
75 | public: |
76 | QQuickSpringAnimationPrivate() |
77 | : QQuickPropertyAnimationPrivate() |
78 | , velocityms(0) |
79 | , maxVelocity(0) |
80 | , mass(1.0) |
81 | , spring(0.) |
82 | , damping(0.) |
83 | , epsilon(0.01) |
84 | , modulus(0.0) |
85 | , useMass(false) |
86 | , haveModulus(false) |
87 | , mode(QSpringAnimation::Track) |
88 | { elapsed.start(); } |
89 | |
90 | void updateMode(); |
91 | qreal velocityms; |
92 | qreal maxVelocity; |
93 | qreal mass; |
94 | qreal spring; |
95 | qreal damping; |
96 | qreal epsilon; |
97 | qreal modulus; |
98 | |
99 | bool useMass : 1; |
100 | bool haveModulus : 1; |
101 | QSpringAnimation::Mode mode; |
102 | |
103 | QSpringAnimation::ActiveAnimationHash activeAnimations; |
104 | QElapsedTimer elapsed; |
105 | }; |
106 | |
107 | QSpringAnimation::QSpringAnimation(QQuickSpringAnimationPrivate *priv) |
108 | : QAbstractAnimationJob() |
109 | , currentValue(0) |
110 | , to(0) |
111 | , velocity(0) |
112 | , startTime(0) |
113 | , dura(0) |
114 | , lastTime(0) |
115 | , stopTime(-1) |
116 | , mode(Track) |
117 | , velocityms(0) |
118 | , maxVelocity(0) |
119 | , mass(1.0) |
120 | , spring(0.) |
121 | , damping(0.) |
122 | , epsilon(0.01) |
123 | , modulus(0.0) |
124 | , useMass(false) |
125 | , haveModulus(false) |
126 | , skipUpdate(false) |
127 | , animationTemplate(priv) |
128 | { |
129 | } |
130 | |
131 | QSpringAnimation::~QSpringAnimation() |
132 | { |
133 | if (animationTemplate) { |
134 | if (target.object()) { |
135 | ActiveAnimationHashIt it = animationTemplate->activeAnimations.find(key: target); |
136 | if (it != animationTemplate->activeAnimations.end() && it.value() == this) |
137 | animationTemplate->activeAnimations.erase(it); |
138 | } else { |
139 | //target is no longer valid, need to search linearly |
140 | for (ActiveAnimationHashIt it = animationTemplate->activeAnimations.begin(); it != animationTemplate->activeAnimations.end(); ++it) { |
141 | if (it.value() == this) { |
142 | animationTemplate->activeAnimations.erase(it); |
143 | break; |
144 | } |
145 | } |
146 | } |
147 | } |
148 | } |
149 | |
150 | int QSpringAnimation::duration() const |
151 | { |
152 | return -1; |
153 | } |
154 | |
155 | void QSpringAnimation::restart() |
156 | { |
157 | if (isRunning() || (stopTime != -1 && (animationTemplate->elapsed.elapsed() - stopTime) < DELAY_STOP_TIMER_INTERVAL)) { |
158 | skipUpdate = true; |
159 | init(); |
160 | } else { |
161 | skipUpdate = false; |
162 | //init() will be triggered when group starts |
163 | } |
164 | } |
165 | |
166 | void QSpringAnimation::init() |
167 | { |
168 | lastTime = startTime = 0; |
169 | stopTime = -1; |
170 | } |
171 | |
172 | void QSpringAnimation::updateCurrentTime(int time) |
173 | { |
174 | if (skipUpdate) { |
175 | skipUpdate = false; |
176 | return; |
177 | } |
178 | |
179 | if (mode == Track) { |
180 | stop(); |
181 | return; |
182 | } |
183 | |
184 | int elapsed = time - lastTime; |
185 | |
186 | if (!elapsed) |
187 | return; |
188 | |
189 | int count = elapsed / 16; |
190 | |
191 | if (mode == Spring) { |
192 | if (elapsed < 16) // capped at 62fps. |
193 | return; |
194 | lastTime = time - (elapsed - count * 16); |
195 | } else { |
196 | lastTime = time; |
197 | } |
198 | |
199 | qreal srcVal = to; |
200 | |
201 | bool stopped = false; |
202 | |
203 | if (haveModulus) { |
204 | currentValue = fmod(x: currentValue, y: modulus); |
205 | srcVal = fmod(x: srcVal, y: modulus); |
206 | } |
207 | if (mode == Spring) { |
208 | // Real men solve the spring DEs using RK4. |
209 | // We'll do something much simpler which gives a result that looks fine. |
210 | for (int i = 0; i < count; ++i) { |
211 | qreal diff = srcVal - currentValue; |
212 | if (haveModulus && qAbs(t: diff) > modulus / 2) { |
213 | if (diff < 0) |
214 | diff += modulus; |
215 | else |
216 | diff -= modulus; |
217 | } |
218 | if (useMass) |
219 | velocity = velocity + (spring * diff - damping * velocity) / mass; |
220 | else |
221 | velocity = velocity + spring * diff - damping * velocity; |
222 | if (maxVelocity > 0.) { |
223 | // limit velocity |
224 | if (velocity > maxVelocity) |
225 | velocity = maxVelocity; |
226 | else if (velocity < -maxVelocity) |
227 | velocity = -maxVelocity; |
228 | } |
229 | currentValue += velocity * 16.0 / 1000.0; |
230 | if (haveModulus) { |
231 | currentValue = fmod(x: currentValue, y: modulus); |
232 | if (currentValue < 0.0) |
233 | currentValue += modulus; |
234 | } |
235 | } |
236 | if (qAbs(t: velocity) < epsilon && qAbs(t: srcVal - currentValue) < epsilon) { |
237 | velocity = 0.0; |
238 | currentValue = srcVal; |
239 | stopped = true; |
240 | } |
241 | } else { |
242 | qreal moveBy = elapsed * velocityms; |
243 | qreal diff = srcVal - currentValue; |
244 | if (haveModulus && qAbs(t: diff) > modulus / 2) { |
245 | if (diff < 0) |
246 | diff += modulus; |
247 | else |
248 | diff -= modulus; |
249 | } |
250 | if (diff > 0) { |
251 | currentValue += moveBy; |
252 | if (haveModulus) |
253 | currentValue = std::fmod(x: currentValue, y: modulus); |
254 | } else { |
255 | currentValue -= moveBy; |
256 | if (haveModulus && currentValue < 0.0) |
257 | currentValue = std::fmod(x: currentValue, y: modulus) + modulus; |
258 | } |
259 | if (lastTime - startTime >= dura) { |
260 | currentValue = to; |
261 | stopped = true; |
262 | } |
263 | } |
264 | |
265 | qreal old_to = to; |
266 | |
267 | QQmlPropertyPrivate::write(that: target, currentValue, |
268 | QQmlPropertyData::BypassInterceptor | |
269 | QQmlPropertyData::DontRemoveBinding); |
270 | |
271 | if (stopped && old_to == to) { // do not stop if we got restarted |
272 | if (animationTemplate) |
273 | stopTime = animationTemplate->elapsed.elapsed(); |
274 | stop(); |
275 | } |
276 | } |
277 | |
278 | void QSpringAnimation::updateState(QAbstractAnimationJob::State newState, QAbstractAnimationJob::State /*oldState*/) |
279 | { |
280 | if (newState == QAbstractAnimationJob::Running) |
281 | init(); |
282 | } |
283 | |
284 | void QSpringAnimation::debugAnimation(QDebug d) const |
285 | { |
286 | d << "SpringAnimationJob(" << Qt::hex << (const void *) this << Qt::dec << ")" << "velocity:" << maxVelocity |
287 | << "spring:" << spring << "damping:" << damping << "epsilon:" << epsilon << "modulus:" << modulus |
288 | << "mass:" << mass << "target:" << target.object() << "property:" << target.name() |
289 | << "to:" << to << "current velocity:" << velocity; |
290 | } |
291 | |
292 | |
293 | void QQuickSpringAnimationPrivate::updateMode() |
294 | { |
295 | if (spring == 0. && maxVelocity == 0.) |
296 | mode = QSpringAnimation::Track; |
297 | else if (spring > 0.) |
298 | mode = QSpringAnimation::Spring; |
299 | else { |
300 | mode = QSpringAnimation::Velocity; |
301 | for (QSpringAnimation::ActiveAnimationHashIt it = activeAnimations.begin(), end = activeAnimations.end(); it != end; ++it) { |
302 | QSpringAnimation *animation = *it; |
303 | animation->startTime = animation->lastTime; |
304 | qreal dist = qAbs(t: animation->currentValue - animation->to); |
305 | if (haveModulus && dist > modulus / 2) |
306 | dist = modulus - fmod(x: dist, y: modulus); |
307 | animation->dura = dist / velocityms; |
308 | } |
309 | } |
310 | } |
311 | |
312 | /*! |
313 | \qmltype SpringAnimation |
314 | \instantiates QQuickSpringAnimation |
315 | \inqmlmodule QtQuick |
316 | \ingroup qtquick-transitions-animations |
317 | \inherits NumberAnimation |
318 | |
319 | \brief Allows a property to track a value in a spring-like motion. |
320 | |
321 | SpringAnimation mimics the oscillatory behavior of a spring, with the appropriate \l spring constant to |
322 | control the acceleration and the \l damping to control how quickly the effect dies away. |
323 | |
324 | You can also limit the maximum \l velocity of the animation. |
325 | |
326 | The following \l Rectangle moves to the position of the mouse using a |
327 | SpringAnimation when the mouse is clicked. The use of the \l Behavior |
328 | on the \c x and \c y values indicates that whenever these values are |
329 | changed, a SpringAnimation should be applied. |
330 | |
331 | \snippet qml/springanimation.qml 0 |
332 | |
333 | Like any other animation type, a SpringAnimation can be applied in a |
334 | number of ways, including transitions, behaviors and property value |
335 | sources. The \l {Animation and Transitions in Qt Quick} documentation shows a |
336 | variety of methods for creating animations. |
337 | |
338 | \sa SmoothedAnimation, {Animation and Transitions in Qt Quick}, {Qt Quick Examples - Animation}, {Qt Quick Demo - Clocks} |
339 | */ |
340 | |
341 | QQuickSpringAnimation::QQuickSpringAnimation(QObject *parent) |
342 | : QQuickNumberAnimation(*(new QQuickSpringAnimationPrivate),parent) |
343 | { |
344 | } |
345 | |
346 | QQuickSpringAnimation::~QQuickSpringAnimation() |
347 | { |
348 | Q_D(QQuickSpringAnimation); |
349 | for (QSpringAnimation::ActiveAnimationHashIt it = d->activeAnimations.begin(), end = d->activeAnimations.end(); it != end; ++it) |
350 | it.value()->clearTemplate(); |
351 | } |
352 | |
353 | /*! |
354 | \qmlproperty real QtQuick::SpringAnimation::velocity |
355 | |
356 | This property holds the maximum velocity allowed when tracking the source. |
357 | |
358 | The default value is 0 (no maximum velocity). |
359 | */ |
360 | |
361 | qreal QQuickSpringAnimation::velocity() const |
362 | { |
363 | Q_D(const QQuickSpringAnimation); |
364 | return d->maxVelocity; |
365 | } |
366 | |
367 | void QQuickSpringAnimation::setVelocity(qreal velocity) |
368 | { |
369 | Q_D(QQuickSpringAnimation); |
370 | d->maxVelocity = velocity; |
371 | d->velocityms = velocity / 1000.0; |
372 | d->updateMode(); |
373 | } |
374 | |
375 | /*! |
376 | \qmlproperty real QtQuick::SpringAnimation::spring |
377 | |
378 | This property describes how strongly the target is pulled towards the |
379 | source. The default value is 0 (that is, the spring-like motion is disabled). |
380 | |
381 | The useful value range is 0 - 5.0. |
382 | |
383 | When this property is set and the \l velocity value is greater than 0, |
384 | the \l velocity limits the maximum speed. |
385 | */ |
386 | qreal QQuickSpringAnimation::spring() const |
387 | { |
388 | Q_D(const QQuickSpringAnimation); |
389 | return d->spring; |
390 | } |
391 | |
392 | void QQuickSpringAnimation::setSpring(qreal spring) |
393 | { |
394 | Q_D(QQuickSpringAnimation); |
395 | d->spring = spring; |
396 | d->updateMode(); |
397 | } |
398 | |
399 | /*! |
400 | \qmlproperty real QtQuick::SpringAnimation::damping |
401 | This property holds the spring damping value. |
402 | |
403 | This value describes how quickly the spring-like motion comes to rest. |
404 | The default value is 0. |
405 | |
406 | The useful value range is 0 - 1.0. The lower the value, the faster it |
407 | comes to rest. |
408 | */ |
409 | qreal QQuickSpringAnimation::damping() const |
410 | { |
411 | Q_D(const QQuickSpringAnimation); |
412 | return d->damping; |
413 | } |
414 | |
415 | void QQuickSpringAnimation::setDamping(qreal damping) |
416 | { |
417 | Q_D(QQuickSpringAnimation); |
418 | if (damping > 1.) |
419 | damping = 1.; |
420 | |
421 | d->damping = damping; |
422 | } |
423 | |
424 | |
425 | /*! |
426 | \qmlproperty real QtQuick::SpringAnimation::epsilon |
427 | This property holds the spring epsilon. |
428 | |
429 | The epsilon is the rate and amount of change in the value which is close enough |
430 | to 0 to be considered equal to zero. This will depend on the usage of the value. |
431 | For pixel positions, 0.25 would suffice. For scale, 0.005 will suffice. |
432 | |
433 | The default is 0.01. Tuning this value can provide small performance improvements. |
434 | */ |
435 | qreal QQuickSpringAnimation::epsilon() const |
436 | { |
437 | Q_D(const QQuickSpringAnimation); |
438 | return d->epsilon; |
439 | } |
440 | |
441 | void QQuickSpringAnimation::setEpsilon(qreal epsilon) |
442 | { |
443 | Q_D(QQuickSpringAnimation); |
444 | d->epsilon = epsilon; |
445 | } |
446 | |
447 | /*! |
448 | \qmlproperty real QtQuick::SpringAnimation::modulus |
449 | This property holds the modulus value. The default value is 0. |
450 | |
451 | Setting a \a modulus forces the target value to "wrap around" at the modulus. |
452 | For example, setting the modulus to 360 will cause a value of 370 to wrap around to 10. |
453 | */ |
454 | qreal QQuickSpringAnimation::modulus() const |
455 | { |
456 | Q_D(const QQuickSpringAnimation); |
457 | return d->modulus; |
458 | } |
459 | |
460 | void QQuickSpringAnimation::setModulus(qreal modulus) |
461 | { |
462 | Q_D(QQuickSpringAnimation); |
463 | if (d->modulus != modulus) { |
464 | d->haveModulus = modulus != 0.0; |
465 | d->modulus = modulus; |
466 | d->updateMode(); |
467 | emit modulusChanged(); |
468 | } |
469 | } |
470 | |
471 | /*! |
472 | \qmlproperty real QtQuick::SpringAnimation::mass |
473 | This property holds the "mass" of the property being moved. |
474 | |
475 | The value is 1.0 by default. |
476 | |
477 | A greater mass causes slower movement and a greater spring-like |
478 | motion when an item comes to rest. |
479 | */ |
480 | qreal QQuickSpringAnimation::mass() const |
481 | { |
482 | Q_D(const QQuickSpringAnimation); |
483 | return d->mass; |
484 | } |
485 | |
486 | void QQuickSpringAnimation::setMass(qreal mass) |
487 | { |
488 | Q_D(QQuickSpringAnimation); |
489 | if (d->mass != mass && mass > 0.0) { |
490 | d->useMass = mass != 1.0; |
491 | d->mass = mass; |
492 | emit massChanged(); |
493 | } |
494 | } |
495 | |
496 | QAbstractAnimationJob* QQuickSpringAnimation::transition(QQuickStateActions &actions, |
497 | QQmlProperties &modified, |
498 | TransitionDirection direction, |
499 | QObject *defaultTarget) |
500 | { |
501 | Q_D(QQuickSpringAnimation); |
502 | Q_UNUSED(direction); |
503 | |
504 | QContinuingAnimationGroupJob *wrapperGroup = new QContinuingAnimationGroupJob(); |
505 | |
506 | QQuickStateActions dataActions = QQuickNumberAnimation::createTransitionActions(actions, modified, defaultTarget); |
507 | if (!dataActions.isEmpty()) { |
508 | QSet<QAbstractAnimationJob*> anims; |
509 | for (int i = 0; i < dataActions.size(); ++i) { |
510 | QSpringAnimation *animation; |
511 | bool needsRestart = false; |
512 | const QQmlProperty &property = dataActions.at(i).property; |
513 | if (d->activeAnimations.contains(key: property)) { |
514 | animation = d->activeAnimations[property]; |
515 | needsRestart = true; |
516 | } else { |
517 | animation = new QSpringAnimation(d); |
518 | d->activeAnimations.insert(key: property, value: animation); |
519 | animation->target = property; |
520 | } |
521 | wrapperGroup->appendAnimation(animation: initInstance(animation)); |
522 | |
523 | animation->to = dataActions.at(i).toValue.toReal(); |
524 | animation->startTime = 0; |
525 | animation->velocityms = d->velocityms; |
526 | animation->mass = d->mass; |
527 | animation->spring = d->spring; |
528 | animation->damping = d->damping; |
529 | animation->epsilon = d->epsilon; |
530 | animation->modulus = d->modulus; |
531 | animation->useMass = d->useMass; |
532 | animation->haveModulus = d->haveModulus; |
533 | animation->mode = d->mode; |
534 | animation->dura = -1; |
535 | animation->maxVelocity = d->maxVelocity; |
536 | |
537 | if (d->fromIsDefined) |
538 | animation->currentValue = dataActions.at(i).fromValue.toReal(); |
539 | else |
540 | animation->currentValue = property.read().toReal(); |
541 | if (animation->mode == QSpringAnimation::Velocity) { |
542 | qreal dist = qAbs(t: animation->currentValue - animation->to); |
543 | if (d->haveModulus && dist > d->modulus / 2) |
544 | dist = d->modulus - fmod(x: dist, y: d->modulus); |
545 | animation->dura = dist / animation->velocityms; |
546 | } |
547 | |
548 | if (needsRestart) |
549 | animation->restart(); |
550 | anims.insert(value: animation); |
551 | } |
552 | const auto copy = d->activeAnimations; |
553 | for (QSpringAnimation *anim : copy) { |
554 | if (!anims.contains(value: anim)) { |
555 | anim->clearTemplate(); |
556 | d->activeAnimations.remove(key: anim->target); |
557 | } |
558 | } |
559 | } |
560 | return wrapperGroup; |
561 | } |
562 | |
563 | QT_END_NAMESPACE |
564 | |
565 | #include "moc_qquickspringanimation_p.cpp" |
566 | |