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/*!
5 \class QSequentialAnimationGroup
6 \inmodule QtCore
7 \brief The QSequentialAnimationGroup class provides a sequential group of animations.
8 \since 4.6
9 \ingroup animation
10
11 QSequentialAnimationGroup is a QAnimationGroup that runs its
12 animations in sequence, i.e., it starts one animation after
13 another has finished playing. The animations are played in the
14 order they are added to the group (using
15 \l{QAnimationGroup::}{addAnimation()} or
16 \l{QAnimationGroup::}{insertAnimation()}). The animation group
17 finishes when its last animation has finished.
18
19 At each moment there is at most one animation that is active in
20 the group; it is returned by currentAnimation(). An empty group
21 has no current animation.
22
23 A sequential animation group can be treated as any other
24 animation, i.e., it can be started, stopped, and added to other
25 groups. You can also call addPause() or insertPause() to add a
26 pause to a sequential animation group.
27
28 \snippet code/src_corelib_animation_qsequentialanimationgroup.cpp 0
29
30 In this example, \c anim1 and \c anim2 are two already set up
31 \l{QPropertyAnimation}s.
32
33 \sa QAnimationGroup, QAbstractAnimation, {The Animation Framework}
34*/
35
36#include "qsequentialanimationgroup.h"
37#include "qsequentialanimationgroup_p.h"
38
39#include "qpauseanimation.h"
40
41#include <QtCore/qdebug.h>
42
43QT_BEGIN_NAMESPACE
44
45typedef QList<QAbstractAnimation *>::ConstIterator AnimationListConstIt;
46
47bool QSequentialAnimationGroupPrivate::atEnd() const
48{
49 // we try to detect if we're at the end of the group
50 //this is true if the following conditions are true:
51 // 1. we're in the last loop
52 // 2. the direction is forward
53 // 3. the current animation is the last one
54 // 4. the current animation has reached its end
55 const int animTotalCurrentTime = QAbstractAnimationPrivate::get(q: currentAnimation)->totalCurrentTime;
56 return (currentLoop == loopCount - 1
57 && direction == QAbstractAnimation::Forward
58 && currentAnimation == animations.last()
59 && animTotalCurrentTime == animationActualTotalDuration(index: currentAnimationIndex));
60}
61
62int QSequentialAnimationGroupPrivate::animationActualTotalDuration(int index) const
63{
64 QAbstractAnimation *anim = animations.at(i: index);
65 int ret = anim->totalDuration();
66 if (ret == -1 && actualDuration.size() > index)
67 ret = actualDuration.at(i: index); //we can try the actual duration there
68 return ret;
69}
70
71QSequentialAnimationGroupPrivate::AnimationIndex QSequentialAnimationGroupPrivate::indexForCurrentTime() const
72{
73 Q_ASSERT(!animations.isEmpty());
74
75 AnimationIndex ret;
76 int duration = 0;
77
78 for (int i = 0; i < animations.size(); ++i) {
79 duration = animationActualTotalDuration(index: i);
80
81 // 'animation' is the current animation if one of these reasons is true:
82 // 1. it's duration is undefined
83 // 2. it ends after msecs
84 // 3. it is the last animation (this can happen in case there is at least 1 uncontrolled animation)
85 // 4. it ends exactly in msecs and the direction is backwards
86 if (duration == -1 || currentTime < (ret.timeOffset + duration)
87 || (currentTime == (ret.timeOffset + duration) && direction == QAbstractAnimation::Backward)) {
88 ret.index = i;
89 return ret;
90 }
91
92 // 'animation' has a non-null defined duration and is not the one at time 'msecs'.
93 ret.timeOffset += duration;
94 }
95
96 // this can only happen when one of those conditions is true:
97 // 1. the duration of the group is undefined and we passed its actual duration
98 // 2. there are only 0-duration animations in the group
99 ret.timeOffset -= duration;
100 ret.index = animations.size() - 1;
101 return ret;
102}
103
104void QSequentialAnimationGroupPrivate::restart()
105{
106 // restarting the group by making the first/last animation the current one
107 if (direction == QAbstractAnimation::Forward) {
108 lastLoop = 0;
109 if (currentAnimationIndex == 0)
110 activateCurrentAnimation();
111 else
112 setCurrentAnimation(index: 0);
113 } else { // direction == QAbstractAnimation::Backward
114 lastLoop = loopCount - 1;
115 int index = animations.size() - 1;
116 if (currentAnimationIndex == index)
117 activateCurrentAnimation();
118 else
119 setCurrentAnimation(index);
120 }
121}
122
123/*!
124 \internal
125 This manages advancing the execution of a group running forwards (time has gone forward),
126 which is the same behaviour for rewinding the execution of a group running backwards
127 (time has gone backward).
128*/
129void QSequentialAnimationGroupPrivate::advanceForwards(const AnimationIndex &newAnimationIndex)
130{
131 if (lastLoop < currentLoop) {
132 // we need to fast forward to the end
133 for (int i = currentAnimationIndex; i < animations.size(); ++i) {
134 QAbstractAnimation *anim = animations.at(i);
135 setCurrentAnimation(index: i, intermediate: true);
136 anim->setCurrentTime(animationActualTotalDuration(index: i));
137 }
138 // this will make sure the current animation is reset to the beginning
139 if (animations.size() == 1)
140 // we need to force activation because setCurrentAnimation will have no effect
141 activateCurrentAnimation();
142 else
143 setCurrentAnimation(index: 0, intermediate: true);
144 }
145
146 // and now we need to fast forward from the current position to
147 for (int i = currentAnimationIndex; i < newAnimationIndex.index; ++i) { //### WRONG,
148 QAbstractAnimation *anim = animations.at(i);
149 setCurrentAnimation(index: i, intermediate: true);
150 anim->setCurrentTime(animationActualTotalDuration(index: i));
151 }
152 // setting the new current animation will happen later
153}
154
155/*!
156 \internal
157 This manages rewinding the execution of a group running forwards (time has gone forward),
158 which is the same behaviour for advancing the execution of a group running backwards
159 (time has gone backward).
160*/
161void QSequentialAnimationGroupPrivate::rewindForwards(const AnimationIndex &newAnimationIndex)
162{
163 if (lastLoop > currentLoop) {
164 // we need to fast rewind to the beginning
165 for (int i = currentAnimationIndex; i >= 0 ; --i) {
166 QAbstractAnimation *anim = animations.at(i);
167 setCurrentAnimation(index: i, intermediate: true);
168 anim->setCurrentTime(0);
169 }
170 // this will make sure the current animation is reset to the end
171 if (animations.size() == 1)
172 // we need to force activation because setCurrentAnimation will have no effect
173 activateCurrentAnimation();
174 else
175 setCurrentAnimation(index: animations.size() - 1, intermediate: true);
176 }
177
178 // and now we need to fast rewind from the current position to
179 for (int i = currentAnimationIndex; i > newAnimationIndex.index; --i) {
180 QAbstractAnimation *anim = animations.at(i);
181 setCurrentAnimation(index: i, intermediate: true);
182 anim->setCurrentTime(0);
183 }
184 // setting the new current animation will happen later
185}
186
187/*!
188 \fn QSequentialAnimationGroup::currentAnimationChanged(QAbstractAnimation *current)
189
190 QSequentialAnimationGroup emits this signal when currentAnimation
191 has been changed. \a current is the current animation.
192
193 \sa currentAnimation()
194*/
195
196
197/*!
198 Constructs a QSequentialAnimationGroup.
199 \a parent is passed to QObject's constructor.
200*/
201QSequentialAnimationGroup::QSequentialAnimationGroup(QObject *parent)
202 : QAnimationGroup(*new QSequentialAnimationGroupPrivate, parent)
203{
204}
205
206/*!
207 \internal
208*/
209QSequentialAnimationGroup::QSequentialAnimationGroup(QSequentialAnimationGroupPrivate &dd,
210 QObject *parent)
211 : QAnimationGroup(dd, parent)
212{
213}
214
215/*!
216 Destroys the animation group. It will also destroy all its animations.
217*/
218QSequentialAnimationGroup::~QSequentialAnimationGroup()
219{
220}
221
222/*!
223 Adds a pause of \a msecs to this animation group.
224 The pause is considered as a special type of animation, thus
225 \l{QAnimationGroup::animationCount()}{animationCount} will be
226 increased by one.
227
228 \sa insertPause(), QAnimationGroup::addAnimation()
229*/
230QPauseAnimation *QSequentialAnimationGroup::addPause(int msecs)
231{
232 QPauseAnimation *pause = new QPauseAnimation(msecs);
233 addAnimation(animation: pause);
234 return pause;
235}
236
237/*!
238 Inserts a pause of \a msecs milliseconds at \a index in this animation
239 group.
240
241 \sa addPause(), QAnimationGroup::insertAnimation()
242*/
243QPauseAnimation *QSequentialAnimationGroup::insertPause(int index, int msecs)
244{
245 Q_D(const QSequentialAnimationGroup);
246
247 if (index < 0 || index > d->animations.size()) {
248 qWarning(msg: "QSequentialAnimationGroup::insertPause: index is out of bounds");
249 return nullptr;
250 }
251
252 QPauseAnimation *pause = new QPauseAnimation(msecs);
253 insertAnimation(index, animation: pause);
254 return pause;
255}
256
257
258/*!
259 \property QSequentialAnimationGroup::currentAnimation
260 \brief the animation in the current time.
261*/
262QAbstractAnimation *QSequentialAnimationGroup::currentAnimation() const
263{
264 Q_D(const QSequentialAnimationGroup);
265 return d->currentAnimation;
266}
267
268QBindable<QAbstractAnimation *> QSequentialAnimationGroup::bindableCurrentAnimation() const
269{
270 return &d_func()->currentAnimation;
271}
272
273/*!
274 \reimp
275*/
276int QSequentialAnimationGroup::duration() const
277{
278 Q_D(const QSequentialAnimationGroup);
279 int ret = 0;
280
281 for (AnimationListConstIt it = d->animations.constBegin(), cend = d->animations.constEnd(); it != cend; ++it) {
282 const int currentDuration = (*it)->totalDuration();
283 if (currentDuration == -1)
284 return -1; // Undetermined length
285
286 ret += currentDuration;
287 }
288
289 return ret;
290}
291
292/*!
293 \reimp
294*/
295void QSequentialAnimationGroup::updateCurrentTime(int currentTime)
296{
297 Q_D(QSequentialAnimationGroup);
298 if (!d->currentAnimation)
299 return;
300
301 const QSequentialAnimationGroupPrivate::AnimationIndex newAnimationIndex = d->indexForCurrentTime();
302
303 // remove unneeded animations from actualDuration list
304 while (newAnimationIndex.index < d->actualDuration.size())
305 d->actualDuration.removeLast();
306
307 // newAnimationIndex.index is the new current animation
308 if (d->lastLoop < d->currentLoop
309 || (d->lastLoop == d->currentLoop && d->currentAnimationIndex < newAnimationIndex.index)) {
310 // advancing with forward direction is the same as rewinding with backwards direction
311 d->advanceForwards(newAnimationIndex);
312 } else if (d->lastLoop > d->currentLoop
313 || (d->lastLoop == d->currentLoop && d->currentAnimationIndex > newAnimationIndex.index)) {
314 // rewinding with forward direction is the same as advancing with backwards direction
315 d->rewindForwards(newAnimationIndex);
316 }
317
318 d->setCurrentAnimation(index: newAnimationIndex.index);
319
320 const int newCurrentTime = currentTime - newAnimationIndex.timeOffset;
321
322 if (d->currentAnimation) {
323 d->currentAnimation->setCurrentTime(newCurrentTime);
324 if (d->atEnd()) {
325 //we make sure that we don't exceed the duration here
326 d->currentTime += QAbstractAnimationPrivate::get(q: d->currentAnimation)->totalCurrentTime - newCurrentTime;
327 stop();
328 }
329 } else {
330 //the only case where currentAnimation could be null
331 //is when all animations have been removed
332 Q_ASSERT(d->animations.isEmpty());
333 d->currentTime = 0;
334 stop();
335 }
336
337 d->lastLoop = d->currentLoop;
338}
339
340/*!
341 \reimp
342*/
343void QSequentialAnimationGroup::updateState(QAbstractAnimation::State newState,
344 QAbstractAnimation::State oldState)
345{
346 Q_D(QSequentialAnimationGroup);
347 QAnimationGroup::updateState(newState, oldState);
348
349 if (!d->currentAnimation)
350 return;
351
352 switch (newState) {
353 case Stopped:
354 d->currentAnimation->stop();
355 break;
356 case Paused:
357 if (oldState == d->currentAnimation->state()
358 && oldState == QSequentialAnimationGroup::Running) {
359 d->currentAnimation->pause();
360 }
361 else
362 d->restart();
363 break;
364 case Running:
365 if (oldState == d->currentAnimation->state()
366 && oldState == QSequentialAnimationGroup::Paused)
367 d->currentAnimation->start();
368 else
369 d->restart();
370 break;
371 }
372}
373
374/*!
375 \reimp
376*/
377void QSequentialAnimationGroup::updateDirection(QAbstractAnimation::Direction direction)
378{
379 Q_D(QSequentialAnimationGroup);
380 // we need to update the direction of the current animation
381 if (state() != Stopped && d->currentAnimation)
382 d->currentAnimation->setDirection(direction);
383}
384
385/*!
386 \reimp
387*/
388bool QSequentialAnimationGroup::event(QEvent *event)
389{
390 return QAnimationGroup::event(event);
391}
392
393void QSequentialAnimationGroupPrivate::setCurrentAnimation(int index, bool intermediate)
394{
395 Q_Q(QSequentialAnimationGroup);
396 // currentAnimation.removeBindingUnlessInWrapper()
397 // is not necessary here, since it is read only
398
399 index = qMin(a: index, b: animations.size() - 1);
400
401 if (index == -1) {
402 Q_ASSERT(animations.isEmpty());
403 currentAnimationIndex = -1;
404 currentAnimation = nullptr;
405 return;
406 }
407
408 // need these two checks below because this func can be called after the current animation
409 // has been removed
410 if (index == currentAnimationIndex && animations.at(i: index) == currentAnimation)
411 return;
412
413 // stop the old current animation
414 if (currentAnimation)
415 currentAnimation->stop();
416
417 currentAnimationIndex = index;
418 currentAnimation = animations.at(i: index);
419
420 emit q->currentAnimationChanged(current: currentAnimation);
421
422 activateCurrentAnimation(intermediate);
423}
424
425void QSequentialAnimationGroupPrivate::activateCurrentAnimation(bool intermediate)
426{
427 if (!currentAnimation || state == QSequentialAnimationGroup::Stopped)
428 return;
429
430 currentAnimation->stop();
431
432 // we ensure the direction is consistent with the group's direction
433 currentAnimation->setDirection(direction);
434
435 // connects to the finish signal of uncontrolled animations
436 if (currentAnimation->totalDuration() == -1)
437 connectUncontrolledAnimation(anim: currentAnimation);
438
439 currentAnimation->start();
440 if (!intermediate && state == QSequentialAnimationGroup::Paused)
441 currentAnimation->pause();
442}
443
444void QSequentialAnimationGroupPrivate::_q_uncontrolledAnimationFinished()
445{
446 Q_Q(QSequentialAnimationGroup);
447 Q_ASSERT(qobject_cast<QAbstractAnimation *>(q->sender()) == currentAnimation);
448
449 // we trust the duration returned by the animation
450 while (actualDuration.size() < (currentAnimationIndex + 1))
451 actualDuration.append(t: -1);
452 actualDuration[currentAnimationIndex] = currentAnimation->currentTime();
453
454 disconnectUncontrolledAnimation(anim: currentAnimation);
455
456 if ((direction == QAbstractAnimation::Forward && currentAnimation == animations.last())
457 || (direction == QAbstractAnimation::Backward && currentAnimationIndex == 0)) {
458 // we don't handle looping of a group with undefined duration
459 q->stop();
460 } else if (direction == QAbstractAnimation::Forward) {
461 // set the current animation to be the next one
462 setCurrentAnimation(index: currentAnimationIndex + 1);
463 } else {
464 // set the current animation to be the previous one
465 setCurrentAnimation(index: currentAnimationIndex - 1);
466 }
467}
468
469/*!
470 \internal
471 This method is called whenever an animation is added to
472 the group at index \a index.
473 Note: We only support insertion after the current animation
474*/
475void QSequentialAnimationGroupPrivate::animationInsertedAt(qsizetype index)
476{
477 if (currentAnimation == nullptr) {
478 setCurrentAnimation(index: 0); // initialize the current animation
479 Q_ASSERT(currentAnimation);
480 }
481
482 if (currentAnimationIndex == index
483 && currentAnimation->currentTime() == 0 && currentAnimation->currentLoop() == 0) {
484 //in this case we simply insert an animation before the current one has actually started
485 setCurrentAnimation(index);
486 }
487
488 //we update currentAnimationIndex in case it has changed (the animation pointer is still valid)
489 currentAnimationIndex = animations.indexOf(t: currentAnimation);
490
491 if (index < currentAnimationIndex || currentLoop != 0) {
492 qWarning(msg: "QSequentialGroup::insertAnimation only supports to add animations after the current one.");
493 return; //we're not affected because it is added after the current one
494 }
495}
496
497/*!
498 \internal
499 This method is called whenever an animation is removed from
500 the group at index \a index. The animation is no more listed when this
501 method is called.
502*/
503void QSequentialAnimationGroupPrivate::animationRemoved(qsizetype index, QAbstractAnimation *anim)
504{
505 Q_Q(QSequentialAnimationGroup);
506 QAnimationGroupPrivate::animationRemoved(index, anim);
507
508 if (!currentAnimation)
509 return;
510
511 if (actualDuration.size() > index)
512 actualDuration.removeAt(i: index);
513
514 const qsizetype currentIndex = animations.indexOf(t: currentAnimation);
515 if (currentIndex == -1) {
516 //we're removing the current animation
517
518 disconnectUncontrolledAnimation(anim: currentAnimation);
519
520 if (index < animations.size())
521 setCurrentAnimation(index); //let's try to take the next one
522 else if (index > 0)
523 setCurrentAnimation(index: index - 1);
524 else// case all animations were removed
525 setCurrentAnimation(index: -1);
526 } else if (currentAnimationIndex > index) {
527 currentAnimationIndex--;
528 }
529
530 // duration of the previous animations up to the current animation
531 currentTime = 0;
532 for (qsizetype i = 0; i < currentAnimationIndex; ++i) {
533 const int current = animationActualTotalDuration(index: i);
534 currentTime += current;
535 }
536
537 if (currentIndex != -1) {
538 //the current animation is not the one being removed
539 //so we add its current time to the current time of this group
540 currentTime += QAbstractAnimationPrivate::get(q: currentAnimation)->totalCurrentTime;
541 }
542
543 //let's also update the total current time
544 totalCurrentTime = currentTime + loopCount * q->duration();
545}
546
547QT_END_NAMESPACE
548
549#include "moc_qsequentialanimationgroup.cpp"
550

source code of qtbase/src/corelib/animation/qsequentialanimationgroup.cpp