Qt列表等控制項實現平滑滾動&deepin啟動器存在的問題

Qt自帶的的列表控制項是不能平滑滾動的,但如果滾動速度快的話很容易引起視線丟失,體驗效果很差。本篇主要講述如何在Qt中對列表控制項加入平滑滾動。文中以QScrollArea控制項為例,其他控制項方法一樣。

原理

Qt的列表控制項中,有以下兩個介面:

void QAbstractScrollArea::setHorizontalScrollBar(QScrollBar *scrollBar);
void QAbstractScrollArea::setVerticalScrollBar(QScrollBar *scrollBar);

顯然上述兩個介面的作用是設置控制項的橫縱兩個滾動條,以指針形式傳入,我們將以此來實現控制頁面的滾動。傳入滾動條對象後,我們就可以使用setValue()來間接控制頁面的滾動了。然後再使用QPropertyAnimation類來實現滾動的效果。

實現

一、自定義滾動條控制項

需要實現兩個功能:

  • 當使用setValue()對滾動條的滑塊進行移動時,滑塊會在一個時間段內以某種規律連續的移動到目標位置,而不是瞬間移動。
  • 新增一個槽函數void scroll(int value),實現傳入一個數字後相對滾動指定距離。如:scroll(100)就是向下滾動100個單位。

頭文件:

#ifndef SMOOTHSCROLLBAR_H
#define SMOOTHSCROLLBAR_H

#include <QScrollBar>
#include <QPropertyAnimation>
class SmoothScrollBar : public QScrollBar
{
    Q_OBJECT
public:
    SmoothScrollBar(QWidget *parent=nullptr);
private:
    //這裡重寫滑鼠事件的目的是在手動點擊或拖動滾動條時更新m_targetValue_v變數,並且在拖動時立即結束滾動的動畫。
    //這裡如果不明白作用,可以先注釋掉看看手動拖動滾動條時對動畫有什麼影響。
    void mousePressEvent(QMouseEvent *) override;
    void mouseReleaseEvent(QMouseEvent *) override;
    void mouseMoveEvent(QMouseEvent *) override;

    QPropertyAnimation *m_scrollAni; //用來實現動畫
    int m_targetValue_v; //用來記錄目標位置的變數
public slots:
    void setValue(int value); //重寫的setValue槽函數,實現動畫效果
    void scroll(int value); //新增相對滾動的槽函數,value為滾動距離的矢量表示
signals:
};

#endif // SMOOTHSCROLLBAR_H

源文件:

#include "smoothscrollbar.h"
#include <QWheelEvent>
SmoothScrollBar::SmoothScrollBar(QWidget* parent):QScrollBar(parent)
{
    m_scrollAni=new QPropertyAnimation;
    m_scrollAni->setTargetObject(this);
    m_scrollAni->setPropertyName("value");
    m_scrollAni->setEasingCurve(QEasingCurve::OutQuint); //設置動畫曲線,在Qt文檔中有詳細的介紹
    m_scrollAni->setDuration(800); //設置動畫時間,數值越小播放越快
    m_targetValue_v=value(); //將m_targetValue_v初始化
}

void SmoothScrollBar::setValue(int value)
{
    m_scrollAni->stop();//停止現在的動畫,防止出現衝突
    m_scrollAni->setStartValue(this->value()); //設置動畫滾動的初始值為當前位置
    m_scrollAni->setEndValue(value); //設置動畫的結束位置為目標值
    m_scrollAni->start(); //開始動畫
}

void SmoothScrollBar::scroll(int value)
{
    m_targetValue_v-=value; //將目標值和相對位置進行運算
    setValue(m_targetValue_v); //開始動畫
}

void SmoothScrollBar::mousePressEvent(QMouseEvent *e)
{
    //當使用滑鼠操作滾動條時,不會刷新m_targetValue_v的值,因而需要重寫事件,對其進行刷新。
    m_scrollAni->stop();
    QScrollBar::mousePressEvent(e);
    m_targetValue_v=value();
}

void SmoothScrollBar::mouseReleaseEvent(QMouseEvent *e)
{
    m_scrollAni->stop();
    QScrollBar::mouseReleaseEvent(e);
    m_targetValue_v=value();
}

void SmoothScrollBar::mouseMoveEvent(QMouseEvent *e)
{
    m_scrollAni->stop();
    QScrollBar::mouseMoveEvent(e);
    m_targetValue_v=value();
}

二、自定義列表控制項

將列表的滾動條替換為我們剛剛自定義的滾動條

頭文件:

#ifndef SMOOTHSCROLLAREA_H
#define SMOOTHSCROLLAREA_H

