前言
目前见的比较多的轮播图有平移和旋转两种方式。平移类似淘宝那种切换幻灯片一样的效果,旋转一般是近大远小,看起来有点3D的感觉。本文代码实现旋转轮播图效果如下:
完整代码链接: https://github.com/gongjianbo/MyTestCode/tree/master/Qt/TestQt_20211029_Swiper
设计思路思路
1.计算坐标
可以现在 3D 场景中设计效果,图片是绕着 Y 轴旋转的,如下图:
由于我们的观察点是固定的,就能拆分成两个平面进行处理:
一个是 xz 组成的旋转轨迹平面,x 对应绘制时的坐标点 x 分量,z 对应绘制时的前后顺序,近大远小,同时 z 值更大的堆叠在上层。可以根据球圆上点坐标的方式根据旋转角度计算 x 和 z 的值。
另一个是绘制时的 xy 屏幕坐标系,可以直接将 y 在图上居中,x 取上一步获取到的 x 值。要注意的是 Qt 绘制时,原点在左上角,右下角为正方向。
2.切换动画
切换动画可以用 QPropertyAnimation 属性动画来做。
要注意的一个点就是头尾切换时。如果第一个切换到最后一个,可以将当前 index 设置为最后一个 index+1,这样再去减 1 动画就能正常过渡;或者最后一个切换到第一个,可以将当前 index 设置为第一个 index-1,这样再去加 1 动画就能正常过渡。否则可能出现从头逐个切换到最后或者反过来,这明显就破坏了旋转的连续性。
主要实现代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPainterPath>
#include <QMouseEvent>
#include <QHoverEvent>
#include <QPainter>
#include <QImage>
#include <QPropertyAnimation>
#include <QTimer>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
Q_PROPERTY(double curIndex READ getCurIndex WRITE setCurIndex NOTIFY curIndexChanged)
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
double getCurIndex() const;
void setCurIndex(double index);
protected:
bool event(QEvent *e) override;
void paintEvent(QPaintEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
private:
//计算图片位置
void calcImagePos();
//计算按钮位置
void calcBtnPath();
//切换到下一个图
void toPrev();
//切换到上一个图
void toNext();
signals:
void curIndexChanged();
private:
Ui::MainWindow *ui;
struct ImageNode{
QImage img;
double xf{0};
double yf{0};
double zf{0};
};
//当前图片列表
QVector<ImageNode> imageList;
//按钮位置
QVector<QRectF> btnList;
//图片z堆叠顺序
QVector<int> drawList;
//当前设置的index
int setIndex{0};
//属性动画绘制用到的index
double curIndex{0.0};
//旋转动画
QPropertyAnimation animation;
//自动切换定时器
QTimer swipTimer;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <cmath>
#include <QtMath>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setMouseTracking(true);
//界面上放了两个按钮用来前进后退,切换当前图片
connect(ui->btnPrev,&QPushButton::clicked,this,&MainWindow::toPrev);
connect(ui->btnNext,&QPushButton::clicked,this,&MainWindow::toNext);
//随便搞五个色块image当作图片
int alpha=255;
QList<QColor> colors=QList<QColor>{
QColor(255,0,0,alpha),
QColor(0,255,0,alpha),
QColor(0,0,255,alpha),
QColor(255,255,0,alpha),
QColor(0,255,255,alpha) };
for(int i=0;i<colors.size();i++)
{
ImageNode node;
node.img=QImage(200,120,QImage::Format_ARGB32);
node.img.fill(colors.at(i));
QPainter p(&node.img);
p.fillRect(QRectF(10,10,20,20),Qt::gray);
imageList.append(node);
}
calcImagePos();
calcBtnPath();
//自动切换
connect(&swipTimer,&QTimer::timeout,[this]{
//这里可以判断下是否hover某个图,不切换,略
toNext();
});
swipTimer.start(2000);
//切换动画
animation.setTargetObject(this);
animation.setPropertyName("curIndex");
animation.setEasingCurve(QEasingCurve::OutQuart);
animation.setDuration(1000);
//动画结束后定时切换
connect(&animation,&QPropertyAnimation::stateChanged,
this,[this](QAbstractAnimation::State newState, QAbstractAnimation::State oldState){
oldState;
if(newState!=QAbstractAnimation::Stopped){
swipTimer.stop();
}else{
swipTimer.start(2000);
}
});
}
MainWindow::~MainWindow()
{
//避免结束程序stop时异常
swipTimer.disconnect(this);
animation.disconnect(this);
swipTimer.stop();
animation.stop();
delete ui;
}
double MainWindow::getCurIndex() const
{
return curIndex;
}
void MainWindow::setCurIndex(double index)
{
curIndex=index;
emit curIndexChanged();
//属性动画设置值会调用该接口,此处计算位置并刷新
calcImagePos();
update();
}
bool MainWindow::event(QEvent *e)
{
switch (e->type()) {
case QEvent::HoverMove:{
QHoverEvent *hover = static_cast<QHoverEvent*>(e);
if(hover){
//放到哪个按钮上就切换到对应图片
for(int i=0;i<btnList.size();i++)
{
if(btnList.at(i).contains(hover->pos()))
{
setIndex=i;
animation.stop();
animation.setStartValue(getCurIndex());
animation.setEndValue((double)setIndex);
animation.start();
break;
}
}
}
}
break;
default:
break;
}
return QMainWindow::event(e);
}
void MainWindow::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.fillRect(rect(),Qt::white);
if(imageList.isEmpty())
return;
//切换两种效果
if(!ui->checkBox->isChecked())
{
painter.save();
//平移到中心点绘制,便于计算
painter.translate(width()/2,height()/2);
for(int i=0;i<imageList.size()&&i<drawList.size();i++)
{
const ImageNode &node=imageList.at(drawList.at(i));
QPointF center=QPointF(node.xf*node.img.width(),
node.zf*30);
//缩放系数归一化到[0.5,1.0]
const double scale=0.5+0.25*(node.zf+1);
QRectF rect=QRectF(0,0,
node.img.width()*scale,
node.img.height()*scale);
rect.moveCenter(center);
painter.drawImage(rect,node.img);
}
painter.restore();
}
else
{
for(int i=0;i<imageList.size()&&i<drawList.size();i++)
{
const ImageNode &node=imageList.at(drawList.at(i));
QPointF center=QPointF(node.xf*node.img.width(),
node.zf*20);
//缩放系数归一化到[0.5,1.0]
const double scale=0.5+0.25*(node.zf+1);
QRectF rect=QRectF(0,0,
node.img.width()*scale,
node.img.height()*scale);
rect.moveCenter(rect.topLeft());
painter.save();
painter.translate(width()/2,height()/2);
painter.translate(center);
QTransform trans;
const double step=360.0/imageList.size();
const double degree=step*drawList.at(i)-curIndex*step;
trans.rotate(-degree,Qt::YAxis);
painter.setTransform(trans, true);
painter.drawImage(rect,node.img);
painter.restore();
}
}
//底部画几个圆圈
painter.setPen(Qt::NoPen);
painter.setRenderHint(QPainter::Antialiasing);
for(int i=0;i<btnList.size();i++)
{
painter.setBrush(i==setIndex?Qt::black:Qt::gray);
painter.drawEllipse(btnList.at(i));
}
}
void MainWindow::resizeEvent(QResizeEvent *event)
{
QMainWindow::resizeEvent(event);
calcBtnPath();
}
void MainWindow::calcImagePos()
{
if(imageList.isEmpty())
return;
drawList.resize(imageList.size());
//每个图之间的角度间隔
const double step=360.0/imageList.size();
//绘制时会平移中心点,所以这里以0.0为中心点计算
for(int i=0;i<imageList.size();i++)
{
ImageNode &node=imageList[i];
//0度为0,+90度是让当前图z值为1
const double degree=90+step*i-curIndex*step;
const double radians=qDegreesToRadians(degree);
//取反则为顺时针变化
node.xf=-cos(radians);
node.zf=sin(radians);
//存下标用于计算堆叠顺序
drawList[i]=i;
}
//根据z排堆叠顺序
std::sort(drawList.begin(),drawList.end(),
[this](int a, int b){
return imageList.at(a).zf<imageList.at(b).zf;
});
}
void MainWindow::calcBtnPath()
{
if(imageList.isEmpty())
return;
//按钮也可以根据curIndex值来做过渡动画,略
btnList.resize(imageList.size());
//绘制位置的规则自己随便定
int w=rect().width();
int h=rect().height();
//底部画几个圆圈
int btn_space=20;
int btn_width=14;
int bar_width=btn_width*imageList.size()+btn_space*(imageList.size()-1);
for(int i=0;i<imageList.size();i++)
{
btnList[i]=QRectF(w/2-bar_width/2+(btn_space+btn_width)*i,
h/2+100,
btn_width,btn_width);
}
}
void MainWindow::toPrev()
{
animation.stop();
setIndex--;
//到头了,就切换到尾巴上,为了动画连续,startvalue也设置到尾巴上
if(setIndex<0){
setIndex=imageList.size()-1;
animation.setStartValue(getCurIndex()+imageList.size());
}else{
animation.setStartValue(getCurIndex());
}
animation.setEndValue((double)setIndex);
animation.start();
}
void MainWindow::toNext()
{
animation.stop();
setIndex++;
//到尾了,就切换到头上,为了动画连续,startvalue也设置到头上
if(setIndex>imageList.size()-1){
setIndex=0;
animation.setStartValue(getCurIndex()-imageList.size());
}else{
animation.setStartValue(getCurIndex());
}
animation.setEndValue((double)setIndex);
animation.start();
}