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