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 "qquickmaterialripple_p.h"
5
6#include <QtCore/qmath.h>
7#include <QtQuick/private/qquickitem_p.h>
8#include <QtQuick/private/qsgadaptationlayer_p.h>
9#include <QtQuickControls2Impl/private/qquickanimatednode_p.h>
10#include <QtQuickTemplates2/private/qquickabstractbutton_p.h>
11#include <QtQuickTemplates2/private/qquickabstractbutton_p_p.h>
12
13QT_BEGIN_NAMESPACE
14
15enum WavePhase { WaveEnter, WaveExit };
16
17static const int RIPPLE_ENTER_DELAY = 80;
18static const int OPACITY_ENTER_DURATION_FAST = 120;
19static const int WAVE_OPACITY_DECAY_DURATION = 333;
20static const qreal WAVE_TOUCH_DOWN_ACCELERATION = 1024.0;
21
22class QQuickMaterialRippleWaveNode : public QQuickAnimatedNode
23{
24public:
25 QQuickMaterialRippleWaveNode(QQuickMaterialRipple *ripple);
26
27 void exit();
28 void updateCurrentTime(int time) override;
29 void sync(QQuickItem *item) override;
30
31private:
32 qreal m_from = 0;
33 qreal m_to = 0;
34 qreal m_value = 0;
35 WavePhase m_phase = WaveEnter;
36 QPointF m_anchor;
37 QRectF m_bounds;
38};
39
40QQuickMaterialRippleWaveNode::QQuickMaterialRippleWaveNode(QQuickMaterialRipple *ripple)
41 : QQuickAnimatedNode(ripple)
42{
43 start(duration: qRound(d: 1000.0 * qSqrt(v: ripple->diameter() / 2.0 / WAVE_TOUCH_DOWN_ACCELERATION)));
44
45 QSGOpacityNode *opacityNode = new QSGOpacityNode;
46 appendChildNode(node: opacityNode);
47
48 QQuickItemPrivate *d = QQuickItemPrivate::get(item: ripple);
49 QSGInternalRectangleNode *rectNode = d->sceneGraphContext()->createInternalRectangleNode();
50 rectNode->setAntialiasing(true);
51 opacityNode->appendChildNode(node: rectNode);
52}
53
54void QQuickMaterialRippleWaveNode::exit()
55{
56 m_phase = WaveExit;
57 m_from = m_value;
58 setDuration(WAVE_OPACITY_DECAY_DURATION);
59 restart();
60 connect(sender: this, signal: &QQuickAnimatedNode::stopped, context: this, slot: &QObject::deleteLater);
61}
62
63void QQuickMaterialRippleWaveNode::updateCurrentTime(int time)
64{
65 qreal p = 1.0;
66 if (duration() > 0)
67 p = time / static_cast<qreal>(duration());
68
69 m_value = m_from + (m_to - m_from) * p;
70 p = m_value / m_to;
71
72 const qreal dx = (1.0 - p) * (m_anchor.x() - m_bounds.width() / 2);
73 const qreal dy = (1.0 - p) * (m_anchor.y() - m_bounds.height() / 2);
74
75 QMatrix4x4 m;
76 m.translate(x: qRound(d: (m_bounds.width() - m_value) / 2 + dx),
77 y: qRound(d: (m_bounds.height() - m_value) / 2 + dy));
78 setMatrix(m);
79
80 QSGOpacityNode *opacityNode = static_cast<QSGOpacityNode *>(firstChild());
81 Q_ASSERT(opacityNode->type() == QSGNode::OpacityNodeType);
82 qreal opacity = 1.0;
83 if (m_phase == WaveExit)
84 opacity -= static_cast<qreal>(time) / WAVE_OPACITY_DECAY_DURATION;
85 opacityNode->setOpacity(opacity);
86
87 QSGInternalRectangleNode *rectNode = static_cast<QSGInternalRectangleNode *>(opacityNode->firstChild());
88 Q_ASSERT(rectNode->type() == QSGNode::GeometryNodeType);
89 rectNode->setRect(QRectF(0, 0, m_value, m_value));
90 rectNode->setRadius(m_value / 2);
91 rectNode->update();
92}
93
94void QQuickMaterialRippleWaveNode::sync(QQuickItem *item)
95{
96 QQuickMaterialRipple *ripple = static_cast<QQuickMaterialRipple *>(item);
97 m_to = ripple->diameter();
98 m_anchor = ripple->anchorPoint();
99 m_bounds = ripple->boundingRect();
100
101 QSGOpacityNode *opacityNode = static_cast<QSGOpacityNode *>(firstChild());
102 Q_ASSERT(opacityNode->type() == QSGNode::OpacityNodeType);
103
104 QSGInternalRectangleNode *rectNode = static_cast<QSGInternalRectangleNode *>(opacityNode->firstChild());
105 Q_ASSERT(rectNode->type() == QSGNode::GeometryNodeType);
106 rectNode->setColor(ripple->color());
107}
108
109class QQuickMaterialRippleBackgroundNode : public QQuickAnimatedNode
110{
111 Q_OBJECT
112
113public:
114 QQuickMaterialRippleBackgroundNode(QQuickMaterialRipple *ripple);
115
116 void updateCurrentTime(int time) override;
117 void sync(QQuickItem *item) override;
118
119private:
120 bool m_active = false;
121};
122
123QQuickMaterialRippleBackgroundNode::QQuickMaterialRippleBackgroundNode(QQuickMaterialRipple *ripple)
124 : QQuickAnimatedNode(ripple)
125{
126 setDuration(OPACITY_ENTER_DURATION_FAST);
127
128 QSGOpacityNode *opacityNode = new QSGOpacityNode;
129 opacityNode->setOpacity(0.0);
130 appendChildNode(node: opacityNode);
131
132 QQuickItemPrivate *d = QQuickItemPrivate::get(item: ripple);
133 QSGInternalRectangleNode *rectNode = d->sceneGraphContext()->createInternalRectangleNode();
134 rectNode->setAntialiasing(true);
135 opacityNode->appendChildNode(node: rectNode);
136}
137
138void QQuickMaterialRippleBackgroundNode::updateCurrentTime(int time)
139{
140 qreal opacity = time / static_cast<qreal>(duration());
141 if (!m_active)
142 opacity = 1.0 - opacity;
143
144 QSGOpacityNode *opacityNode = static_cast<QSGOpacityNode *>(firstChild());
145 Q_ASSERT(opacityNode->type() == QSGNode::OpacityNodeType);
146 opacityNode->setOpacity(opacity);
147}
148
149void QQuickMaterialRippleBackgroundNode::sync(QQuickItem *item)
150{
151 QQuickMaterialRipple *ripple = static_cast<QQuickMaterialRipple *>(item);
152 if (m_active != ripple->isActive()) {
153 m_active = ripple->isActive();
154 setDuration(m_active ? OPACITY_ENTER_DURATION_FAST : WAVE_OPACITY_DECAY_DURATION);
155 restart();
156 }
157
158 QSGOpacityNode *opacityNode = static_cast<QSGOpacityNode *>(firstChild());
159 Q_ASSERT(opacityNode->type() == QSGNode::OpacityNodeType);
160
161 QSGInternalRectangleNode *rectNode = static_cast<QSGInternalRectangleNode *>(opacityNode->firstChild());
162 Q_ASSERT(rectNode->type() == QSGNode::GeometryNodeType);
163
164 const qreal w = ripple->width();
165 const qreal h = ripple->height();
166 const qreal sz = qSqrt(v: w * w + h * h);
167
168 QMatrix4x4 matrix;
169 if (qFuzzyIsNull(d: ripple->clipRadius())) {
170 matrix.translate(x: qRound(d: (w - sz) / 2), y: qRound(d: (h - sz) / 2));
171 rectNode->setRect(QRectF(0, 0, sz, sz));
172 rectNode->setRadius(sz / 2);
173 } else {
174 rectNode->setRect(QRectF(0, 0, w, h));
175 rectNode->setRadius(ripple->clipRadius());
176 }
177
178 setMatrix(matrix);
179 rectNode->setColor(ripple->color());
180 rectNode->update();
181}
182
183QQuickMaterialRipple::QQuickMaterialRipple(QQuickItem *parent)
184 : QQuickItem(parent)
185{
186 setFlag(flag: ItemHasContents);
187}
188
189bool QQuickMaterialRipple::isActive() const
190{
191 return m_active;
192}
193
194void QQuickMaterialRipple::setActive(bool active)
195{
196 if (active == m_active)
197 return;
198
199 m_active = active;
200 update();
201}
202
203QColor QQuickMaterialRipple::color() const
204{
205 return m_color;
206}
207
208void QQuickMaterialRipple::setColor(const QColor &color)
209{
210 if (m_color == color)
211 return;
212
213 m_color = color;
214 update();
215}
216
217qreal QQuickMaterialRipple::clipRadius() const
218{
219 return m_clipRadius;
220}
221
222void QQuickMaterialRipple::setClipRadius(qreal radius)
223{
224 if (qFuzzyCompare(p1: m_clipRadius, p2: radius))
225 return;
226
227 m_clipRadius = radius;
228 update();
229}
230
231bool QQuickMaterialRipple::isPressed() const
232{
233 return m_pressed;
234}
235
236void QQuickMaterialRipple::setPressed(bool pressed)
237{
238 if (pressed == m_pressed)
239 return;
240
241 m_pressed = pressed;
242
243 if (!isEnabled()) {
244 exitWave();
245 return;
246 }
247
248 if (pressed) {
249 if (m_trigger == Press)
250 prepareWave();
251 else
252 exitWave();
253 } else {
254 if (m_trigger == Release)
255 enterWave();
256 else
257 exitWave();
258 }
259}
260
261QQuickMaterialRipple::Trigger QQuickMaterialRipple::trigger() const
262{
263 return m_trigger;
264}
265
266void QQuickMaterialRipple::setTrigger(Trigger trigger)
267{
268 m_trigger = trigger;
269}
270
271QPointF QQuickMaterialRipple::anchorPoint() const
272{
273 const QRectF bounds = boundingRect();
274 const QPointF center = bounds.center();
275 if (!m_anchor)
276 return center;
277
278 QPointF anchorPoint = bounds.center();
279 if (QQuickAbstractButton *button = qobject_cast<QQuickAbstractButton *>(object: m_anchor))
280 anchorPoint = QQuickAbstractButtonPrivate::get(button)->pressPoint;
281 anchorPoint = mapFromItem(item: m_anchor, point: anchorPoint);
282
283 // calculate whether the anchor point is within the ripple circle bounds,
284 // that is, whether waves should start expanding from the anchor point
285 const qreal r = qSqrt(v: bounds.width() * bounds.width() + bounds.height() * bounds.height()) / 2;
286 if (QLineF(center, anchorPoint).length() < r)
287 return anchorPoint;
288
289 // if the anchor point is outside the ripple circle bounds, start expanding
290 // from the intersection point of the ripple circle and a line from its center
291 // to the the anchor point
292 const qreal p = qAtan2(y: anchorPoint.y() - center.y(), x: anchorPoint.x() - center.x());
293 return QPointF(center.x() + r * qCos(v: p), center.y() + r * qSin(v: p));
294}
295
296QQuickItem *QQuickMaterialRipple::anchor() const
297{
298 return m_anchor;
299}
300
301void QQuickMaterialRipple::setAnchor(QQuickItem *item)
302{
303 m_anchor = item;
304}
305
306qreal QQuickMaterialRipple::diameter() const
307{
308 const qreal w = width();
309 const qreal h = height();
310 return qSqrt(v: w * w + h * h);
311}
312
313void QQuickMaterialRipple::itemChange(ItemChange change, const ItemChangeData &data)
314{
315 QQuickItem::itemChange(change, data);
316}
317
318QSGNode *QQuickMaterialRipple::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
319{
320 QQuickItemPrivate *d = QQuickItemPrivate::get(item: this);
321 QQuickDefaultClipNode *clipNode = d->clipNode();
322 if (clipNode) {
323 clipNode->setRadius(m_clipRadius);
324 clipNode->setRect(boundingRect());
325 clipNode->update();
326 }
327
328 QSGNode *container = oldNode;
329 if (!container)
330 container = new QSGNode;
331
332 QQuickMaterialRippleBackgroundNode *backgroundNode = static_cast<QQuickMaterialRippleBackgroundNode *>(container->firstChild());
333 if (!backgroundNode) {
334 backgroundNode = new QQuickMaterialRippleBackgroundNode(this);
335 backgroundNode->setObjectName(objectName());
336 container->appendChildNode(node: backgroundNode);
337 }
338 backgroundNode->sync(item: this);
339
340 // enter new waves
341 int i = m_waves;
342 QQuickMaterialRippleWaveNode *enterNode = static_cast<QQuickMaterialRippleWaveNode *>(backgroundNode->nextSibling());
343 while (i-- > 0) {
344 if (!enterNode) {
345 enterNode = new QQuickMaterialRippleWaveNode(this);
346 container->appendChildNode(node: enterNode);
347 }
348 enterNode->sync(item: this);
349 enterNode = static_cast<QQuickMaterialRippleWaveNode *>(enterNode->nextSibling());
350 }
351
352 // exit old waves
353 int j = container->childCount() - 1 - m_waves;
354 while (j-- > 0) {
355 QQuickMaterialRippleWaveNode *exitNode = static_cast<QQuickMaterialRippleWaveNode *>(backgroundNode->nextSibling());
356 if (exitNode) {
357 exitNode->exit();
358 exitNode->sync(item: this);
359 }
360 }
361
362 return container;
363}
364
365void QQuickMaterialRipple::timerEvent(QTimerEvent *event)
366{
367 QQuickItem::timerEvent(event);
368
369 if (event->timerId() == m_enterDelay)
370 enterWave();
371}
372
373void QQuickMaterialRipple::prepareWave()
374{
375 if (m_enterDelay <= 0)
376 m_enterDelay = startTimer(interval: RIPPLE_ENTER_DELAY);
377}
378
379void QQuickMaterialRipple::enterWave()
380{
381 if (m_enterDelay > 0) {
382 killTimer(id: m_enterDelay);
383 m_enterDelay = 0;
384 }
385
386 ++m_waves;
387 update();
388}
389
390void QQuickMaterialRipple::exitWave()
391{
392 if (m_enterDelay > 0) {
393 killTimer(id: m_enterDelay);
394 m_enterDelay = 0;
395 }
396
397 if (m_waves > 0) {
398 --m_waves;
399 update();
400 }
401}
402
403QT_END_NAMESPACE
404
405#include "moc_qquickmaterialripple_p.cpp"
406
407#include "qquickmaterialripple.moc"
408

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