1 | // Copyright (C) 2022 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 "qquickframeanimation_p.h" |
5 | |
6 | #include <QtCore/qcoreapplication.h> |
7 | #include <QtCore/qelapsedtimer.h> |
8 | #include "private/qabstractanimationjob_p.h" |
9 | #include <private/qobject_p.h> |
10 | #include <qdebug.h> |
11 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | class QFrameAnimationJob : public QAbstractAnimationJob |
15 | { |
16 | int duration() const override { |
17 | return 1; |
18 | } |
19 | }; |
20 | |
21 | class QQuickFrameAnimationPrivate : public QObjectPrivate, public QAnimationJobChangeListener |
22 | { |
23 | Q_DECLARE_PUBLIC(QQuickFrameAnimation) |
24 | public: |
25 | QQuickFrameAnimationPrivate() {} |
26 | |
27 | void animationCurrentLoopChanged(QAbstractAnimationJob *) override { |
28 | maybeTick(); |
29 | } |
30 | |
31 | void maybeTick() |
32 | { |
33 | Q_Q(QQuickFrameAnimation); |
34 | if (!running || paused) |
35 | return; |
36 | |
37 | qint64 elapsedTimeNs = elapsedTimer.nsecsElapsed(); |
38 | qint64 frameTimeNs = elapsedTimeNs - prevElapsedTimeNs; |
39 | if (prevFrameTimeNs != frameTimeNs) { |
40 | frameTime = qreal(frameTimeNs) / 1000000000.0; |
41 | Q_EMIT q->frameTimeChanged(); |
42 | } |
43 | |
44 | const qreal f = 0.1; |
45 | qreal newSmoothFrameTime = f * frameTime + (1.0 - f) * smoothFrameTime; |
46 | if (!qFuzzyCompare(p1: newSmoothFrameTime, p2: smoothFrameTime)) { |
47 | smoothFrameTime = newSmoothFrameTime; |
48 | Q_EMIT q->smoothFrameTimeChanged(); |
49 | } |
50 | |
51 | q->setElapsedTime(elapsedTime + frameTime); |
52 | |
53 | const int frame = (firstTick && currentFrame > 0) ? 0 : currentFrame + 1; |
54 | q->setCurrentFrame(frame); |
55 | |
56 | prevElapsedTimeNs = elapsedTimeNs; |
57 | prevFrameTimeNs = frameTimeNs; |
58 | firstTick = false; |
59 | |
60 | Q_EMIT q->triggered(); |
61 | } |
62 | |
63 | // Handle the running/pausing state updates. |
64 | void updateState() |
65 | { |
66 | if (!componentComplete) |
67 | return; |
68 | |
69 | if (running && !paused) { |
70 | if (firstTick) { |
71 | elapsedTime = 0; |
72 | elapsedTimer.start(); |
73 | } |
74 | prevElapsedTimeNs = elapsedTimer.nsecsElapsed(); |
75 | frameJob.start(); |
76 | } else { |
77 | frameJob.stop(); |
78 | } |
79 | } |
80 | |
81 | private: |
82 | QFrameAnimationJob frameJob; |
83 | QElapsedTimer elapsedTimer; |
84 | int currentFrame = 0; |
85 | qreal frameTime = 0.0; |
86 | qreal smoothFrameTime = 0.0; |
87 | qreal elapsedTime = 0.0; |
88 | qint64 prevFrameTimeNs = 0; |
89 | qint64 prevElapsedTimeNs = 0; |
90 | bool running = false; |
91 | bool paused = false; |
92 | bool componentComplete = false; |
93 | bool firstTick = true; |
94 | }; |
95 | |
96 | /*! |
97 | \qmltype FrameAnimation |
98 | \instantiates QQuickFrameAnimation |
99 | \inqmlmodule QtQuick |
100 | \ingroup qtquick-interceptors |
101 | \since 6.4 |
102 | \brief Triggers a handler at every animation frame update. |
103 | |
104 | A FrameAnimation can be used to trigger an action every time animations |
105 | have progressed and an animation frame has been rendered. See the documentation |
106 | about the \l{qtquick-visualcanvas-scenegraph.html}{Scene Graph} for in-depth |
107 | information about the threaded and basic render loops. |
108 | |
109 | For general animations, prefer using \c NumberAnimation and other \c Animation |
110 | elements as those provide declarative way to describe the animations. |
111 | |
112 | FrameAnimation on the other hand should be used for custom imperative animations |
113 | and in use-cases like these: |
114 | \list |
115 | \li When you need to run some code on every frame update. Or e.g. every other frame, |
116 | maybe using progressive rendering. |
117 | \li When the speed / target is changing during the animation, normal QML animations |
118 | can be too limiting. |
119 | \li When more accurate frame update time is needed, e.g. for fps counter. |
120 | \endlist |
121 | |
122 | Compared to \c Timer which allows to set the \c interval time, FrameAnimation runs |
123 | always in synchronization with the animation updates. If you have used \c Timer |
124 | with a short interval for custom animations like below, please consider switching |
125 | to use FrameAnimation instead for smoother animations. |
126 | \code |
127 | // BAD |
128 | Timer { |
129 | interval: 16 |
130 | repeat: true |
131 | running: true |
132 | onTriggered: { |
133 | // Animate something |
134 | } |
135 | } |
136 | |
137 | // GOOD |
138 | FrameAnimation { |
139 | running: true |
140 | onTriggered: { |
141 | // Animate something |
142 | } |
143 | } |
144 | \endcode |
145 | */ |
146 | QQuickFrameAnimation::QQuickFrameAnimation(QObject *parent) |
147 | : QObject(*(new QQuickFrameAnimationPrivate), parent) |
148 | { |
149 | Q_D(QQuickFrameAnimation); |
150 | d->frameJob.addAnimationChangeListener(listener: d, QAbstractAnimationJob::CurrentLoop); |
151 | d->frameJob.setLoopCount(-1); |
152 | } |
153 | |
154 | /*! |
155 | \qmlsignal QtQuick::FrameAnimation::triggered() |
156 | |
157 | This signal is emitted when the FrameAnimation has progressed to a new frame. |
158 | */ |
159 | |
160 | /*! |
161 | \qmlproperty bool QtQuick::FrameAnimation::running |
162 | |
163 | If set to true, starts the frame animation; otherwise stops it. |
164 | |
165 | \a running defaults to false. |
166 | |
167 | \sa stop(), start(), restart() |
168 | */ |
169 | bool QQuickFrameAnimation::isRunning() const |
170 | { |
171 | Q_D(const QQuickFrameAnimation); |
172 | return d->running; |
173 | } |
174 | |
175 | void QQuickFrameAnimation::setRunning(bool running) |
176 | { |
177 | Q_D(QQuickFrameAnimation); |
178 | if (d->running != running) { |
179 | d->running = running; |
180 | d->firstTick = true; |
181 | Q_EMIT runningChanged(); |
182 | d->updateState(); |
183 | } |
184 | } |
185 | |
186 | /*! |
187 | \qmlproperty bool QtQuick::FrameAnimation::paused |
188 | |
189 | If set to true, pauses the frame animation; otherwise resumes it. |
190 | |
191 | \a paused defaults to false. |
192 | |
193 | \sa pause(), resume() |
194 | */ |
195 | bool QQuickFrameAnimation::isPaused() const |
196 | { |
197 | Q_D(const QQuickFrameAnimation); |
198 | return d->paused; |
199 | } |
200 | |
201 | void QQuickFrameAnimation::setPaused(bool paused) |
202 | { |
203 | Q_D(QQuickFrameAnimation); |
204 | if (d->paused != paused) { |
205 | d->paused = paused; |
206 | Q_EMIT pausedChanged(); |
207 | d->updateState(); |
208 | } |
209 | } |
210 | |
211 | /*! |
212 | \qmlproperty int QtQuick::FrameAnimation::currentFrame |
213 | \readonly |
214 | |
215 | This property holds the number of frame updates since the start. |
216 | When the frame animation is restarted, currentFrame starts from \c 0. |
217 | |
218 | The following example shows how to react on frame updates. |
219 | |
220 | \code |
221 | FrameAnimation { |
222 | running: true |
223 | onTriggered: { |
224 | // Run code on every frame update. |
225 | } |
226 | } |
227 | \endcode |
228 | |
229 | This property can also be used for rendering only every nth frame. Consider an |
230 | advanced usage where the UI contains two heavy elements and to reach smooth 60fps |
231 | overall frame rate, you decide to render these heavy elements at 30fps, first one |
232 | on every even frames and second one on every odd frames: |
233 | |
234 | \code |
235 | FrameAnimation { |
236 | running: true |
237 | onTriggered: { |
238 | if (currentFrame % 2 == 0) |
239 | updateUIElement1(); |
240 | else |
241 | updateUIElement2(); |
242 | } |
243 | } |
244 | \endcode |
245 | |
246 | By default, \c frame is 0. |
247 | */ |
248 | int QQuickFrameAnimation::currentFrame() const |
249 | { |
250 | Q_D(const QQuickFrameAnimation); |
251 | return d->currentFrame; |
252 | } |
253 | |
254 | /*! |
255 | \qmlproperty qreal QtQuick::FrameAnimation::frameTime |
256 | \readonly |
257 | |
258 | This property holds the time (in seconds) since the previous frame update. |
259 | |
260 | The following example shows how to use frameTime to animate item with |
261 | varying speed, adjusting to screen refresh rates and possible fps drops. |
262 | |
263 | \code |
264 | Rectangle { |
265 | id: rect |
266 | property real speed: 90 |
267 | width: 100 |
268 | height: 100 |
269 | color: "red" |
270 | anchors.centerIn: parent |
271 | } |
272 | |
273 | FrameAnimation { |
274 | id: frameAnimation |
275 | running: true |
276 | onTriggered: { |
277 | // Rotate the item speed-degrees / second. |
278 | rect.rotation += rect.speed * frameTime |
279 | } |
280 | } |
281 | \endcode |
282 | |
283 | By default, \c frameTime is 0. |
284 | */ |
285 | qreal QQuickFrameAnimation::frameTime() const |
286 | { |
287 | Q_D(const QQuickFrameAnimation); |
288 | return d->frameTime; |
289 | } |
290 | |
291 | /*! |
292 | \qmlproperty qreal QtQuick::FrameAnimation::smoothFrameTime |
293 | \readonly |
294 | |
295 | This property holds the smoothed time (in seconds) since the previous frame update. |
296 | |
297 | The following example shows how to use smoothFrameTime to show average fps. |
298 | |
299 | \code |
300 | Text { |
301 | text: "fps: " + frameAnimation.fps.toFixed(0) |
302 | } |
303 | |
304 | FrameAnimation { |
305 | id: frameAnimation |
306 | property real fps: smoothFrameTime > 0 ? (1.0 / smoothFrameTime) : 0 |
307 | running: true |
308 | } |
309 | \endcode |
310 | |
311 | By default, \c smoothFrameTime is 0. |
312 | */ |
313 | qreal QQuickFrameAnimation::smoothFrameTime() const |
314 | { |
315 | Q_D(const QQuickFrameAnimation); |
316 | return d->smoothFrameTime; |
317 | } |
318 | |
319 | /*! |
320 | \qmlproperty qreal QtQuick::FrameAnimation::elapsedTime |
321 | \readonly |
322 | |
323 | This property holds the time (in seconds) since the previous start. |
324 | |
325 | By default, \c elapsedTime is 0. |
326 | */ |
327 | qreal QQuickFrameAnimation::elapsedTime() const |
328 | { |
329 | Q_D(const QQuickFrameAnimation); |
330 | return d->elapsedTime; |
331 | } |
332 | |
333 | /*! |
334 | \qmlmethod QtQuick::FrameAnimation::start() |
335 | \brief Starts the frame animation |
336 | |
337 | If the frame animation is already running, calling this method has no effect. The |
338 | \c running property will be true following a call to \c start(). |
339 | */ |
340 | void QQuickFrameAnimation::start() |
341 | { |
342 | setRunning(true); |
343 | } |
344 | |
345 | /*! |
346 | \qmlmethod QtQuick::FrameAnimation::stop() |
347 | \brief Stops the frame animation |
348 | |
349 | If the frame animation is not running, calling this method has no effect. Both the \c running and |
350 | \c paused properties will be false following a call to \c stop(). |
351 | */ |
352 | void QQuickFrameAnimation::stop() |
353 | { |
354 | setRunning(false); |
355 | setPaused(false); |
356 | } |
357 | |
358 | /*! |
359 | \qmlmethod QtQuick::FrameAnimation::restart() |
360 | \brief Restarts the frame animation |
361 | |
362 | If the FrameAnimation is not running it will be started, otherwise it will be |
363 | stopped, reset to initial state and started. The \c running property |
364 | will be true following a call to \c restart(). |
365 | */ |
366 | void QQuickFrameAnimation::restart() |
367 | { |
368 | stop(); |
369 | start(); |
370 | } |
371 | |
372 | /*! |
373 | \qmlmethod QtQuick::FrameAnimation::pause() |
374 | \brief Pauses the frame animation |
375 | |
376 | If the frame animation is already paused or not \c running, calling this method has no effect. |
377 | The \c paused property will be true following a call to \c pause(). |
378 | */ |
379 | void QQuickFrameAnimation::pause() |
380 | { |
381 | setPaused(true); |
382 | } |
383 | |
384 | /*! |
385 | \qmlmethod QtQuick::FrameAnimation::resume() |
386 | \brief Resumes a paused frame animation |
387 | |
388 | If the frame animation is not paused or not \c running, calling this method has no effect. |
389 | The \c paused property will be false following a call to \c resume(). |
390 | */ |
391 | void QQuickFrameAnimation::resume() |
392 | { |
393 | setPaused(false); |
394 | } |
395 | |
396 | /*! |
397 | \qmlmethod QtQuick::FrameAnimation::reset() |
398 | \brief Resets the frame animation properties |
399 | |
400 | Calling this method resets the \c frame and \c elapsedTime to their initial |
401 | values (0). This method has no effect on \c running or \c paused properties |
402 | and can be called while they are true or false. |
403 | |
404 | The difference between calling \c reset() and \c restart() is that \c reset() |
405 | will always initialize the properties while \c restart() initializes them only |
406 | at the next frame update which doesn't happen e.g. if \c restart() is |
407 | immediately followed by \c pause(). |
408 | */ |
409 | void QQuickFrameAnimation::reset() |
410 | { |
411 | Q_D(QQuickFrameAnimation); |
412 | setElapsedTime(0); |
413 | setCurrentFrame(0); |
414 | d->prevElapsedTimeNs = 0; |
415 | d->elapsedTimer.start(); |
416 | } |
417 | |
418 | /*! |
419 | \internal |
420 | */ |
421 | void QQuickFrameAnimation::classBegin() |
422 | { |
423 | Q_D(QQuickFrameAnimation); |
424 | d->componentComplete = false; |
425 | } |
426 | |
427 | /*! |
428 | \internal |
429 | */ |
430 | void QQuickFrameAnimation::componentComplete() |
431 | { |
432 | Q_D(QQuickFrameAnimation); |
433 | d->componentComplete = true; |
434 | d->updateState(); |
435 | } |
436 | |
437 | /*! |
438 | \internal |
439 | */ |
440 | void QQuickFrameAnimation::setCurrentFrame(int frame) |
441 | { |
442 | Q_D(QQuickFrameAnimation); |
443 | if (d->currentFrame != frame) { |
444 | d->currentFrame = frame; |
445 | Q_EMIT currentFrameChanged(); |
446 | } |
447 | } |
448 | |
449 | /*! |
450 | \internal |
451 | */ |
452 | void QQuickFrameAnimation::setElapsedTime(qreal elapsedTime) |
453 | { |
454 | Q_D(QQuickFrameAnimation); |
455 | if (!qFuzzyCompare(p1: d->elapsedTime, p2: elapsedTime)) { |
456 | d->elapsedTime = elapsedTime; |
457 | Q_EMIT elapsedTimeChanged(); |
458 | } |
459 | } |
460 | |
461 | QT_END_NAMESPACE |
462 | |
463 | #include "moc_qquickframeanimation_p.cpp" |
464 | |