qtqpainterpathqpropertyanimation

Change QPropertyAnimation duration on the fly


I'm using this solution to animate ellipses along a QPainterPath. But I need to slowly increase or decrease the speed of the animation. I create a timer and set a new duration for the animation but the result is a choppy animation because the ellipses start from the beginning. Is there a better way to accomplish this?

class AnimatedEllipses: public QGraphicsObject
{
    Q_OBJECT
    Q_PROPERTY(int progress READ progress WRITE setProgress)
private:
    QGraphicsPathItem path;
    QList<QGraphicsEllipseItem*> ellipses;
    int propProgress;
    QPropertyAnimation* animation;

public:
    int progress() const { return propProgress;}
    void setProgress(int value)
    {
        propProgress = value;
        int index = 0;
        for (QGraphicsEllipseItem* ellipse: ellipses)
        {
            // Keep value between 0 and length.
            int lgt = (propProgress + index * 40) % int(path.path().length());
            qreal percent = path.path().percentAtLength(lgt);
            ++index;
            ellipse->setPos(path.path().pointAtPercent(percent));
        }
    }
    AnimatedEllipses(QPainterPath const& path): QGraphicsObject(), path(path), propProgress(0)
    {
        qreal pos = 0;
        qreal length = path.length();
        while (pos < length)
        {
            qreal percent = path.percentAtLength(pos);
            QPointF pointAtPercent = path.pointAtPercent(percent);
            pos += 40;
            QGraphicsEllipseItem * item = new QGraphicsEllipseItem(-10, -10, 20, 20, this);
            item->setPos(pointAtPercent);
            ellipses << item;
        }

        animation = new QPropertyAnimation(this, "progress");
        animation->setStartValue(0);
        animation->setEndValue(length);
        animation->setDuration(10000);
        animation->setLoopCount(-1);
        animation->start();

        QTimer *timer = new QTimer();
        connect(timer, SIGNAL(timeout()), this, SLOT(SlotTimeOut()));
        timer->start(1000);
    }

    void SlotTimeOut() {
        int newDuration = GetRandomDuration();
        animation->setDuration(newDuration);
    }

    // QGraphicsItem interface
public:
    QRectF boundingRect() const { return path.boundingRect();}
    void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget){}
};

Solution

  • You can do this with custom QEasingCurve. Here is a small example for progressbar value.

    MainWindow.h

    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    public:
        explicit MainWindow(QWidget *parent = 0);
        ~MainWindow();
    
    private slots:
        void OnChangeDurationTimer();
    
    private:
        Ui::MainWindow *ui;
        QPropertyAnimation m_animProgress;
    };
    

    Initialization

    MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent),
        ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
    
        m_animProgress.setTargetObject(ui->progressBar);
        m_animProgress.setPropertyName("value");
        m_animProgress.setStartValue(0);
        m_animProgress.setEndValue(100);
        m_animProgress.setDuration(10000);
        m_animProgress.setLoopCount(-1);
    
        //setting custom QEasingCurve
        QEasingCurve eCurve;
        eCurve.setCustomType(myEasingFunction);
        m_animProgress.setEasingCurve(eCurve);
    
        m_animProgress.start();
    
        QTimer::singleShot(3000, this, &MainWindow::OnChangeDurationTimer);  //timer to change duration
    }
    

    Most interesting is function myEasingFunction for custom EasingCurve

    qreal g_offset = 0;       //value last animation stopped with
    qreal g_offsetLast = 0;   //keep current value of animation
    
    qreal myEasingFunction(qreal progress)
    {
        qreal val = g_offset + progress;
        while (val > 1) {
            val -= 1;  //normalize
        }
        g_offsetLast = val;
        return val;
    }
    

    And changing duration on timer

    void MainWindow::OnChangeDurationTimer()
    {
        g_offset = g_offsetLast;  //remember stopped value
        m_animProgress.stop();
        m_animProgress.setDuration((rand() % 10 + 1) * 1000);
        m_animProgress.start();
    
        QTimer::singleShot(3000, this, &MainWindow::OnChangeDurationTimer);  //next changing
    }