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