1.demo效果
2.demo项目构建
- 打开Qt Creator,按照图上操作
- 使用Minggw64构建运行
3.代码详细剖析
代码目录结构
├📁easing
| 📄animation.h
| 📦easing.pro
| 🎴easing.qrc
| 🎴form.ui
| 📄main.cpp
| 📄window.cpp
| 📄window.h
├📁images
| 🎴qt-logo.png
代码实现
- /easing/animation.h
#ifndef ANIMATION_H
#define ANIMATION_H
#include <QtWidgets>
#include <QtCore/qpropertyanimation.h>
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();
}
//每一毫秒都会调用的函数,实现这个虚函数
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;
};
#endif // ANIMATION_H
- /easing/easing.pro
QT += widgets
requires(qtConfig(listwidget))
HEADERS = window.h \
animation.h
SOURCES = main.cpp \
window.cpp
FORMS = form.ui
RESOURCES = easing.qrc
# install
target.path = $$[QT_INSTALL_EXAMPLES]/widgets/animation/easing
INSTALLS += target
- /easing/form.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>545</width>
<height>471</height>
</rect>
</property>
<property name="windowTitle">
<string>Easing curves</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2">
<widget class="QListWidget" name="easingCurvePicker">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>120</height>
</size>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="movement">
<enum>QListView::Static</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>false</bool>
</property>
<property name="viewMode">
<enum>QListView::IconMode</enum>
</property>
<property name="selectionRectVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="title">
<string>Path type</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QRadioButton" name="lineRadio">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>40</height>
</size>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Line</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string>buttonGroup</string>
</attribute>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="circleRadio">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>40</height>
</size>
</property>
<property name="text">
<string>Circle</string>
</property>
<attribute name="buttonGroup">
<string>buttonGroup</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Properties</string>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Period</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="periodSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="minimum">
<double>-1.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>-1.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="amplitudeSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="minimum">
<double>-1.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>-1.000000000000000</double>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Overshoot</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QDoubleSpinBox" name="overshootSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="minimum">
<double>-1.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
<property name="value">
<double>-1.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Amplitude</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QGraphicsView" name="graphicsView">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>
- /easing/main.cpp
#include <QtWidgets>
#include "window.h"
#include <QtDebug>
int main(int argc, char **argv)
{
Q_INIT_RESOURCE(easing);
QApplication app(argc, argv);
Window w;
//固定400*400大小
w.resize(400, 400);
w.show();
return app.exec();
}
- /easing/window.cpp
#include "window.h"
Window::Window(QWidget *parent)
: QWidget(parent),
m_iconSize(64, 64)
{
m_ui.setupUi(this);
//listview设置图标显示模式
m_ui.easingCurvePicker->setIconSize(m_iconSize);
//设置最小高度
m_ui.easingCurvePicker->setMinimumHeight(m_iconSize.height() + 50);
//直线或圆设置按钮组
m_ui.buttonGroup->setId(m_ui.lineRadio, 0);
m_ui.buttonGroup->setId(m_ui.circleRadio, 1);
//曲线对象
QEasingCurve dummy;
//周期
m_ui.periodSpinBox->setValue(dummy.period());
//振幅
m_ui.amplitudeSpinBox->setValue(dummy.amplitude());
//过冲
m_ui.overshootSpinBox->setValue(dummy.overshoot());
//list中被点击绑定
connect(m_ui.easingCurvePicker, &QListWidget::currentRowChanged,
this, &Window::curveChanged);
//按钮组被点击
connect(m_ui.buttonGroup, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked),
this, &Window::pathChanged);
connect(m_ui.periodSpinBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this, &Window::periodChanged);
connect(m_ui.amplitudeSpinBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this, &Window::amplitudeChanged);
connect(m_ui.overshootSpinBox, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this, &Window::overshootChanged);
//初始化List及图标
createCurveIcons();
//声明图片
QPixmap pix(QLatin1String(":/images/qt-logo.png"));
//item对象
m_item = new PixmapItem(pix);
//场景添加对象
m_scene.addItem(m_item);
m_ui.graphicsView->setScene(&m_scene);
//声明动画对象,赋值动画对象和 位置属性
m_anim = new Animation(m_item, "pos");
//设置动画曲线
m_anim->setEasingCurve(QEasingCurve::OutBounce);
//list默认选中第一个
m_ui.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) {
//TCB 样条曲线
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::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;
//枚举转文字
QMetaEnum metaEnum = mo.enumerator(mo.indexOfEnumerator("Type"));
// Skip QEasingCurve::Custom
for (int i = 0; i < QEasingCurve::NCurveTypes - 1; ++i) {
painter.fillRect(QRect(QPoint(0, 0), m_iconSize), brush);
//动画曲线
QEasingCurve curve = createEasingCurve((QEasingCurve::Type) i);
painter.setPen(QColor(0, 0, 255, 64));
qreal xAxis = m_iconSize.height()/1.5;
qreal yAxis = m_iconSize.width()/3;
//X轴、y轴
painter.drawLine(0, xAxis, m_iconSize.width(), xAxis);
painter.drawLine(yAxis, 0, yAxis, m_iconSize.height());
//比例为icon的一半
qreal curveScale = m_iconSize.height()/2;
painter.setPen(Qt::NoPen);
// start point 开始点
painter.setBrush(Qt::red);
QPoint start(yAxis, xAxis - curveScale * curve.valueForProgress(0));
painter.drawRect(start.x() - 1, start.y() - 1, 3, 3);
// end point 结束点
painter.setBrush(Qt::blue);
QPoint end(yAxis + curveScale, xAxis - curveScale * curve.valueForProgress(1));
painter.drawRect(end.x() - 1, end.y() - 1, 3, 3);
//绘制路径
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, QColor(32, 32, 32));
painter.setRenderHint(QPainter::Antialiasing, false);
QListWidgetItem *item = new QListWidgetItem;
item->setIcon(QIcon(pix));
item->setText(metaEnum.key(i));
m_ui.easingCurvePicker->addItem(item);
}
}
void Window::startAnimation()
{
//开启动画
m_anim->setStartValue(QPointF(0, 0));
m_anim->setEndValue(QPointF(100, 100));
m_anim->setDuration(2000);
m_anim->setLoopCount(-1); // forever
m_anim->start();
}
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;
//判断是否存振幅、周期和过冲
m_ui.periodSpinBox->setEnabled(isElastic);
m_ui.amplitudeSpinBox->setEnabled(isElastic || isBounce);
m_ui.overshootSpinBox->setEnabled(curveType >= QEasingCurve::InBack && curveType <= QEasingCurve::OutInBack);
}
void Window::pathChanged(QAbstractButton *button)
{
const int index = m_ui.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);
}
- /easing/window.h
#include <QtWidgets>
#include "ui_form.h"
#include "animation.h"
class PixmapItem : public QObject, public QGraphicsPixmapItem
{
Q_OBJECT
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:
//创建曲线对应的icon,放到list中
void createCurveIcons();
//开启动画
void startAnimation();
Ui::Form m_ui;
//场景对象
QGraphicsScene m_scene;
//场景中的元素
PixmapItem *m_item;
//自定义动画对象
Animation *m_anim;
//listview的icon大小
QSize m_iconSize;
};
4.实现过程分析
-
官方demo使用自定义的Animation继承QPropertyAnimation,实现了updateCurrentTime虚函数来实现圆的动作。
-
setEasingCurve对动画类设置查直曲线,其中曲线有多种类型见QEasingCurve类中枚举
-
new Animation(m_item, “pos”);时候指定了pos属性,这是QWidget中的属性,如果要改变别的属性,怎么查。
我们可以看QWidget头文件中使用Q_PROPERTY的属性即可。
-
createCurveIcons中绘制模拟插值曲线的实现很精妙,大家可以抄袭。
-
QEasingCurve内置很多插值曲线,但是常用的就几种