1// Copyright (C) 2017 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 "qquickmaterialbusyindicator_p.h"
5
6#include <QtCore/qmath.h>
7#include <QtCore/qeasingcurve.h>
8#include <QtGui/qpainter.h>
9#include <QtQuick/qsgimagenode.h>
10#include <QtQuick/qquickwindow.h>
11#include <QtQuickControls2Impl/private/qquickanimatednode_p.h>
12
13QT_BEGIN_NAMESPACE
14
15/*
16 Relevant Android code:
17
18 - core/res/res/anim/progress_indeterminate_rotation_material.xml contains
19 the rotation animation data.
20 - core/res/res/anim/progress_indeterminate_material.xml contains the trim
21 animation data.
22 - core/res/res/interpolator/trim_start_interpolator.xml and
23 core/res/res/interpolator/trim_end_interpolator.xml contain the start
24 and end trim path interpolators.
25 - addCommand() in core/java/android/util/PathParser.java has a list of the
26 different path commands available.
27*/
28
29static const int SpanAnimationDuration = 700;
30static const int RotationAnimationDuration = SpanAnimationDuration * 6;
31static const int TargetRotation = 720;
32static const int OneDegree = 16;
33static const qreal MinSweepSpan = 10 * OneDegree;
34static const qreal MaxSweepSpan = 300 * OneDegree;
35
36class QQuickMaterialBusyIndicatorNode : public QQuickAnimatedNode
37{
38public:
39 QQuickMaterialBusyIndicatorNode(QQuickMaterialBusyIndicator *item);
40
41 void sync(QQuickItem *item) override;
42
43protected:
44 void updateCurrentTime(int time) override;
45
46private:
47 int m_lastStartAngle = 0;
48 int m_lastEndAngle = 0;
49 qreal m_width = 0;
50 qreal m_height = 0;
51 qreal m_devicePixelRatio = 1;
52 QColor m_color;
53};
54
55QQuickMaterialBusyIndicatorNode::QQuickMaterialBusyIndicatorNode(QQuickMaterialBusyIndicator *item)
56 : QQuickAnimatedNode(item)
57{
58 setLoopCount(Infinite);
59 setCurrentTime(item->elapsed());
60 setDuration(RotationAnimationDuration);
61
62 QSGImageNode *textureNode = item->window()->createImageNode();
63 textureNode->setOwnsTexture(true);
64 appendChildNode(node: textureNode);
65
66 // A texture seems to be required here, but we don't have one yet, as we haven't drawn anything,
67 // so just use a blank image.
68 QImage blankImage(item->width(), item->height(), QImage::Format_ARGB32_Premultiplied);
69 blankImage.fill(color: Qt::transparent);
70 textureNode->setTexture(item->window()->createTextureFromImage(image: blankImage));
71}
72
73void QQuickMaterialBusyIndicatorNode::updateCurrentTime(int time)
74{
75 const qreal w = m_width;
76 const qreal h = m_height;
77 const qreal size = qMin(a: w, b: h);
78 const qreal dx = (w - size) / 2;
79 const qreal dy = (h - size) / 2;
80
81 QImage image(size * m_devicePixelRatio, size * m_devicePixelRatio, QImage::Format_ARGB32_Premultiplied);
82 image.fill(color: Qt::transparent);
83
84 QPainter painter(&image);
85 painter.setRenderHint(hint: QPainter::Antialiasing);
86
87 QPen pen;
88 QSGImageNode *textureNode = static_cast<QSGImageNode *>(firstChild());
89 pen.setColor(m_color);
90 pen.setWidth(qCeil(v: size / 12) * m_devicePixelRatio);
91 painter.setPen(pen);
92
93 const qreal percentageComplete = time / qreal(RotationAnimationDuration);
94 const qreal spanPercentageComplete = (time % SpanAnimationDuration) / qreal(SpanAnimationDuration);
95 const int iteration = time / SpanAnimationDuration;
96 int startAngle = 0;
97 int endAngle = 0;
98
99 if (iteration % 2 == 0) {
100 if (m_lastStartAngle > 360 * OneDegree)
101 m_lastStartAngle -= 360 * OneDegree;
102
103 // The start angle is only affected by the rotation animation for the "grow" phase.
104 startAngle = m_lastStartAngle;
105 QEasingCurve angleCurve(QEasingCurve::OutQuad);
106 const qreal percentage = angleCurve.valueForProgress(progress: spanPercentageComplete);
107 endAngle = m_lastStartAngle + MinSweepSpan + percentage * (MaxSweepSpan - MinSweepSpan);
108 m_lastEndAngle = endAngle;
109 } else {
110 // Both the start angle *and* the span are affected by the "shrink" phase.
111 QEasingCurve angleCurve(QEasingCurve::InQuad);
112 const qreal percentage = angleCurve.valueForProgress(progress: spanPercentageComplete);
113 startAngle = m_lastEndAngle - MaxSweepSpan + percentage * (MaxSweepSpan - MinSweepSpan);
114 endAngle = m_lastEndAngle;
115 m_lastStartAngle = startAngle;
116 }
117
118 const int halfPen = pen.width() / 2;
119 const QRectF arcBounds = QRectF(halfPen, halfPen,
120 m_devicePixelRatio * size - pen.width(),
121 m_devicePixelRatio * size - pen.width());
122 // The current angle of the rotation animation.
123 const qreal rotation = OneDegree * percentageComplete * -TargetRotation;
124 startAngle -= rotation;
125 endAngle -= rotation;
126 const int angleSpan = endAngle - startAngle;
127 painter.drawArc(rect: arcBounds, a: -startAngle, alen: -angleSpan);
128 painter.end();
129
130 textureNode->setRect(QRectF(dx, dy, size, size));
131 textureNode->setTexture(window()->createTextureFromImage(image));
132}
133
134void QQuickMaterialBusyIndicatorNode::sync(QQuickItem *item)
135{
136 QQuickMaterialBusyIndicator *indicator = static_cast<QQuickMaterialBusyIndicator *>(item);
137 m_color = indicator->color();
138 m_width = indicator->width();
139 m_height = indicator->height();
140 m_devicePixelRatio = indicator->window()->effectiveDevicePixelRatio();
141}
142
143QQuickMaterialBusyIndicator::QQuickMaterialBusyIndicator(QQuickItem *parent) :
144 QQuickItem(parent)
145{
146 setFlag(flag: ItemHasContents);
147}
148
149QColor QQuickMaterialBusyIndicator::color() const
150{
151 return m_color;
152}
153
154void QQuickMaterialBusyIndicator::setColor(const QColor &color)
155{
156 if (m_color == color)
157 return;
158
159 m_color = color;
160 update();
161}
162
163bool QQuickMaterialBusyIndicator::isRunning() const
164{
165 return isVisible();
166}
167
168void QQuickMaterialBusyIndicator::setRunning(bool running)
169{
170 if (running)
171 setVisible(true);
172}
173
174int QQuickMaterialBusyIndicator::elapsed() const
175{
176 return m_elapsed;
177}
178
179void QQuickMaterialBusyIndicator::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
180{
181 QQuickItem::itemChange(change, data);
182 switch (change) {
183 case ItemOpacityHasChanged:
184 if (qFuzzyIsNull(d: data.realValue))
185 setVisible(false);
186 break;
187 case ItemVisibleHasChanged:
188 update();
189 break;
190 default:
191 break;
192 }
193}
194
195QSGNode *QQuickMaterialBusyIndicator::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
196{
197 QQuickMaterialBusyIndicatorNode *node = static_cast<QQuickMaterialBusyIndicatorNode *>(oldNode);
198 if (isRunning() && width() > 0 && height() > 0) {
199 if (!node) {
200 node = new QQuickMaterialBusyIndicatorNode(this);
201 node->start();
202 }
203 node->sync(item: this);
204 } else {
205 m_elapsed = node ? node->currentTime() : 0;
206 delete node;
207 node = nullptr;
208 }
209 return node;
210}
211
212QT_END_NAMESPACE
213
214#include "moc_qquickmaterialbusyindicator_p.cpp"
215

source code of qtdeclarative/src/quickcontrols/material/impl/qquickmaterialbusyindicator.cpp