跳转至

example/gui/theme/MotionSpecPage.cpp

Motion Spec page - Implementation. More...

Namespaces

Name
cf
cf::ui
cf::ui::gallery

Detailed Description

Motion Spec page - Implementation.

Author: CFDesktop Team

Version: 0.1

Date: 2026-02-28

Source code

#include "MotionSpecPage.h"
#include "ui/core/material/cfmaterial_motion.h"
#include "ui/core/material/cfmaterial_scheme.h"
#include "ui/core/theme.h"

#include <QApplication>
#include <QComboBox>
#include <QDebug>
#include <QFont>
#include <QFontMetrics>
#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPropertyAnimation>
#include <QTimer>

namespace {

QString easingTypeToString(cf::ui::base::Easing::Type type) {
    switch (type) {
        case cf::ui::base::Easing::Type::Emphasized:
            return "Emphasized";
        case cf::ui::base::Easing::Type::EmphasizedDecelerate:
            return "EmphasizedDecelerate";
        case cf::ui::base::Easing::Type::EmphasizedAccelerate:
            return "EmphasizedAccelerate";
        case cf::ui::base::Easing::Type::Standard:
            return "Standard";
        case cf::ui::base::Easing::Type::StandardDecelerate:
            return "StandardDecelerate";
        case cf::ui::base::Easing::Type::StandardAccelerate:
            return "StandardAccelerate";
        case cf::ui::base::Easing::Type::Linear:
            return "Linear";
        default:
            return "Unknown";
    }
}

} // namespace

namespace cf::ui::gallery {

// =============================================================================
// MotionPreviewWidget Implementation
// =============================================================================

MotionPreviewWidget::MotionPreviewWidget(const core::MotionSpec& spec, const QString& name,
                                         QWidget* parent)
    : QWidget(parent), spec_(spec), name_(name) {
    setMinimumHeight(100);
    setMaximumHeight(160);

    timer_ = new QTimer(this);
    connect(timer_, &QTimer::timeout, this, &MotionPreviewWidget::updateAnimation);

    resetAnimation();
}

void MotionPreviewWidget::setProgress(float progress) {
    progress_ = qBound(0.0f, progress, 1.0f);

    float eased = calculateEasedProgress(progress_);
    QRectF r = rect().adjusted(40, 30, -40, -30);
    ballPosition_ = QPointF(r.left() + eased * r.width(), r.center().y());

    update();
    emit progressChanged();
}

float MotionPreviewWidget::calculateEasedProgress(float linearProgress) const {
    QEasingCurve curve = spec_.toEasingCurve();
    return curve.valueForProgress(linearProgress);
}

void MotionPreviewWidget::startAnimation() {
    isAnimating_ = true;
    elapsed_ = 0.0f;
    progress_ = 0.0f;
    timer_->start(16);
}

void MotionPreviewWidget::resetAnimation() {
    isAnimating_ = false;
    timer_->stop();
    progress_ = 0.0f;
    elapsed_ = 0.0f;
    ballPosition_ = QPointF(rect().left() + 40, rect().center().y());
    update();
}

void MotionPreviewWidget::updateAnimation() {
    if (!isAnimating_)
        return;

    elapsed_ += 16.0f;

    if (elapsed_ >= spec_.durationMs) {
        elapsed_ = spec_.durationMs;
        setProgress(1.0f);
        isAnimating_ = false;
        timer_->stop();
        emit animationFinished();
    } else {
        float linearProgress = elapsed_ / spec_.durationMs;
        setProgress(linearProgress);
    }
}

void MotionPreviewWidget::paintEvent(QPaintEvent*) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    QRectF r = rect().adjusted(20, 20, -20, -20);

    // Background track
    QColor trackColor = isDarkTheme_ ? QColor(60, 60, 60) : QColor(220, 220, 220);
    QPainterPath trackPath;
    trackPath.addRoundedRect(r, 8, 8);
    painter.setPen(Qt::NoPen);
    painter.setBrush(trackColor);
    painter.drawPath(trackPath);

