前言
Easing Curves Example 显示了如何使用缓和曲线来控制动画的速度。官方提供的动画案例,也就这个比较贴近于动画框架。其他的案例可能要放到其他框架中了,等梳理完其他部分的框架后,再来翻译或者注释代码。
这个案例涉及了一些Qt 方面的其他知识:属性系统、元对象系统、视图框架、Qt 2D绘图、QListWidget,但是重点是学习在动画中使用QEasingCurve以及自定义属性动画的方法。
官方使用的是界面文件,出于学习的目的以及发博客的便利,界面改用代码实现。
源代码资源详见Qt安装目录: ..\Qt5_15\Examples\Qt-5.15.0\widgets\animation\easing\
以下是效果图:
Animation.hpp
#include <QtWidgets>
class Animation : public QPropertyAnimation
{
public:
enum PathType{
LinearPath,
CirclePath,
NPathTypes
};
Animation(QObject *target,const QByteArray &prop)
:QPropertyAnimation(target,prop)
{
setPathType(LinearPath);
}
void setPathType(PathType pathType)
{
if(pathType>=NPathTypes)
qWarning("Unknown pathType %d",pathType);
m_pathType = pathType;
m_path = QPainterPath();
}
// 每次动画的currentTime更改时都会调用此函数。
void updateCurrentTime(int currentTime) override
{
// 如果是圆环路径
if(m_pathType == CirclePath ){
if( m_path.isEmpty() ) {
QPointF to = endValue().toPointF();
QPointF from = startValue().toPointF();
m_path.moveTo(from);
m_path.addEllipse(QRectF(from,to));
}
int dura = duration();
const qreal progress = ( (dura == 0) ? 1 : ( ( ( (currentTime-1) % dura ) + 1 ) / qreal(dura) ) );
qreal easedProgress = easingCurve().valueForProgress(progress);
if( easedProgress > 1.0 )
easedProgress -= 1.0;
else if( easedProgress < 0)
easedProgress += 1.0;
QPointF pt = m_path.pointAtPercent(easedProgress);
// 更新目标对象的属性的当前值。
updateCurrentValue(pt);
emit valueChanged(pt);
} else {
QPropertyAnimation::updateCurrentTime(currentTime);
}
}
QPainterPath m_path;
PathType m_pathType;
};
Window.h
包含一个QGraphicsPixmapItem的子类PixmapItem
#include <QtWidgets>
#include "Animation.hpp"
// QGraphicsPixmapItem是视图框架中的 Item
class PixmapItem : public QObject,public QGraphicsPixmapItem{
Q_OBJECT
// 参考 The Property System文档,pos本不是QGraphicsPixmapItem具有的属性
// 通过这种宏使pos变量称为了PixmapItem的属性,可用于元对象,此处用作属性动画
Q_PROPERTY(QPointF pos READ pos WRITE setPos)
public:
// 构造函数
PixmapItem(const QPixmap &pix): QGraphicsPixmapItem(pix){};
};
class Window : public QWidget
{
Q_OBJECT
public:
Window(QWidget *parent = nullptr);
private slots:
void curveChanged(int row);
void pathChanged(QAbstractButton *button);
void periodChanged(double);
void amplitudeChanged(double);
void overshootChanged(double);
private:
//为QListWidget创建Icon
void createCurveIcons();
// 启动动画
void startAnimation();
// QGraphicsScene是视图框架中的Item的容器
QGraphicsScene m_scene;
PixmapItem *m_item;
Animation *m_anim;
//QListWidget包含的Item的Icon的大小
QSize m_iconSize;
private:
// 列表控件
QListWidget *easingCurvePicker;
// 按钮组,用处使代码简单一些
QButtonGroup *buttonGroup;
// QEaseingCurve的几个常见属性的控件
QDoubleSpinBox *periodSpinBox; //周期
QDoubleSpinBox *amplitudeSpinBox; //幅值
QDoubleSpinBox *overshootSpinBox; //超调量
};
Window.cpp
#include "Window.h"
Window::Window(QWidget *parent)
: QWidget(parent),
m_iconSize(64,64)
{
// QListWidget ,是基于Item的控件部分,不是主要目的
// 但是关于QListWiget部分很好地展示了使用设定图标的用法
easingCurvePicker = new QListWidget;
easingCurvePicker->setIconSize(m_iconSize); //设置图标大小
easingCurvePicker->setMaximumSize(QSize(16777215, 120)); // 主要限制了最大高度120
easingCurvePicker->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); //垂直滚动条不用
easingCurvePicker->setFlow(QListView::LeftToRight); // 设置流向
// easingCurvePicker->setProperty("isWrapping", QVariant(false)); //将对象的 isWrapping 属性的值设置为 false。
easingCurvePicker->setWrapping(false); // 这行代码代替上面一行
//设置QListView的查看模式。
//设置查看模式将根据传入的参数移动启用或禁用拖放。 对于ListMode,默认移动为“Static”(禁用拖放); 对于IconMode,默认移动为Free(启用拖放)。
easingCurvePicker->setViewMode(QListView::IconMode);
easingCurvePicker->setMovement(QListView::Static); //设置移动“Static”(禁用拖放)
QRadioButton *lineRadio = new QRadioButton("lineRadio");
lineRadio->setChecked(true); //设置默认值
QRadioButton *circleRadio = new QRadioButton("circleRadio");
QGroupBox *group2 = new QGroupBox("Path Type");
QVBoxLayout *group2Layout = new QVBoxLayout(group2);
group2Layout->addWidget(lineRadio);
group2Layout->addWidget(circleRadio);
buttonGroup = new QButtonGroup(group2);
buttonGroup->addButton(lineRadio,0);
buttonGroup->addButton(circleRadio,1);
QGroupBox *group = new QGroupBox("Properties");
QGridLayout *groupLayout = new QGridLayout(group);
groupLayout->addWidget(new QLabel("Period"),0,0,1,1);
periodSpinBox = new QDoubleSpinBox;
groupLayout->addWidget(periodSpinBox,0,1,1,1);
groupLayout->addWidget(new QLabel("Amplitude"),1,0,1,1);
amplitudeSpinBox = new QDoubleSpinBox;
groupLayout->addWidget(amplitudeSpinBox,1,1,1,1);
groupLayout->addWidget(new QLabel("Overshoot"),2,0,1,1);
overshootSpinBox = new QDoubleSpinBox;
groupLayout->addWidget(overshootSpinBox,2,1,1,1);
QVBoxLayout *groups = new QVBoxLayout;
groups->addWidget(group2);
groups->addWidget(group);
groups->addStretch();
QGraphicsView *graphicsView = new QGraphicsView;
QGridLayout *mainLayout = new QGridLayout(this);
mainLayout->addWidget(easingCurvePicker,0,0,1,2);
mainLayout->addLayout(groups,1,0,1,1);
mainLayout->addWidget(graphicsView,1,1,2,1);
QEasingCurve dummy; //默认为线性缓动曲线
periodSpinBox->setValue(dummy.period());
amplitudeSpinBox->setValue(dummy.amplitude());
overshootSpinBox->setValue(dummy.overshoot());
connect(easingCurvePicker,&QListWidget::currentRowChanged,
this,&Window::curveChanged);
connect(buttonGroup,QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked),
this,&Window::pathChanged);
connect(periodSpinBox,QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this,&Window::periodChanged);
connect(amplitudeSpinBox,QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this,&Window::amplitudeChanged);
connect(overshootSpinBox,QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this,&Window::overshootChanged);
// 为QListWidget创建图标
createCurveIcons();
// 视图框架部分
QPixmap pix("images/qt-logo.png");
m_item = new PixmapItem(pix);
m_scene.addItem(m_item);
graphicsView->setScene(&m_scene);
// 重点就是这个 动画
m_anim = new Animation(m_item,"pos");
m_anim->setEasingCurve(QEasingCurve::OutBounce);
easingCurvePicker->setCurrentRow(int(QEasingCurve::OutBounce));
startAnimation();
}
QEasingCurve createEasingCurve(QEasingCurve::Type curveType)
{
QEasingCurve curve(curveType);
if (curveType == QEasingCurve::BezierSpline) {
curve.addCubicBezierSegment(QPointF(0.4, 0.1), QPointF(0.6, 0.9), QPointF(1.0, 1.0));
} else if (curveType == QEasingCurve::TCBSpline) {
curve.addTCBSegment(QPointF(0.0, 0.0), 0, 0, 0);
curve.addTCBSegment(QPointF(0.3, 0.4), 0.2, 1, -0.2);
curve.addTCBSegment(QPointF(0.7, 0.6), -0.2, 1, 0.2);
curve.addTCBSegment(QPointF(1.0, 1.0), 0, 0, 0);
}
return curve;
}
void Window::curveChanged(int row)
{
QEasingCurve::Type curveType = (QEasingCurve::Type)row;
m_anim->setEasingCurve(createEasingCurve(curveType));
m_anim->setCurrentTime(0);
bool isElastic = curveType >= QEasingCurve::InElastic && curveType <= QEasingCurve::OutInElastic;
bool isBounce = curveType >= QEasingCurve::InBounce && curveType <= QEasingCurve::OutInBounce;
periodSpinBox->setEnabled(isElastic);
amplitudeSpinBox->setEnabled(isElastic || isBounce);
overshootSpinBox->setEnabled(curveType >= QEasingCurve::InBack && curveType <= QEasingCurve::OutInBack);
}
void Window::pathChanged(QAbstractButton *button)
{
const int index = buttonGroup->id(button);
m_anim->setPathType(Animation::PathType(index));
}
void Window::periodChanged(double value)
{
QEasingCurve curve = m_anim->easingCurve();
curve.setPeriod(value);
m_anim->setEasingCurve(curve);
}
void Window::amplitudeChanged(double value)
{
QEasingCurve curve = m_anim->easingCurve();
curve.setAmplitude(value);
m_anim->setEasingCurve(curve);
}
void Window::overshootChanged(double value)
{
QEasingCurve curve = m_anim->easingCurve();
curve.setOvershoot(value);
m_anim->setEasingCurve(curve);
}
void Window::createCurveIcons()
{
// 设置背景颜色
QPixmap pix(m_iconSize);
QPainter painter(&pix);
QLinearGradient gradient(0,0, 0, m_iconSize.height());
gradient.setColorAt(0.0, QColor(240, 240, 240));
gradient.setColorAt(1.0, QColor(224, 224, 224));
QBrush brush(gradient);
// 元对象系统,相当于反射
const QMetaObject &mo = QEasingCurve::staticMetaObject;
// 返回具有给定索引的枚举数的元数据。
// int QMetaObject::indexOfEnumerator(const char *name) const
QMetaEnum metaEnum = mo.enumerator(mo.indexOfEnumerator("Type"));
// 跳过 QEasingCurve::Custom 这个类型的曲线
for (int i = 0; i < QEasingCurve::NCurveTypes - 1; ++i)
{
// 用渐变色填充背景
painter.fillRect(QRect(QPoint(0, 0), m_iconSize), brush);
// 用i做参数,迭代每种缓速曲线
QEasingCurve curve = createEasingCurve((QEasingCurve::Type) i);
painter.setPen(QColor(0, 0, 255, 64));
// 画x轴 和 y轴
qreal xAxis = m_iconSize.height()/1.5;
qreal yAxis = m_iconSize.width()/3;
painter.drawLine(0, xAxis, m_iconSize.width(), xAxis);
painter.drawLine(yAxis, 0, yAxis, m_iconSize.height());
// qreal curveScale = m_iconSize.height();
qreal curveScale = m_iconSize.height()/2;
painter.setPen(Qt::NoPen);
// 起点 红色
painter.setBrush(Qt::red);
QPoint start(yAxis, xAxis - curveScale * curve.valueForProgress(0));
painter.drawRect(start.x() - 1, start.y() - 1, 3, 3);
// 终点 蓝色
painter.setBrush(Qt::blue);
QPoint end(yAxis + curveScale, xAxis - curveScale * curve.valueForProgress(1));
painter.drawRect(end.x() - 1, end.y() - 1, 3, 3);
// 绘制路径,类似于各种图像的超类,Java中的Shape
QPainterPath curvePath;
curvePath.moveTo(start);
for (qreal t = 0; t <= 1.0; t+=1.0/curveScale) {
QPoint to;
to.setX(yAxis + curveScale * t);
to.setY(xAxis - curveScale * curve.valueForProgress(t));
curvePath.lineTo(to);
}
painter.setRenderHint(QPainter::Antialiasing, true);
painter.strokePath(curvePath, QColorConstants::Green);
painter.setRenderHint(QPainter::Antialiasing, false);
QListWidgetItem *item = new QListWidgetItem;
item->setIcon(QIcon(pix));
item->setText( QString("%1:").arg(i+1) + QString( metaEnum.key(i)));
// QListWidget添加项目
easingCurvePicker->addItem(item);
}
}
// 设置动画的起始和终点值、持续时间、循环次数
void Window::startAnimation()
{
m_anim->setStartValue(QPointF(0,0));
m_anim->setEndValue(QPointF(100,100));
m_anim->setDuration(2000); // 2 秒
m_anim->setLoopCount(-1); // 反复循环
m_anim->start();
}
mian.cpp
#include "Window.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Window w;
w.resize(500,400);
w.show();
return a.exec();
}