ui/widget/material/base/ripple_helper.cpp¶
Material Design Ripple Helper Implementation. More...
Namespaces¶
| Name |
|---|
| cf |
| cf::ui |
| cf::ui::widget |
| cf::ui::widget::material |
| cf::ui::widget::material::base |
Detailed Description¶
Material Design Ripple Helper Implementation.
Author: Material Design Framework Team
Version: 0.1
Since: 0.1
Date: 2026-02-28
Manages ripple effect lifecycle and rendering following Material Design 3. Ripples expand from touch point with fade-out on release.
Source code¶
#include "ripple_helper.h"
#include "components/material/cfmaterial_animation_factory.h"
#include "components/timing_animation.h"
#include <QPainter>
#include <QPainterPath>
#include <QRadialGradient>
namespace cf::ui::widget::material::base {
using namespace cf::ui::components::material;
using namespace cf::ui::components;
RippleHelper::RippleHelper(cf::WeakPtr<components::material::CFMaterialAnimationFactory> factory,
QObject* parent)
: QObject(parent), m_mode(Mode::Bounded), m_color(Qt::black), m_animator(factory) {}
// ============================================================================
// Configuration
// ============================================================================
void RippleHelper::setMode(Mode mode) {
m_mode = mode;
}
void RippleHelper::setColor(const cf::ui::base::CFColor& color) {
m_color = color;
}
// ============================================================================
// Event Handlers
// ============================================================================
float RippleHelper::maxRadius(const QRectF& rect, const QPointF& center) const {
// Calculate distance to each corner
QPointF topLeft = rect.topLeft();
QPointF topRight = rect.topRight();
QPointF bottomLeft = rect.bottomLeft();
QPointF bottomRight = rect.bottomRight();
float d1 = std::hypot(center.x() - topLeft.x(), center.y() - topLeft.y());
float d2 = std::hypot(center.x() - topRight.x(), center.y() - topRight.y());
float d3 = std::hypot(center.x() - bottomLeft.x(), center.y() - bottomLeft.y());
float d4 = std::hypot(center.x() - bottomRight.x(), center.y() - bottomRight.y());
return std::max({d1, d2, d3, d4});
}
void RippleHelper::onPress(const QPoint& pos, const QRectF& widgetRect) {
// Performance mode check
auto* factory = m_animator.Get();
if (!factory || !factory->isAllEnabled()) {
return; // Skip ripples if animations disabled
}
// Cancel any existing ripples before starting a new one
// This prevents visual artifacts from rapid clicks
onCancel();
// Create new ripple
MdRipple ripple;
ripple.center = QPointF(pos);
ripple.radius = 0.0f;
ripple.opacity = 1.0f;
ripple.releasing = false;
// Calculate final radius
ripple.maxRadius = maxRadius(widgetRect, ripple.center);
// Store ripple
m_ripples.append(ripple);
// Start expand animation
auto anim = factory->getAnimation("md.animation.rippleExpand");
if (anim) {
// Use index to track which ripple this animation controls
int index = m_ripples.size() - 1;
// Get raw pointer and set range if it's a timing animation
auto* rawAnim = anim.Get();
auto* timingAnim = static_cast<components::ICFTimingAnimation*>(rawAnim);
if (timingAnim) {
timingAnim->setRange(0.0f, 1.0f);
}
connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this,
[this, index](float progress) {
if (index >= 0 && index < m_ripples.size()) {
m_ripples[index].radius = m_ripples[index].maxRadius * progress;
emit repaintNeeded();
}
});
connect(rawAnim, &components::ICFAbstractAnimation::finished, this, [this, index]() {
// Animation done but ripple may still be in releasing state
emit repaintNeeded();
});
rawAnim->start(components::ICFAbstractAnimation::Direction::Forward);
}
emit repaintNeeded();
}
void RippleHelper::onRelease() {
// Trigger fade-out for all non-releasing ripples
auto* factory = m_animator.Get();
if (!factory)
return;
for (int i = 0; i < m_ripples.size(); ++i) {
if (!m_ripples[i].releasing) {
m_ripples[i].releasing = true;
// Start fade animation
auto anim = factory->getAnimation("md.animation.rippleFade");
if (anim) {
int index = i; // Capture for lambda
// Get raw pointer and set range if it's a timing animation
auto* rawAnim = anim.Get();
auto* timingAnim = static_cast<components::ICFTimingAnimation*>(rawAnim);
if (timingAnim) {
timingAnim->setRange(1.0f, 0.0f);
}
connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this,
[this, index](float progress) {
if (index >= 0 && index < m_ripples.size()) {
m_ripples[index].opacity = progress;
emit repaintNeeded();
}
});
connect(rawAnim, &components::ICFAbstractAnimation::finished, this,
[this, index]() {
// Remove finished ripple
if (index >= 0 && index < m_ripples.size()) {
m_ripples.removeAt(index);
emit repaintNeeded();
}
});
rawAnim->start(components::ICFAbstractAnimation::Direction::Forward);
}
}
}
emit repaintNeeded();
}
void RippleHelper::onCancel() {
// Remove all active ripples immediately
if (!m_ripples.isEmpty()) {
m_ripples.clear();
emit repaintNeeded();
}
}
// ============================================================================
// Painting
// ============================================================================
namespace {
// Material Design 3 ripple fixed opacity (12% as per MD3 specs)
constexpr float RIPPLE_FIXED_OPACITY = 0.12f;
}
void RippleHelper::paint(QPainter* painter, const QPainterPath& clipPath) {
if (m_ripples.isEmpty() || !painter) {
return;
}
painter->save();
// Apply clipping for bounded mode
if (m_mode == Mode::Bounded) {
painter->setClipPath(clipPath);
}
// Draw each ripple
for (const auto& ripple : m_ripples) {
if (ripple.radius <= 0.0f || ripple.opacity <= 0.0f) {
continue;
}
// Create radial gradient for smooth ripple edge
QRadialGradient gradient(ripple.center, ripple.radius);
QColor color = m_color.native_color();
// Apply fixed ripple opacity (Material Design 3 spec)
// The ripple.opacity controls the fade-out animation, while RIPPLE_FIXED_OPACITY
// ensures the ripple always has the correct visual strength
color.setAlphaF(RIPPLE_FIXED_OPACITY * ripple.opacity);
// Center is solid, edge fades slightly
gradient.setColorAt(0.0f, color);
gradient.setColorAt(0.7f, color);
gradient.setColorAt(1.0f, QColor(color.red(), color.green(), color.blue(), 0));
painter->setBrush(QBrush(gradient));
painter->setPen(Qt::NoPen);
painter->drawEllipse(ripple.center, ripple.radius, ripple.radius);
}
painter->restore();
}
// ============================================================================
// State Query
// ============================================================================
bool RippleHelper::hasActiveRipple() const {
return !m_ripples.isEmpty();
}
} // namespace cf::ui::widget::material::base
Updated on 2026-03-09 at 10:14:01 +0000