    // Progress fill
    QEasingCurve curve = spec_.toEasingCurve();
    QColor progressColor;
    switch (spec_.easing) {
        case base::Easing::Type::Emphasized:
        case base::Easing::Type::EmphasizedDecelerate:
            progressColor = QColor(103, 80, 164);
            break;
        case base::Easing::Type::EmphasizedAccelerate:
            progressColor = QColor(155, 93, 175);
            break;
        case base::Easing::Type::Standard:
        case base::Easing::Type::StandardDecelerate:
        case base::Easing::Type::StandardAccelerate:
            progressColor = QColor(98, 91, 113);
            break;
        case base::Easing::Type::Linear:
            progressColor = QColor(98, 91, 113);
            break;
        default:
            progressColor = QColor(103, 80, 164);
            break;
    }

    float eased = calculateEasedProgress(progress_);
    float fillWidth = r.width() * eased;

    if (fillWidth > 0) {
        QRectF fillRect(r.left(), r.top(), fillWidth, r.height());
        QPainterPath fillPath;
        fillPath.addRoundedRect(fillRect, 8, 8);

        QLinearGradient gradient(r.left(), 0, r.left() + fillWidth, 0);
        gradient.setColorAt(0, progressColor);
        gradient.setColorAt(1, progressColor.lighter(120));
        painter.setBrush(gradient);
        painter.drawPath(fillPath);
    }

    // Animated ball
    float ballRadius = 10;
    QPainterPath ballPath;
    ballPath.addEllipse(ballPosition_, ballRadius, ballRadius);

    painter.setPen(Qt::NoPen);
    painter.setBrush(QColor(0, 0, 0, 50));
    painter.drawEllipse(ballPosition_ + QPointF(0, 2), ballRadius, ballRadius);

    QRadialGradient ballGradient(ballPosition_, ballRadius);
    ballGradient.setColorAt(0, QColor(255, 255, 255));
    ballGradient.setColorAt(1, QColor(220, 220, 220));
    painter.setBrush(ballGradient);
    painter.drawPath(ballPath);

    // Time markers
    QFont markerFont("Segoe UI", 8);
    painter.setFont(markerFont);
    QColor markerColor = isDarkTheme_ ? QColor(180, 180, 180) : QColor(100, 100, 100);
    painter.setPen(markerColor);

    painter.drawText(QRectF(r.left(), r.bottom() + 5, 40, 20), Qt::AlignCenter, "0ms");
    painter.drawText(QRectF(r.right() - 40, r.bottom() + 5, 40, 20), Qt::AlignCenter,
                     QString("%1ms").arg(spec_.durationMs));

    // Current time label
    if (isAnimating_ || progress_ > 0) {
        int currentTime = static_cast<int>(elapsed_);
        painter.drawText(QRectF(r.center().x() - 30, r.top() - 18, 60, 20), Qt::AlignCenter,
                         QString("%1ms").arg(currentTime));
    }
}

void MotionPreviewWidget::resizeEvent(QResizeEvent* event) {
    QWidget::resizeEvent(event);
    resetAnimation();
}

void MotionPreviewWidget::updateSpec(const core::MotionSpec& spec) {
    spec_ = spec;
    resetAnimation();
}

// =============================================================================
// MotionCardWidget Implementation
// =============================================================================

MotionCardWidget::MotionCardWidget(const core::MotionSpec& spec, const QString& name,
                                   const QString& description, QWidget* parent)
    : QWidget(parent), spec_(spec), name_(name), description_(description) {
    setMinimumSize(240, 180);
    setMaximumSize(280, 220);
    setCursor(Qt::PointingHandCursor);

    backgroundColor_ = QColor(250, 250, 250);
    surfaceColor_ = QColor(245, 245, 245);
    onSurfaceColor_ = QColor(60, 60, 60);

    updateCurvePath();
}

