跳转至

ui/widget/material/base/elevation_controller.cpp

Material Design Elevation Controller Implementation. More...

Namespaces

Name
cf
cf::ui
cf::ui::widget
cf::ui::widget::material
cf::ui::widget::material::base

Detailed Description

Material Design Elevation Controller Implementation.

Author: Material Design Framework Team

Version: 0.1

Since: 0.1

Date: 2026-02-28

Manages Material elevation levels and renders corresponding shadows. Supports animated transitions between elevation levels.

Material Elevation Levels (dp → shadow params): Level 0: 0dp - No shadow Level 1: 1dp - blur=2px, offset=1px, opacity=0.15 Level 2: 3dp - blur=4px, offset=2px, opacity=0.20 Level 3: 6dp - blur=8px, offset=4px, opacity=0.25 Level 4: 8dp - blur=12px, offset=6px, opacity=0.30 Level 5: 12dp - blur=16px, offset=8px, opacity=0.35

Source code

#include "elevation_controller.h"
#include "base/color_helper.h"
#include "base/device_pixel.h"
#include "base/math_helper.h"
#include "components/animation.h"
#include "components/material/cfmaterial_animation_factory.h"
#include "components/material/cfmaterial_animation_strategy.h"
#include "components/timing_animation.h"

#include <QApplication>
#include <QPainter>
#include <QPainterPath>
#include <QWidget>
#include <cmath> // For std::tan, std::floor, std::ceil

namespace cf::ui::widget::material::base {

using namespace cf::ui::components::material;
using namespace cf::ui::base;
using namespace cf::ui::math;

MdElevationController::MdElevationController(
    cf::WeakPtr<components::material::CFMaterialAnimationFactory> factory, QObject* parent)
    : QObject(parent), m_currentLevel(0.0f), m_targetLevel(0), m_animator(factory) {}

MdElevationController::~MdElevationController() {
    cancelCurrentAnimation();
}

// ============================================================================
// Configuration
// ============================================================================

void MdElevationController::setElevation(int level) {
    level = clamp(level, 0, 5);
    m_targetLevel = level;
    m_currentLevel = static_cast<float>(level);
}

void MdElevationController::setLightSourceAngle(float degrees) {
    m_lightSourceAngle = degrees;
}

int MdElevationController::elevation() const {
    return m_targetLevel;
}

void MdElevationController::setPressed(bool pressed) {
    if (m_isPressed == pressed) {
        return;
    }

    m_isPressed = pressed;

    // Calculate target press offset
    device::CanvasUnitHelper helper(qApp ? qApp->devicePixelRatio() : 1.0);
    float targetOffset = 0.0f;
    if (pressed) {
        targetOffset = helper.dpToPx(static_cast<float>(m_targetLevel) * 2.0f);
    }

    // Animate to the new offset
    animatePressOffsetTo(targetOffset);
}

float MdElevationController::pressOffset() const {
    // Return the animated press offset value
    return m_currentPressOffset;
}

void MdElevationController::cancelCurrentAnimation() {
    if (m_pressOffsetAnimation) {
        auto* anim = m_pressOffsetAnimation.Get();
        if (anim) {
            // Disconnect all signals from this animation to this object
            disconnect(anim, &components::ICFAbstractAnimation::progressChanged, this, nullptr);
            disconnect(anim, &components::ICFAbstractAnimation::finished, this, nullptr);
            // Stop the animation
            anim->stop();
        }
        m_pressOffsetAnimation = nullptr;
    }
}

void MdElevationController::onAnimationFinished() {
    m_pressOffsetAnimation = nullptr;
}

void MdElevationController::animatePressOffsetTo(float to) {
    auto* factory = m_animator.Get();
    if (!factory || !factory->isAllEnabled()) {
        // Direct set if animations disabled
        m_currentPressOffset = to;
        emit pressOffsetChanged();
        return;
    }

    // CRITICAL FIX: Cancel any currently running animation before starting a new one.
    // This prevents multiple animations from competing to update m_currentPressOffset,
    // which causes visual glitches during rapid press/release events.
    cancelCurrentAnimation();

    // Start animation from current press offset value
    float from = m_currentPressOffset;

    // Create custom animation descriptor with longer duration for smoother press effect
    // Using "md.motion.longEnter" for slower, more noticeable animation
    AnimationDescriptor desc(
        "fade",                // Animation type
        "md.motion.longEnter", // Motion spec (longer duration for smooth press)
        "opacity",             // Property (we'll override with setRange)
        from,                  // Start value
        to                     // End value
    );

    // Create animation from descriptor
    auto anim = factory->createAnimation(desc, nullptr, this);
    if (!anim) {
        // Fallback: direct set if animation creation fails
        m_currentPressOffset = to;
        emit pressOffsetChanged();
        return;
    }

    // Save animation reference for cancellation
    m_pressOffsetAnimation = anim;

    // 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(from, to);
    }

    // Connect progress signal
    // Note: Qt::UniqueConnection cannot be used with lambdas, but cancelCurrentAnimation()
    // disconnects all signals before starting a new animation, preventing duplicates
    //
    // CRITICAL: progressChanged emits 0-1 normalized progress, NOT actual offset values.
    // We must interpolate between from and to to get the actual offset.
    connect(rawAnim, &components::ICFAbstractAnimation::progressChanged, this,
            [this, from, to](float progress) {
                // progress is 0-1, interpolate to get actual offset value
                m_currentPressOffset = from + (to - from) * progress;
                emit pressOffsetChanged();
            });

    // Connect finished signal to clear the animation reference
    connect(rawAnim, &components::ICFAbstractAnimation::finished, this,
            &MdElevationController::onAnimationFinished, Qt::UniqueConnection);

