Search code examples
pythonpyqt5qgraphicsviewqtimerqvariantanimation

How to make a constant and smooth animation?


So I want to make an animation as the arrow from the Waze app, I want that it moves smoothly and constant. I don't know how to do it inside the QtGraphicsView. My object(arrow) moves by an QVariantAnimation, Which interpolates from the current position until the next position. End it slightly stops. I don't wanna this feature (stops), I want that my animation runs continuous and smoothly. Does anyone know how?

Here is a minimum reproducible example:

from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5 import QtGui
import sys
import random      

random.seed(0)
             
class Example(QtWidgets.QWidget):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.button = QtWidgets.QPushButton("Start", self)
        self.button.clicked.connect(self.doAnim)
        self.button.move(10, 10)
        self.scene = QtWidgets.QGraphicsScene()
        self.view = QtWidgets.QGraphicsView(self)
        self.view.setScene(self.scene)
        self.view.setGeometry(150, 30, 500, 800)
        self.setGeometry(300, 300, 380, 300)
        self.setWindowTitle('Animation')
        pen = QtGui.QPen()
        pen.setBrush(QtGui.QBrush(QtCore.Qt.darkBlue))
        pen.setWidth(5)
        self.scene.addEllipse(0,0,10,10, pen)
        self.show()  
        self.ellipse = self.scene.items()[0]

    def doAnim(self):
        # Every time that I click on the start buttom this animation runs 
        # and stops, 
        pos = self.ellipse.pos()
        new_pos = QtCore.QPointF(pos.x()+ random.randint(-10, 10), pos.y() +random.randint(-10, 10))
        self.anim = QtCore.QVariantAnimation()
        self.anim.setDuration(1000)
        self.anim.setStartValue(pos)
        self.anim.setEndValue(new_pos)
        self.anim.setLoopCount(-1)
        self.anim.valueChanged.connect(self.ellipse.setPos)
        self.anim.start(QtCore.QVariantAnimation.DeleteWhenStopped)

    

if __name__ == "__main__":
    import sys 
    app = QtWidgets.QApplication(sys.argv)
    ex = Example()
    ex.show()
    app.exec_()

Solution

  • The simple solution is to connect to the finished signal of the animation, and set again the start/end values if a new target position is available.

    from random import randrange
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class Example(QtWidgets.QWidget):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            layout = QtWidgets.QGridLayout(self)
            self.startButton = QtWidgets.QPushButton("Start")
            layout.addWidget(self.startButton)
            self.addPointButton = QtWidgets.QPushButton("Add target point")
            layout.addWidget(self.addPointButton, 0, 1)
    
            self.view = QtWidgets.QGraphicsView()
            layout.addWidget(self.view, 1, 0, 1, 2)
            self.scene = QtWidgets.QGraphicsScene()
            self.view.setScene(self.scene)
            self.scene.setSceneRect(-10, -10, 640, 480)
    
            pen = QtGui.QPen(QtCore.Qt.darkBlue, 5)
            self.ellipse = self.scene.addEllipse(0, 0, 10, 10, pen)
    
            self.queue = []
    
            self.startButton.clicked.connect(self.begin)
            self.addPointButton.clicked.connect(self.addPoint)
            self.anim = QtCore.QVariantAnimation()
            self.anim.setDuration(1000)
            self.anim.valueChanged.connect(self.ellipse.setPos)
            self.anim.setStartValue(self.ellipse.pos())
            self.anim.finished.connect(self.checkPoint)
    
        def begin(self):
            self.startButton.setEnabled(False)
            self.addPoint()
    
        def addPoint(self):
            self.queue.append(QtCore.QPointF(randrange(600), randrange(400)))
            self.checkPoint()
    
        def checkPoint(self):
            if not self.anim.state() and self.queue:
                if self.anim.currentValue():
                    # a valid currentValue is only returned when the animation has
                    # been started at least once
                    self.anim.setStartValue(self.anim.currentValue())
                self.anim.setEndValue(self.queue.pop(0))
                self.anim.start()