void MotionCardWidget::updateCurvePath() {
    curvePath_ = QPainterPath();
    QRectF curveRect(10, 10, 80, 40);
    curvePath_.moveTo(curveRect.bottomLeft());

    QEasingCurve curve = spec_.toEasingCurve();
    for (int i = 0; i <= 20; i++) {
        float t = i / 20.0f;
        float value = curve.valueForProgress(t);
        QPointF point(curveRect.left() + t * curveRect.width(),
                      curveRect.bottom() - value * curveRect.height());
        curvePath_.lineTo(point);
    }
}

void MotionCardWidget::setThemeColors(const QColor& background, const QColor& surface,
                                      const QColor& onSurface) {
    backgroundColor_ = background;
    surfaceColor_ = surface;
    onSurfaceColor_ = onSurface;
    update();
}

void MotionCardWidget::paintEvent(QPaintEvent*) {
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    QRectF r = rect().adjusted(4, 4, -4, -4);
    float radius = 16;

    // Card background with elevation
    QPainterPath path;
    path.addRoundedRect(r, radius, radius);

    if (isHovered_) {
        painter.setPen(Qt::NoPen);
        painter.setBrush(QColor(0, 0, 0, 15));
        painter.drawPath(path.translated(0, 3));
    }

    QColor bgColor = isHovered_ ? surfaceColor_.lighter(105) : surfaceColor_;
    painter.setPen(QPen(QColor(200, 200, 200), 1));
    painter.setBrush(bgColor);
    painter.drawPath(path);

    // Title
    QFont titleFont("Segoe UI", 11, QFont::Bold);
    painter.setFont(titleFont);
    painter.setPen(onSurfaceColor_);
    QRectF titleRect = r.adjusted(14, 14, -14, 0);
    painter.drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, name_);

    // Description
    QFont descFont("Segoe UI", 8);
    painter.setFont(descFont);
    QColor descColor = onSurfaceColor_;
    descColor.setAlpha(180);
    painter.setPen(descColor);
    QRectF descRect = titleRect.adjusted(0, QFontMetrics(titleFont).height() + 4, 0, 0);
    painter.drawText(descRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, description_);

    // Spec info
    QFont infoFont("Consolas", 9);
    painter.setFont(infoFont);
    QColor infoColor = QColor(103, 80, 164);
    painter.setPen(infoColor);

    QString specText =
        QString("⏱ %1ms • %2").arg(spec_.durationMs).arg(easingTypeToString(spec_.easing));

    QRectF specRect = r.adjusted(14, 0, -14, -45);
    painter.drawText(specRect, Qt::AlignLeft | Qt::AlignBottom, specText);

    // Easing curve preview
    QRectF curveBox(r.right() - 90, r.top() + 14, 76, 44);
    QPainterPath curveBoxPath;
    curveBoxPath.addRoundedRect(curveBox, 8, 8);

    painter.setPen(Qt::NoPen);
    painter.setBrush(isDarkTheme_ ? QColor(40, 40, 40) : QColor(235, 235, 235));
    painter.drawPath(curveBoxPath);

    painter.setPen(QPen(QColor(103, 80, 164), 2));
    painter.setBrush(Qt::NoBrush);
    painter.drawPath(curvePath_);

    // Play button hint
    if (isHovered_) {
        QFont hintFont("Segoe UI", 8);
        painter.setFont(hintFont);
        painter.setPen(onSurfaceColor_);
        painter.drawText(r.adjusted(14, 0, -14, -14), Qt::AlignBottom | Qt::AlignHCenter,
                         "▶ 点击预览动画");
    }
}

void MotionCardWidget::enterEvent(QEnterEvent*) {
    isHovered_ = true;
    update();
}

void MotionCardWidget::leaveEvent(QEvent*) {
    isHovered_ = false;
    update();
}

void MotionCardWidget::mousePressEvent(QMouseEvent*) {
    emit playRequested(spec_);
}

// =============================================================================
// MotionSpecPage Implementation
// =============================================================================

