Qt绘制旋转的轮播图

前言

目前见的比较多的轮播图有平移和旋转两种方式。平移类似淘宝那种切换幻灯片一样的效果,旋转一般是近大远小,看起来有点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();
}

  • 10
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值