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 "private/qsequentialanimationgroupjob_p.h" |
5 | #include "private/qpauseanimationjob_p.h" |
6 | #include "private/qanimationjobutil_p.h" |
7 | |
8 | QT_BEGIN_NAMESPACE |
9 | |
10 | QSequentialAnimationGroupJob::QSequentialAnimationGroupJob() |
11 | : QAnimationGroupJob() |
12 | , m_currentAnimation(nullptr) |
13 | , m_previousLoop(0) |
14 | { |
15 | } |
16 | |
17 | QSequentialAnimationGroupJob::~QSequentialAnimationGroupJob() |
18 | { |
19 | } |
20 | |
21 | bool QSequentialAnimationGroupJob::atEnd() const |
22 | { |
23 | // we try to detect if we're at the end of the group |
24 | //this is true if the following conditions are true: |
25 | // 1. we're in the last loop |
26 | // 2. the direction is forward |
27 | // 3. the current animation is the last one |
28 | // 4. the current animation has reached its end |
29 | |
30 | const int animTotalCurrentTime = m_currentAnimation->currentTime(); |
31 | return (m_currentLoop == m_loopCount - 1 |
32 | && m_direction == Forward |
33 | && !m_children.next(current: m_currentAnimation) |
34 | && animTotalCurrentTime == animationActualTotalDuration(anim: m_currentAnimation)); |
35 | } |
36 | |
37 | int QSequentialAnimationGroupJob::animationActualTotalDuration( |
38 | const QAbstractAnimationJob *anim) const |
39 | { |
40 | int ret = anim->totalDuration(); |
41 | if (ret == -1) { |
42 | int done = uncontrolledAnimationFinishTime(anim); |
43 | // If the animation has reached the end, use the uncontrolledFinished value. |
44 | if (done >= 0 && (anim->loopCount() - 1 == anim->currentLoop() || anim->state() == Stopped)) |
45 | return done; |
46 | } |
47 | return ret; |
48 | } |
49 | |
50 | QSequentialAnimationGroupJob::AnimationIndex QSequentialAnimationGroupJob::indexForCurrentTime() const |
51 | { |
52 | Q_ASSERT(!m_children.isEmpty()); |
53 | |
54 | AnimationIndex ret; |
55 | int duration = 0; |
56 | |
57 | for (const QAbstractAnimationJob *anim : m_children) { |
58 | duration = animationActualTotalDuration(anim); |
59 | |
60 | // 'animation' is the current animation if one of these reasons is true: |
61 | // 1. it's duration is undefined |
62 | // 2. it ends after msecs |
63 | // 3. it is the last animation (this can happen in case there is at least 1 uncontrolled animation) |
64 | // 4. it ends exactly in msecs and the direction is backwards |
65 | if (duration == -1 || m_currentTime < (ret.timeOffset + duration) |
66 | || (m_currentTime == (ret.timeOffset + duration) && m_direction == QAbstractAnimationJob::Backward)) { |
67 | ret.animation = anim; |
68 | return ret; |
69 | } |
70 | |
71 | if (anim == m_currentAnimation) { |
72 | ret.afterCurrent = true; |
73 | } |
74 | |
75 | // 'animation' has a non-null defined duration and is not the one at time 'msecs'. |
76 | ret.timeOffset += duration; |
77 | } |
78 | |
79 | // this can only happen when one of those conditions is true: |
80 | // 1. the duration of the group is undefined and we passed its actual duration |
81 | // 2. there are only 0-duration animations in the group |
82 | ret.timeOffset -= duration; |
83 | ret.animation = m_children.last(); |
84 | return ret; |
85 | } |
86 | |
87 | void QSequentialAnimationGroupJob::restart() |
88 | { |
89 | // restarting the group by making the first/last animation the current one |
90 | if (m_direction == Forward) { |
91 | m_previousLoop = 0; |
92 | if (m_currentAnimation == m_children.first()) |
93 | activateCurrentAnimation(); |
94 | else |
95 | setCurrentAnimation(anim: m_children.first()); |
96 | } |
97 | else { // direction == Backward |
98 | m_previousLoop = m_loopCount - 1; |
99 | if (m_currentAnimation == m_children.last()) |
100 | activateCurrentAnimation(); |
101 | else |
102 | setCurrentAnimation(anim: m_children.last()); |
103 | } |
104 | } |
105 | |
106 | void QSequentialAnimationGroupJob::advanceForwards(const AnimationIndex &newAnimationIndex) |
107 | { |
108 | if (m_previousLoop < m_currentLoop) { |
109 | // we need to fast forward to the end |
110 | for (QAbstractAnimationJob *anim = m_currentAnimation; anim; anim = m_children.next(current: anim)) { |
111 | RETURN_IF_DELETED(setCurrentAnimation(anim, true)); |
112 | RETURN_IF_DELETED(anim->setCurrentTime(animationActualTotalDuration(anim))); |
113 | } |
114 | // this will make sure the current animation is reset to the beginning |
115 | if (m_children.count() == 1) { |
116 | // we need to force activation because setCurrentAnimation will have no effect |
117 | RETURN_IF_DELETED(activateCurrentAnimation()); |
118 | } else { |
119 | RETURN_IF_DELETED(setCurrentAnimation(m_children.first(), true)); |
120 | } |
121 | } |
122 | |
123 | // and now we need to fast forward from the current position to |
124 | for (QAbstractAnimationJob *anim = m_currentAnimation; |
125 | anim && anim != newAnimationIndex.animation; anim = m_children.next(current: anim)) { //### WRONG, |
126 | RETURN_IF_DELETED(setCurrentAnimation(anim, true)); |
127 | RETURN_IF_DELETED(anim->setCurrentTime(animationActualTotalDuration(anim))); |
128 | } |
129 | // setting the new current animation will happen later |
130 | } |
131 | |
132 | void QSequentialAnimationGroupJob::rewindForwards(const AnimationIndex &newAnimationIndex) |
133 | { |
134 | if (m_previousLoop > m_currentLoop) { |
135 | // we need to fast rewind to the beginning |
136 | for (QAbstractAnimationJob *anim = m_currentAnimation; anim; |
137 | anim = m_children.prev(current: anim)) { |
138 | RETURN_IF_DELETED(setCurrentAnimation(anim, true)); |
139 | RETURN_IF_DELETED(anim->setCurrentTime(0)); |
140 | } |
141 | // this will make sure the current animation is reset to the end |
142 | if (m_children.count() == 1) { //count == 1 |
143 | // we need to force activation because setCurrentAnimation will have no effect |
144 | RETURN_IF_DELETED(activateCurrentAnimation()); |
145 | } else { |
146 | RETURN_IF_DELETED(setCurrentAnimation(m_children.last(), true)); |
147 | } |
148 | } |
149 | |
150 | // and now we need to fast rewind from the current position to |
151 | for (QAbstractAnimationJob *anim = m_currentAnimation; |
152 | anim && anim != newAnimationIndex.animation; anim = m_children.prev(current: anim)) { |
153 | RETURN_IF_DELETED(setCurrentAnimation(anim, true)); |
154 | RETURN_IF_DELETED(anim->setCurrentTime(0)); |
155 | } |
156 | // setting the new current animation will happen later |
157 | } |
158 | |
159 | int QSequentialAnimationGroupJob::duration() const |
160 | { |
161 | int ret = 0; |
162 | |
163 | for (const QAbstractAnimationJob *anim : m_children) { |
164 | const int currentDuration = anim->totalDuration(); |
165 | if (currentDuration == -1) |
166 | return -1; // Undetermined length |
167 | |
168 | ret += currentDuration; |
169 | } |
170 | |
171 | return ret; |
172 | } |
173 | |
174 | void QSequentialAnimationGroupJob::clear() |
175 | { |
176 | m_previousLoop = 0; |
177 | QAnimationGroupJob::clear(); |
178 | |
179 | // clear() should call removeAnimation(), which will clear m_currentAnimation, eventually. |
180 | Q_ASSERT(m_currentAnimation == nullptr); |
181 | } |
182 | |
183 | void QSequentialAnimationGroupJob::updateCurrentTime(int currentTime) |
184 | { |
185 | if (!m_currentAnimation) |
186 | return; |
187 | |
188 | const QSequentialAnimationGroupJob::AnimationIndex newAnimationIndex = indexForCurrentTime(); |
189 | |
190 | // newAnimationIndex.index is the new current animation |
191 | if (m_previousLoop < m_currentLoop |
192 | || (m_previousLoop == m_currentLoop && m_currentAnimation != newAnimationIndex.animation && newAnimationIndex.afterCurrent)) { |
193 | // advancing with forward direction is the same as rewinding with backwards direction |
194 | RETURN_IF_DELETED(advanceForwards(newAnimationIndex)); |
195 | |
196 | } else if (m_previousLoop > m_currentLoop |
197 | || (m_previousLoop == m_currentLoop && m_currentAnimation != newAnimationIndex.animation && !newAnimationIndex.afterCurrent)) { |
198 | // rewinding with forward direction is the same as advancing with backwards direction |
199 | RETURN_IF_DELETED(rewindForwards(newAnimationIndex)); |
200 | } |
201 | |
202 | RETURN_IF_DELETED(setCurrentAnimation(newAnimationIndex.animation)); |
203 | |
204 | const int newCurrentTime = currentTime - newAnimationIndex.timeOffset; |
205 | |
206 | if (m_currentAnimation) { |
207 | RETURN_IF_DELETED(m_currentAnimation->setCurrentTime(newCurrentTime)); |
208 | if (atEnd()) { |
209 | //we make sure that we don't exceed the duration here |
210 | m_currentTime += m_currentAnimation->currentTime() - newCurrentTime; |
211 | RETURN_IF_DELETED(stop()); |
212 | } |
213 | } else { |
214 | //the only case where currentAnimation could be null |
215 | //is when all animations have been removed |
216 | Q_ASSERT(m_children.isEmpty()); |
217 | m_currentTime = 0; |
218 | RETURN_IF_DELETED(stop()); |
219 | } |
220 | |
221 | m_previousLoop = m_currentLoop; |
222 | } |
223 | |
224 | void QSequentialAnimationGroupJob::updateState(QAbstractAnimationJob::State newState, |
225 | QAbstractAnimationJob::State oldState) |
226 | { |
227 | QAnimationGroupJob::updateState(newState, oldState); |
228 | |
229 | if (!m_currentAnimation) |
230 | return; |
231 | |
232 | switch (newState) { |
233 | case Stopped: |
234 | m_currentAnimation->stop(); |
235 | break; |
236 | case Paused: |
237 | if (oldState == m_currentAnimation->state() && oldState == Running) |
238 | m_currentAnimation->pause(); |
239 | else |
240 | restart(); |
241 | break; |
242 | case Running: |
243 | if (oldState == m_currentAnimation->state() && oldState == Paused) |
244 | m_currentAnimation->start(); |
245 | else |
246 | restart(); |
247 | break; |
248 | } |
249 | } |
250 | |
251 | void QSequentialAnimationGroupJob::updateDirection(QAbstractAnimationJob::Direction direction) |
252 | { |
253 | // we need to update the direction of the current animation |
254 | if (!isStopped() && m_currentAnimation) |
255 | m_currentAnimation->setDirection(direction); |
256 | } |
257 | |
258 | void QSequentialAnimationGroupJob::setCurrentAnimation( |
259 | const QAbstractAnimationJob *anim, bool intermediate) |
260 | { |
261 | if (!anim) { |
262 | Q_ASSERT(m_children.isEmpty()); |
263 | m_currentAnimation = nullptr; |
264 | return; |
265 | } |
266 | |
267 | if (anim == m_currentAnimation) |
268 | return; |
269 | |
270 | // stop the old current animation |
271 | if (m_currentAnimation) |
272 | m_currentAnimation->stop(); |
273 | |
274 | // Assert that the animation passed as argument is actually part of this group ... |
275 | Q_ASSERT(m_children.contains(anim)); |
276 | |
277 | // ... as then this const_cast is just a shortcut for looking up the non-const |
278 | // pointer in the linked list of jobs. |
279 | m_currentAnimation = const_cast<QAbstractAnimationJob *>(anim); |
280 | |
281 | activateCurrentAnimation(intermediate); |
282 | } |
283 | |
284 | void QSequentialAnimationGroupJob::activateCurrentAnimation(bool intermediate) |
285 | { |
286 | if (!m_currentAnimation || isStopped()) |
287 | return; |
288 | |
289 | m_currentAnimation->stop(); |
290 | |
291 | // we ensure the direction is consistent with the group's direction |
292 | m_currentAnimation->setDirection(m_direction); |
293 | |
294 | // reset the finish time of the animation if it is uncontrolled |
295 | if (m_currentAnimation->totalDuration() == -1) |
296 | resetUncontrolledAnimationFinishTime(anim: m_currentAnimation); |
297 | |
298 | RETURN_IF_DELETED(m_currentAnimation->start()); |
299 | if (!intermediate && isPaused()) |
300 | m_currentAnimation->pause(); |
301 | } |
302 | |
303 | void QSequentialAnimationGroupJob::uncontrolledAnimationFinished(QAbstractAnimationJob *animation) |
304 | { |
305 | Q_UNUSED(animation); |
306 | Q_ASSERT(animation == m_currentAnimation); |
307 | |
308 | setUncontrolledAnimationFinishTime(anim: m_currentAnimation, time: m_currentAnimation->currentTime()); |
309 | |
310 | int totalTime = currentTime(); |
311 | if (m_direction == Forward) { |
312 | // set the current animation to be the next one |
313 | if (auto *anim = m_children.next(current: m_currentAnimation)) |
314 | RETURN_IF_DELETED(setCurrentAnimation(anim)); |
315 | |
316 | for (QAbstractAnimationJob *a = m_children.next(current: animation); a; a = m_children.next(current: a)) { |
317 | int dur = a->duration(); |
318 | if (dur == -1) { |
319 | totalTime = -1; |
320 | break; |
321 | } else { |
322 | totalTime += dur; |
323 | } |
324 | } |
325 | |
326 | } else { |
327 | // set the current animation to be the previous one |
328 | if (auto *anim = m_children.prev(current: m_currentAnimation)) |
329 | RETURN_IF_DELETED(setCurrentAnimation(anim)); |
330 | |
331 | for (QAbstractAnimationJob *a = m_children.prev(current: animation); a; a = m_children.prev(current: a)) { |
332 | int dur = a->duration(); |
333 | if (dur == -1) { |
334 | totalTime = -1; |
335 | break; |
336 | } else { |
337 | totalTime += dur; |
338 | } |
339 | } |
340 | } |
341 | if (totalTime >= 0) |
342 | setUncontrolledAnimationFinishTime(anim: this, time: totalTime); |
343 | if (atEnd()) |
344 | stop(); |
345 | } |
346 | |
347 | void QSequentialAnimationGroupJob::animationInserted(QAbstractAnimationJob *anim) |
348 | { |
349 | if (m_currentAnimation == nullptr) |
350 | RETURN_IF_DELETED(setCurrentAnimation(m_children.first())); // initialize the current animation |
351 | |
352 | if (m_currentAnimation == m_children.next(current: anim) |
353 | && m_currentAnimation->currentTime() == 0 && m_currentAnimation->currentLoop() == 0) { |
354 | //in this case we simply insert the animation before the current one has actually started |
355 | RETURN_IF_DELETED(setCurrentAnimation(anim)); |
356 | } |
357 | |
358 | // TODO |
359 | // if (index < m_currentAnimationIndex || m_currentLoop != 0) { |
360 | // qWarning("QSequentialGroup::insertAnimation only supports to add animations after the current one."); |
361 | // return; //we're not affected because it is added after the current one |
362 | // } |
363 | } |
364 | |
365 | void QSequentialAnimationGroupJob::animationRemoved(QAbstractAnimationJob *anim, QAbstractAnimationJob *prev, QAbstractAnimationJob *next) |
366 | { |
367 | QAnimationGroupJob::animationRemoved(anim, prev, next); |
368 | |
369 | Q_ASSERT(m_currentAnimation); // currentAnimation should always be set |
370 | |
371 | bool removingCurrent = anim == m_currentAnimation; |
372 | if (removingCurrent) { |
373 | if (next) |
374 | RETURN_IF_DELETED(setCurrentAnimation(next)); //let's try to take the next one |
375 | else if (prev) |
376 | RETURN_IF_DELETED(setCurrentAnimation(prev)); |
377 | else// case all animations were removed |
378 | RETURN_IF_DELETED(setCurrentAnimation(nullptr)); |
379 | } |
380 | |
381 | // duration of the previous animations up to the current animation |
382 | m_currentTime = 0; |
383 | for (QAbstractAnimationJob *job : m_children) { |
384 | if (job == m_currentAnimation) |
385 | break; |
386 | m_currentTime += animationActualTotalDuration(anim: job); |
387 | |
388 | } |
389 | |
390 | if (!removingCurrent) { |
391 | //the current animation is not the one being removed |
392 | //so we add its current time to the current time of this group |
393 | m_currentTime += m_currentAnimation->currentTime(); |
394 | } |
395 | |
396 | //let's also update the total current time |
397 | m_totalCurrentTime = m_currentTime + m_loopCount * duration(); |
398 | } |
399 | |
400 | void QSequentialAnimationGroupJob::debugAnimation(QDebug d) const |
401 | { |
402 | d << "SequentialAnimationGroupJob(" << Qt::hex << (const void *) this << Qt::dec << ")" << "currentAnimation:" << (void *)m_currentAnimation; |
403 | |
404 | debugChildren(d); |
405 | } |
406 | |
407 | QT_END_NAMESPACE |
408 | |