MotionSpecPage::MotionSpecPage(QWidget* parent) : ThemePageWidget(parent) {
    // Initialize motion presets
    motionPresets_ = {
        {"Short Enter", "小元素入场 (按钮、图标)", core::MotionPresets::shortEnter()},
        {"Short Exit", "小元素离场", core::MotionPresets::shortExit()},
        {"Medium Enter", "中等元素入场 (卡片、列表)", core::MotionPresets::mediumEnter()},
        {"Medium Exit", "中等元素离场", core::MotionPresets::mediumExit()},
        {"Long Enter", "大元素入场 (对话框、页面)", core::MotionPresets::longEnter()},
        {"Long Exit", "大元素离场", core::MotionPresets::longExit()},
        {"State Change", "状态层动画 (hover、focus)", core::MotionPresets::stateChange()},
        {"Ripple Expand", "涟漪扩散动画", core::MotionPresets::rippleExpand()},
        {"Ripple Fade", "涟漪淡出动画", core::MotionPresets::rippleFade()}};

    setupUI();
    createPreviewSection();
    createMotionCards();
}

void MotionSpecPage::setupUI() {
    auto* mainLayout = new QVBoxLayout(this);
    mainLayout->setContentsMargins(0, 0, 0, 0);
    mainLayout->setSpacing(16);

    // Preview frame
    previewFrame_ = new QFrame(this);
    previewFrame_->setFrameStyle(QFrame::NoFrame);
    previewLayout_ = new QVBoxLayout(previewFrame_);
    previewLayout_->setContentsMargins(20, 16, 20, 16);
    mainLayout->addWidget(previewFrame_);

    // Scroll area for motion cards
    scrollArea_ = new QScrollArea(this);
    scrollArea_->setWidgetResizable(true);
    scrollArea_->setFrameShape(QFrame::NoFrame);
    scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    scrollContent_ = new QWidget();
    scrollLayout_ = new QVBoxLayout(scrollContent_);
    scrollLayout_->setSpacing(16);
    scrollLayout_->setContentsMargins(20, 0, 20, 20);

    cardsLayout_ = new QGridLayout();
    cardsLayout_->setSpacing(12);
    cardsLayout_->setContentsMargins(0, 0, 0, 0);
    scrollLayout_->addLayout(cardsLayout_);
    scrollLayout_->addStretch();

    scrollArea_->setWidget(scrollContent_);
    mainLayout->addWidget(scrollArea_);
}

void MotionSpecPage::createPreviewSection() {
    // Preview title
    previewLabel_ = new QLabel("动画预览区 - 点击下方卡片播放", this);
    QFont previewTitleFont("Segoe UI", 13, QFont::Medium);
    previewLabel_->setFont(previewTitleFont);
    previewLabel_->setStyleSheet("QLabel { color: #49454F; }");
    previewLayout_->addWidget(previewLabel_);

    // Preview widget container
    previewContainer_ = new QHBoxLayout();

    // Preview widget
    previewWidget_ = new MotionPreviewWidget(core::MotionPresets::shortEnter(), "shortEnter", this);
    previewContainer_->addWidget(previewWidget_, 1);

    // Info panel
    QVBoxLayout* infoPanel = new QVBoxLayout();

    // Speed control
    QLabel* speedLabel = new QLabel("播放速度:", this);
    speedLabel->setStyleSheet("QLabel { color: #49454F; }");
    infoPanel->addWidget(speedLabel);

    speedCombo_ = new QComboBox(this);
    speedCombo_->addItem("1x (正常)", 1);
    speedCombo_->addItem("0.5x (慢放)", 2);
    speedCombo_->addItem("0.25x (极慢)", 4);
    speedCombo_->setCurrentIndex(0);
    connect(speedCombo_, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
            &MotionSpecPage::onSpeedChanged);
    infoPanel->addWidget(speedCombo_);

    infoPanel->addStretch();

    // Spec info
    previewInfoLabel_ = new QLabel(this);
    previewInfoLabel_->setWordWrap(true);
    QFont infoFont("Consolas", 9);
    previewInfoLabel_->setFont(infoFont);
    previewInfoLabel_->setStyleSheet("QLabel { color: #49454F; }");
    infoPanel->addWidget(previewInfoLabel_);

    previewContainer_->addLayout(infoPanel, 1);
    previewLayout_->addLayout(previewContainer_);

    // Update initial info
    onPlayRequested(core::MotionPresets::shortEnter());
}

