这次和大家分享一个沙漏风格的加载动画
效果如下:
这是本系列的第六季了, 本次内容的关键在于cubicTo函数的使用, 在这里分享一个非常好用的网站https://www.desmos.com/calculator/cahqdxeshd
在这上面可以手动拖动贝塞尔曲线的控制点, 并且显示了起终点和两个控制点的精确坐标, 这样来使用qt的cubicTo函数就非常方便了.
一共三个文件,可以直接编译运行
//main.cpp
#include "LoadingAnimWidget.h"
#include <QApplication>
#include <QGridLayout>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
w.setWindowTitle("加载动画 第6季");
QGridLayout * mainLayout = new QGridLayout;
auto* anim1= new FillGlassBead;
mainLayout->addWidget(anim1,0,0);
auto* anim2 = new FillGlassBead;
anim2->setWaveType(FillGlassBead::WaveType::SwayingWater);
mainLayout->addWidget(anim2,0,1);
auto* anim3 = new Hourglass;
mainLayout->addWidget(anim3,1,0);
auto* anim4 = new Hourglass;
anim4->setColumnVisibility(true);
anim4->setSandColor("seagreen");
mainLayout->addWidget(anim4,1,1);
w.setLayout(mainLayout);
w.show();
anim1->start();
anim2->start();
anim3->start();
anim4->start();
return a.exec();
}
//LoadingAnimWidget.h
#ifndef LOADINGANIMWIDGET_H
#define LOADINGANIMWIDGET_H
#include <QPropertyAnimation>
#include <QWidget>
class LoadingAnimBase:public QWidget
{
Q_OBJECT
Q_PROPERTY(qreal angle READ angle WRITE setAngle)
public:
LoadingAnimBase(QWidget* parent=nullptr);
virtual ~LoadingAnimBase();
qreal angle()const;
void setAngle(qreal an);
public slots:
virtual void exec();
virtual void start();
virtual void stop();
protected:
QPropertyAnimation mAnim;
qreal mAngle;
};
class FillGlassBead:public LoadingAnimBase{
public:
FillGlassBead(QWidget* parent = nullptr);//一颗玻璃珠,内部逐渐充满液体
enum class WaveType{
PeacefulWater /*平静的水面*/ , SwayingWater /*左右晃动的水面*/
};
void setWaveType(WaveType t);
protected:
void paintEvent(QPaintEvent*);
private:
WaveType mWaveType;
};
class Hourglass:public LoadingAnimBase{
public:
Hourglass(QWidget* parent = nullptr);//沙漏
void setSandColor(const QColor& color);//设置沙子颜色
void setColumnVisibility(bool vis);//设置柱子可见性
protected:
void paintEvent(QPaintEvent*);
private:
QColor mSandColor;
bool mColumnVisible;
};
#endif // LOADINGANIMWIDGET_H
//LoadingAnimWidget.cpp
#include "LoadingAnimWidget.h"
#include <QDebug>
#include <QPaintEvent>
#include <QPainter>
#include <QtMath>
LoadingAnimBase::LoadingAnimBase(QWidget* parent):QWidget(parent){
mAnim.setPropertyName("angle");
mAnim.setTargetObject(this);
mAnim.setDuration(2000);
mAnim.setLoopCount(-1);//run forever
mAnim.setEasingCurve(QEasingCurve::Linear);
setFixedSize(200,200);
mAngle = 0;
}
LoadingAnimBase::~LoadingAnimBase(){}
void LoadingAnimBase::exec(){
if(mAnim.state() == QAbstractAnimation::Stopped){
start();
}
else{
stop();
}
}
void LoadingAnimBase::start(){
mAnim.setStartValue(0);
mAnim.setEndValue(360);
mAnim.start();
}
void LoadingAnimBase::stop(){
mAnim.stop();
}
qreal LoadingAnimBase::angle()const{ return mAngle;}
void LoadingAnimBase::setAngle(qreal an){
mAngle = an;
update();
}
FillGlassBead::FillGlassBead(QWidget* parent):LoadingAnimBase (parent){
mAnim.setDuration(3600);
mWaveType = WaveType::PeacefulWater;
}
void FillGlassBead::setWaveType(WaveType t){
if(mWaveType != t){
mWaveType = t;
update();
}
}
void FillGlassBead::paintEvent(QPaintEvent* e){
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const int x = width();
const int y = height();
static const QColor color("lightseagreen");
if(mAngle < 90){
//只要画一个坠落的小球
painter.translate(x/2,0);
painter.setPen(Qt::NoPen);
painter.setBrush(QBrush(color));
qreal posY = y/4.0 * mAngle / 90;//坠落的小球最低点是高度的四分之一
painter.drawEllipse(QPointF(0,posY),5,5);
}
else{
painter.translate(x/2,0.625*y);
QPen pen(color);
pen.setWidth(4);
painter.setPen(pen);
painter.setBrush(Qt::NoBrush);
const qreal r = 0.375*y;
if(mAngle < 225){
//画一个包裹玻璃珠的圆环
//最高点起点角度是90,弧长是0,最低点起点角度是-90,弧长是360
qreal proportion = (mAngle - 90) / 135.0;
painter.drawArc(QRectF(-r,-r,2*r,2*r), 16*(90-180*proportion),16*360*proportion);
}
else{
//画一个退去的包裹圆环
//最开始起点角度是-90,最后起点角度是90
qreal proportion = (mAngle - 225) / 135.0;
painter.drawArc(QRectF(-r,-r,2*r,2*r),16*(-90+180*proportion),16*(360 - proportion*360));
//再画一个上涨的水波
QPainterPath pp;
const qreal startx = -x/2;
const qreal starty = 0.375*y - 0.75*y*proportion;
if(mWaveType == WaveType::PeacefulWater){
pp.addRect(QRectF(startx,starty,x,y));
}
else{
QPointF start(startx,starty);
QPointF end(start.x() + x,start.y());
const qreal h = qSin(4*M_PI * proportion) *x * 0.3;
QPointF c1( start.x() + 0.25*x, start.y() + h);
QPointF c2( start.x() + 0.75*x, start.y() - h);
pp.moveTo(start);
pp.cubicTo(c1,c2,end);
pp.lineTo(end.x(),999);//999没有具体含义,大一点就行了
pp.lineTo(startx,999);
pp.closeSubpath();
}
painter.setClipPath(pp);
painter.setPen(Qt::NoPen);
painter.setBrush(QBrush(color));
const qreal r2 = r - 4;
painter.drawEllipse(QRectF(-r2,-r2,2*r2,2*r2));
}
}
}
Hourglass::Hourglass(QWidget* parent):LoadingAnimBase (parent),mSandColor("lime"),mColumnVisible(false){ }
void Hourglass::setSandColor(const QColor& color){
if(mSandColor != color){
mSandColor = color;
update();
}
}
void Hourglass::setColumnVisibility(bool vis){
if(vis != mColumnVisible){
mColumnVisible = vis;
update();
}
}
void Hourglass::paintEvent(QPaintEvent*){
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const qreal x = width();
const qreal y = height();
//step1: 先画上下两个瓶盖
qreal ang = (mAngle - 320)/40;//到320度的时候要旋转瓶子
if(ang > 1) ang = 1;
if(ang < 0) ang = 0;
ang *= 180;
QPen pen("lightslategray");//岩灰色的瓶身,就像我冰冷的内心
pen.setWidth(16);
pen.setCapStyle(Qt::RoundCap);
painter.setBrush(Qt::NoBrush);
painter.setPen(pen);
painter.translate(x/2,y/2);
painter.rotate(ang);
painter.drawLine(-x/4,-0.375*y,x/4,-0.375*y);//上方瓶盖
painter.drawLine(-x/4,0.375*y,x/4,0.375*y);// 下方瓶盖
//step2: 画一个瓶身
QPainterPath pp;
const int gap1 = 8;//瓶身距离四分之一水平位置的间距,这个值越大,沙漏越瘦
QPointF start(-x/4 + gap1 , -0.375*y);
QPointF end(-x/32 , 0);
QPointF c1(start.x(),end.y());
QPointF c2(end.x(),end.y() + 0.4 * (start.y() - end.y()));
pp.moveTo(start);
pp.cubicTo(c1,c2,end);
const qreal penWidth = 6;
pen.setWidthF(penWidth);
painter.setPen(pen);
painter.drawPath(pp);//瓶身轮廓左上部分
painter.rotate(180);
painter.drawPath(pp);//右下
painter.rotate(-180);
painter.scale(1,-1);
painter.drawPath(pp);//右上
painter.rotate(180);
painter.drawPath(pp);//左下
//step3: 画两根小柱子(可选)
if(mColumnVisible){
pen.setWidthF(4);
painter.setPen(pen);
painter.drawLine(-x/4,start.y(),-x/4,-start.y());
painter.drawLine(x/4,start.y(),x/4,-start.y());
}
painter.resetTransform();
painter.translate(x/2,y/2);
painter.setPen(Qt::NoPen);
painter.setBrush(QBrush(mSandColor));
if(mAngle < 320){ //step4: 画动态的沙子
//画上面的沙子
QPainterPath sand;
start.setX(start.x() + penWidth/2);//沙子区域要瘦一点,免得盖住了瓶身
end.setX(end.x() + penWidth/2); //沙子区域要瘦一点,免得盖住了瓶身
c1.setX(c1.x() + penWidth/2); //沙子区域要瘦一点,免得盖住了瓶身
c2.setX(c2.x() + penWidth/2); //沙子区域要瘦一点,免得盖住了瓶身
sand.moveTo(start);
sand.cubicTo(c1,c2,end);
sand.lineTo(0,(0.33-0.2*mAngle/320)*y);
sand.lineTo(QPointF(end.x()*-1,end.y()));
sand.cubicTo(QPointF(-c2.x(),c2.y()),QPointF(-c1.x(),c1.y()),QPointF(-start.x(),start.y()));
sand.lineTo(start);
painter.setClipPath(sand);
painter.drawRect(QRectF(-x/2,-y/4+mAngle/320 * y *0.38,x,x));
//画下面的沙子, 一个等腰三角形
QPointF a(start.x() * mAngle/320,0.33*y);
QPointF top(0,(0.33 - mAngle/320 * 0.2) * y);
QPointF b(-a.x(),a.y());
sand.moveTo(a);
sand.lineTo(top);
sand.lineTo(b);
sand.closeSubpath();
painter.setClipPath(sand);
painter.drawRect(-x/2,top.y(),x,x);//这个高度不能太随意,否则会把上面的沙子也画出来
}
else{
//旋转沙子
QPainterPath pp;
pp.moveTo(QPointF(start.x() + penWidth/2,0.33*y));
pp.lineTo(0,0.13*y);
pp.lineTo(QPointF(-start.x()-penWidth/2,0.33*y));
pp.closeSubpath();
QTransform bottoleTrans;
bottoleTrans.rotate(ang);
painter.setClipPath(bottoleTrans.map(pp));
painter.drawRect(QRectF(-x/2,-x/2,x,x));
}
}