    // Start animation
    rawAnim->start(components::ICFAbstractAnimation::Direction::Forward);
}

void MdElevationController::animateTo(int level, const core::MotionSpec& spec) {
    level = clamp(level, 0, 5);

    auto* factory = m_animator.Get();
    if (!factory || !factory->isAllEnabled()) {
        // Direct set if animations disabled
        m_targetLevel = level;
        m_currentLevel = static_cast<float>(level);
        return;
    }

    // Create custom timing animation for elevation transition
    // Note: This requires the factory to support custom animation creation
    // For now, use direct set with smooth transition
    int oldLevel = m_targetLevel;
    m_targetLevel = level;

    // Simple interpolation - in production, use proper animation
    // The actual animation would be handled by the widget's paint loop
    // reading m_currentLevel
    Q_UNUSED(spec);
    Q_UNUSED(oldLevel);
}

// ============================================================================
// Shadow Calculation
// ============================================================================

MdElevationController::ShadowParams MdElevationController::paramsForLevel(float level) const {
    // Predefined parameters for integer levels
    struct LevelParams {
        float blurRadius;
        float offsetY; // Vertical offset (positive =向下)
        float opacity;
    };

    static constexpr LevelParams levelParams[] = {
        {0.0f, 0.0f, 0.00f},  // Level 0
        {2.0f, 1.0f, 0.15f},  // Level 1
        {4.0f, 2.0f, 0.20f},  // Level 2
        {8.0f, 4.0f, 0.25f},  // Level 3
        {12.0f, 6.0f, 0.30f}, // Level 4
        {16.0f, 8.0f, 0.35f}  // Level 5
    };

    // Clamp level to valid range
    level = clamp(level, 0.0f, 5.0f);

    // Interpolate between levels
    int lowerLevel = static_cast<int>(std::floor(level));
    int upperLevel = static_cast<int>(std::ceil(level));
    float t = level - lowerLevel;

    // Handle edge case where level is exactly at an integer
    if (upperLevel == lowerLevel) {
        upperLevel = std::min(5, lowerLevel + 1);
        t = 0.0f;
    }

    const LevelParams& lower = levelParams[lowerLevel];
    const LevelParams& upper = levelParams[upperLevel];

    ShadowParams result;
    result.blurRadius = lerp(lower.blurRadius, upper.blurRadius, t);
    result.offsetY = lerp(lower.offsetY, upper.offsetY, t);
    result.opacity = lerp(lower.opacity, upper.opacity, t);

    // Calculate horizontal offset based on light source angle
    // angle in degrees: 正值 = 光源从左侧, 负值 = 光源从右侧
    // Material Design 默认 15 度 = 光源从左上方照射
    float angleRad = m_lightSourceAngle * 3.14159265f / 180.0f;
    // offsetX与offsetY成比例,模拟光线方向
    // 光源从左上方来,阴影向右下方投射(offsetX 为正)
    result.offsetX = result.offsetY * std::tan(angleRad);

    // 按压时阴影缩小并靠近
    if (m_isPressed) {
        result.offsetX *= 0.5f;
        result.offsetY *= 0.5f;
        result.blurRadius *= 0.7f;
    }

    return result;
}

// ============================================================================
// Painting
// ============================================================================

void MdElevationController::paintShadow(QPainter* painter, const QPainterPath& shape) {
    if (!painter || m_currentLevel <= 0.0f) {
        return;
    }

    ShadowParams params = paramsForLevel(m_currentLevel);

    // Convert dp to pixels - use actual device pixel ratio from QApplication
    device::CanvasUnitHelper helper(qApp ? qApp->devicePixelRatio() : 1.0);
    float blurRadius = helper.dpToPx(params.blurRadius);
    float offsetX = helper.dpToPx(params.offsetX);
    float offsetY = helper.dpToPx(params.offsetY);

    painter->save();

    // Get shape bounding rect for shadow rendering
    QRectF bounds = shape.boundingRect();

    // Create shadow color (black with opacity)
    QColor shadowColor(0, 0, 0, static_cast<int>(params.opacity * 255));

    // Draw shadow layers with light source offset
    // Main shadow layer
    QPainterPath shadowPath = shape;
    shadowPath.translate(offsetX, offsetY);

    painter->setPen(Qt::NoPen);
    painter->setBrush(shadowColor);
    painter->drawPath(shadowPath);

    // Additional layers for blur approximation
    int layers = static_cast<int>(std::ceil(blurRadius / 2.0f));
    for (int i = 1; i <= layers && i <= 3; ++i) {
        float layerOpacity = params.opacity * (1.0f - static_cast<float>(i) / (layers + 1));
        float layerOffsetScale = 1.0f + (blurRadius * i / layers) * 0.3f / offsetY;
        float layerOffsetX = offsetX * layerOffsetScale;
        float layerOffsetY = offsetY * layerOffsetScale;

        QColor layerColor(0, 0, 0, static_cast<int>(layerOpacity * 255));
        QPainterPath layerPath = shape;
        layerPath.translate(layerOffsetX, layerOffsetY);

        painter->setBrush(layerColor);
        painter->drawPath(layerPath);
    }

    painter->restore();
}

CFColor MdElevationController::tonalOverlay(CFColor surface, CFColor primary) const {
    if (m_currentLevel <= 0.0f) {
        return surface;
    }

    // Calculate tonal amount based on elevation
    float tonalAmount = m_currentLevel / 10.0f; // 0.0 to 0.5

    // Blend surface with primary
    return blend(surface, primary, tonalAmount);
}

} // namespace cf::ui::widget::material::base

Updated on 2026-03-09 at 10:14:01 +0000