void MotionSpecPage::createMotionCards() {
    int row = 0, col = 0;
    int maxCols = 3;

    for (const auto& preset : motionPresets_) {
        auto* card =
            new MotionCardWidget(preset.spec, preset.name, preset.description, scrollContent_);

        cardsLayout_->addWidget(card, row, col);
        motionCards_.append(card);

        connect(card, &MotionCardWidget::playRequested, this, &MotionSpecPage::onPlayRequested);

        col++;
        if (col >= maxCols) {
            col = 0;
            row++;
        }
    }
}

void MotionSpecPage::updateWindowTheme() {
    // Background update is handled by applyTheme
}

QString MotionSpecPage::easingTypeToString(base::Easing::Type type) const {
    return ::easingTypeToString(type);
}

void MotionSpecPage::onPlayRequested(const core::MotionSpec& spec) {
    // Find preset name
    QString presetName;
    for (const auto& preset : motionPresets_) {
        if (preset.spec.durationMs == spec.durationMs && preset.spec.easing == spec.easing) {
            presetName = preset.name;
            break;
        }
    }

    // Create a copy with adjusted duration for animation speed
    int speedMultiplier = speedCombo_->currentData().toInt();
    core::MotionSpec adjustedSpec = spec;
    adjustedSpec.durationMs = spec.durationMs * speedMultiplier;

    // Update the existing preview widget with new spec
    if (previewWidget_) {
        previewWidget_->setDarkTheme(isDarkTheme_);
        previewWidget_->updateSpec(adjustedSpec);
    }

    // Update info label
    QString infoText = QString("动画名称: %1\n"
                               "持续时间: %2ms\n"
                               "缓动类型: %3\n"
                               "延迟: %4ms")
                           .arg(presetName)
                           .arg(adjustedSpec.durationMs)
                           .arg(easingTypeToString(spec.easing))
                           .arg(spec.delayMs);

    previewInfoLabel_->setText(infoText);

    // Start animation
    previewWidget_->startAnimation();
}

void MotionSpecPage::onAnimationFinished() {
    // Could add replay functionality here
}

void MotionSpecPage::onSpeedChanged(int index) {
    Q_UNUSED(index);
    // Speed will be applied on next play
}

void MotionSpecPage::applyTheme(const core::ICFTheme& theme) {
    auto& colorScheme = static_cast<const core::MaterialColorScheme&>(theme.color_scheme());

    QColor bg = colorScheme.queryColor("md.background");
    QColor surface = colorScheme.queryColor("md.surface");
    QColor onSurface = colorScheme.queryColor("md.onSurface");

    // Update background
    scrollContent_->setAutoFillBackground(true);
    QPalette pal = scrollContent_->palette();
    pal.setColor(QPalette::Window, bg);
    scrollContent_->setPalette(pal);

    previewLabel_->setStyleSheet(QString("QLabel { color: %1; }").arg(onSurface.name()));
    previewInfoLabel_->setStyleSheet(QString("QLabel { color: %1; }").arg(onSurface.name()));

    // Update preview frame
    previewFrame_->setStyleSheet(
        QString("QFrame { background-color: %1; border-radius: 12px; border: 1px solid %2; }")
            .arg(surface.name())
            .arg(colorScheme.queryColor("md.outlineVariant").name()));

    // Update all cards
    for (auto* card : motionCards_) {
        card->setThemeColors(bg, surface, onSurface);
    }

    isDarkTheme_ = (bg.value() < 128);
    previewWidget_->setDarkTheme(isDarkTheme_);

    for (auto* card : motionCards_) {
        card->setDarkTheme(isDarkTheme_);
    }
}

} // namespace cf::ui::gallery

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