#include <QWidget>
#include <QScrollArea>
#include "smoothscrollbar.h"
class SmoothScrollArea : public QScrollArea
{
    Q_OBJECT
public:
    explicit SmoothScrollArea(QWidget *parent = nullptr);
private:
    SmoothScrollBar* vScrollBar; //縱向滾動條
    void wheelEvent(QWheelEvent* e); //捕獲滑鼠滾輪事件
};

#endif // SMOOTHSCROLLAREA_H

源文件:

#include "smoothScrollArea.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QWheelEvent>
#include <QDebug>
SmoothScrollArea::SmoothScrollArea(QWidget *parent) : QScrollArea(parent)
{
    auto layout = new QVBoxLayout;
    vScrollBar=new SmoothScrollBar();
    vScrollBar->setOrientation(Qt::Orientation::Vertical); //將滾動條設置為縱向
    QWidget* w=new QWidget; //主體Widget
    for (int i=0;i<200 ;i++ ) { //在w中加入200個label,用來測試滾動
        QFont font;
        font.setPointSize(i+1);
        auto a=new QLabel(QString::number(i));
        a->setFont(font);
        layout->addWidget(a);
    }
    setVerticalScrollBar(vScrollBar); //設置縱向滾動條
    w->setLayout(layout); //設置布局
    setWidget(w); //設置widget

}

void SmoothScrollArea::wheelEvent(QWheelEvent *e)
{
    //當捕獲到事件後,調用相對滾動的槽函數
    vScrollBar->scroll(e->angleDelta().y());
}

到此為止,SmoothScrollArea類便可以支援縱向的平滑滾動。其他的列表控制項方法一致。

三、測試效果

SmoothScrollArea列表控制項加入到主窗口後,運行即可。

補充

在此之前參考過deepin-launcher的小窗口模式列表程式碼,deepin的平滑滾動策略存在缺陷,導致體驗較差。這裡我詳細說明一下這一簡單的問題,非deepin用戶或開發者可以到此為止了。

/**
 * @brief AppListView::wheelEvent 滑鼠滑輪事件觸發滑動區域控制項動畫
 * @param e 滑鼠滑輪事件指針對象
 */
void AppListView::wheelEvent(QWheelEvent *e)
{
    if (e->orientation() == Qt::Horizontal)
        return;

    int offset = -e->delta();

    m_scrollAni->stop();
    m_scrollAni->setStartValue(verticalScrollBar()->value());
    m_scrollAni->setEndValue(verticalScrollBar()->value() + offset * m_speedTime);
    m_scrollAni->start();
}

由程式碼中可以看到,首先他屏蔽了橫向滑動的事件,這個主要應對觸控板的一些問題。

offset為滾動的距離,每次滾動之前先停止上次動畫(如果還沒有結束的話),然後在以當前位置為起始位置,以相對offset的位置為結束位置,這就會導致一個問題。

假設有兩個連續的滾動事件被觸發,上邊這個函數就會執行兩次。這裡會出現兩種情況:

  • 兩次連續的事件時間間隔比較大,滑鼠滾輪速度比較慢。
  • 兩次連續事件時間間隔很小,小於動畫時長,滑鼠滾動速度很快。

第一種情況下,deepin的這個方案並沒有什麼問題,兩次動畫之間互不干擾。但是第二種情況就會發現問題,假設動畫時長為800ms,當第一次滾動事件觸發後,隨後800ms的時間中,滾動條將相應的向目標位置滾動,但是當第二次事件在第一次的動畫還未結束時到來的話,第一次的動畫將被打斷,也就是只滾動了一部分就結束了,因此,當連續的快速滾動事件結束後,實際滾動的距離要遠小於期望的距離。

這個問題在本文中的解決方案時建立一個目標位置的變數。此變數用於記錄滾動所期望的位置,不會導致滾動失真。

大致如下:

設記錄目標位置的變數為a,a的值將被初始化為滾動條當前的value,此後,當滑鼠滾動事件被觸發時,首先將a的值通過和滾動距離的計算變為新的位置,此時當前位置與a的值將不再相等,然後在通過動畫將結束位置定為a。這樣處理的好處,通過上邊的方法分析如下:

首先第一種情況是沒有區別的,來看第二種。同樣假設動畫時方案長為800ms,當第一次滾動事件發生後,a將被計算為要滾動的目標位置,隨後的800ms將是動畫的執行過程,當這個過程還未結束時,第二次滾動又將a的值通過和滾動距離的計算,變為一個新的位置,再由動畫去執行。這裡注意,a在兩次變化中,第一次的距離並未丟失,兩次距離相加,當連續的快速滾動事件結束後,實際滾動的距離等於所期望的距離。

deepin那種方案所導致的現象

  • 快速滾動滾輪並不能讓列表的滾動速度便快,甚至還可能不如滾輪滾的慢一點。
  • 滾動的動畫將會在一些情況下看起來不流暢

希望deepin能採納文中的方案。
文中程式碼下載地址://maicss.lanzoui.com/iQHCHswqm6j