| 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 |  | 
| 13 | QT_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 |  | 
| 29 | static const int SpanAnimationDuration = 700; | 
| 30 | static const int RotationAnimationDuration = SpanAnimationDuration * 6; | 
| 31 | static const int TargetRotation = 720; | 
| 32 | static const int OneDegree = 16; | 
| 33 | static const qreal MinSweepSpan = 10 * OneDegree; | 
| 34 | static const qreal MaxSweepSpan = 300 * OneDegree; | 
| 35 |  | 
| 36 | class QQuickMaterialBusyIndicatorNode : public QQuickAnimatedNode | 
| 37 | { | 
| 38 | public: | 
| 39 |     QQuickMaterialBusyIndicatorNode(QQuickMaterialBusyIndicator *item); | 
| 40 |  | 
| 41 |     void sync(QQuickItem *item) override; | 
| 42 |  | 
| 43 | protected: | 
| 44 |     void updateCurrentTime(int time) override; | 
| 45 |  | 
| 46 | private: | 
| 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 |  | 
| 55 | QQuickMaterialBusyIndicatorNode::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 |  | 
| 73 | void 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 |  | 
| 134 | void 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 |  | 
| 143 | QQuickMaterialBusyIndicator::QQuickMaterialBusyIndicator(QQuickItem *parent) : | 
| 144 |     QQuickItem(parent) | 
| 145 | { | 
| 146 |     setFlag(flag: ItemHasContents); | 
| 147 | } | 
| 148 |  | 
| 149 | QColor QQuickMaterialBusyIndicator::color() const | 
| 150 | { | 
| 151 |     return m_color; | 
| 152 | } | 
| 153 |  | 
| 154 | void QQuickMaterialBusyIndicator::setColor(const QColor &color) | 
| 155 | { | 
| 156 |     if (m_color == color) | 
| 157 |         return; | 
| 158 |  | 
| 159 |     m_color = color; | 
| 160 |     update(); | 
| 161 | } | 
| 162 |  | 
| 163 | bool QQuickMaterialBusyIndicator::isRunning() const | 
| 164 | { | 
| 165 |     return m_running; | 
| 166 | } | 
| 167 |  | 
| 168 | void QQuickMaterialBusyIndicator::setRunning(bool running) | 
| 169 | { | 
| 170 |     m_running = running; | 
| 171 |  | 
| 172 |     if (m_running) | 
| 173 |         setVisible(true); | 
| 174 |     // Don't set visible to false if not running, because we use an opacity animation (in QML) to hide ourselves. | 
| 175 | } | 
| 176 |  | 
| 177 | int QQuickMaterialBusyIndicator::elapsed() const | 
| 178 | { | 
| 179 |     return m_elapsed; | 
| 180 | } | 
| 181 |  | 
| 182 | void QQuickMaterialBusyIndicator::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) | 
| 183 | { | 
| 184 |     QQuickItem::itemChange(change, data); | 
| 185 |     switch (change) { | 
| 186 |     case ItemOpacityHasChanged: | 
| 187 |         // If running is set to false and then true within a short period (QTBUG-85860), our | 
| 188 |         // OpacityAnimator cancels the 1 => 0 animation (which was for running being set to false), | 
| 189 |         // setting opacity to 0 and hence visible to false. This happens _after_ setRunning(true) | 
| 190 |         // was called, because the properties were set synchronously but the animation is | 
| 191 |         // asynchronous. To account for this situation, we only hide ourselves if we're not running. | 
| 192 |         if (qFuzzyIsNull(d: data.realValue) && !m_running) | 
| 193 |             setVisible(false); | 
| 194 |         break; | 
| 195 |     case ItemVisibleHasChanged: | 
| 196 |         update(); | 
| 197 |         break; | 
| 198 |     default: | 
| 199 |         break; | 
| 200 |     } | 
| 201 | } | 
| 202 |  | 
| 203 | QSGNode *QQuickMaterialBusyIndicator::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) | 
| 204 | { | 
| 205 |     QQuickMaterialBusyIndicatorNode *node = static_cast<QQuickMaterialBusyIndicatorNode *>(oldNode); | 
| 206 |     if (isRunning() && width() > 0 && height() > 0) { | 
| 207 |         if (!node) { | 
| 208 |             node = new QQuickMaterialBusyIndicatorNode(this); | 
| 209 |             node->start(); | 
| 210 |         } | 
| 211 |         node->sync(item: this); | 
| 212 |     } else { | 
| 213 |         m_elapsed = node ? node->currentTime() : 0; | 
| 214 |         delete node; | 
| 215 |         node = nullptr; | 
| 216 |     } | 
| 217 |     return node; | 
| 218 | } | 
| 219 |  | 
| 220 | QT_END_NAMESPACE | 
| 221 |  | 
| 222 | #include "moc_qquickmaterialbusyindicator_p.cpp" | 
| 223 |  |