简介:SideWidget.zip 面向具备一定 Qt 基础的桌面应用开发者,聚焦侧边栏抽屉式渐入渐出交互的实现,适合希望为界面补充流畅动效、提升视觉层次的中级学习者参考。包内共 12 个文件,以 4 个 cpp 源文件与 3 个 h 头文件承载界面逻辑与动画调度,3 个 ui 文件提供主窗口、侧边栏和表单的可视化布局,另含 1 个 pro 工程配置与 1 个 user 环境记录,整体约 9KB,体量轻巧,便于直接编译运行。代码围绕 QPropertyAnimation 对 geometry 与透明度属性做插值,配合 onSlideOut()、onSlideIn() 两个槽函数及信号槽绑定,串联按钮点击、鼠标事件与动画播放,清晰示范了滑出时宽度递增、透明度由 0 到 1 的完整过程,并对 duration 与 easingCurve 的调节留有空间。已有 1242 人学习,可作为理解 Qt 动画框架与自定义 Widget 协作的范例,也便于改造为可停靠侧栏或迁移到实际项目。
1. 侧边栏不该靠setFixedWidth贴近边缘:SideWidget.zip 的用途
很多人第一次做侧边栏,都是拖一个 QWidget 贴边停靠,setFixedWidth设完就直接交付,随后因产品提出“要能滑出来、要有点淡入的感觉”而返工。SideWidget.zip 针对这个场景:纯 Qt Widgets 侧边栏组件,宽度动画配透明度渐变,展开与收起复用同一套状态机,不依赖 QML,C++ 工程直接编。适合已经会写 Qt 界面但没系统做过属性动画的人,也适合想找一个可改可拆的动画骨架的老手。下面按拆包、接入、动画骨架、交互联动、排查的顺序走一遍。
2. 拆包与工程接入:把 SideWidget 编进现有 Qt 工程
拿到一个 zip,最忌讳的操作是先把main.cpp和CMakeLists.txt原封不动拖进项目根目录。组件类资源通常自带一份 demo 工程,demo 里的main会和你的main冲突,CMakeLists.txt里的project()和qt_add_executable()也会抢主工程的构建目标。我的习惯是先单独解压、单独编一次,确认能跑起来,再谈合并。
2.1 三类文件的职责边界
先看文件清单,别急着#include:
# 只看不写,确认压缩包里有没有 demo 工程和资源文件 unzip -l SideWidget.zip # 解到一个独立的第三方目录,别污染 src/ unzip -q SideWidget.zip -d ./third_party/sidewidget # 两层目录足够看清结构,再深就是 Qt 自动生成的构建产物了 find ./third_party/sidewidget -maxdepth 2 -type f | sort这类包的典型结构大致分三类,文件名以你解压出来的为准:
| 文件类型 | 常见位置 | 职责 | 你该不该改 |
|---|---|---|---|
| 对外头文件 | include/sidewidget.h | 公开接口、Q_PROPERTY属性声明、信号 | 尽量不改,改了就锁死升级路径 |
| 实现文件 | src/sidewidget.cpp | 动画驱动、事件过滤、paintEvent绘制 | 要改就改这里 |
| 资源与 demo | res/*.qss、demo/*.cpp | 默认样式、最小可运行验证 | demo 只读,样式按项目换 |
判断一个组件包能不能用,我的标准很朴素:头文件里有没有Q_PROPERTY。如果展开收起全靠setFixedWidth加QTimer轮询,那它本质上没有“可动画属性”,你后续想接QEasingCurve、想中途打断、想做“半展开”都做不了。有Q_PROPERTY(qreal …)或Q_PROPERTY(int …)的,才是能改的骨架。
2.2 CMake 与 qmake 两条接入路径
接入方式取决于主工程用哪套构建系统。CMake 下我一般编成STATIC库而不是把 cpp 直接塞进add_executable,理由是动画参数你大概率要调十几轮,独立目标能只重编这一个文件,主工程不动。
# 编成静态库,避免每次调动画参数都全量重编主工程 add_library(sidewidget STATIC third_party/sidewidget/src/sidewidget.cpp ) # PUBLIC 让使用方能直接 #include "sidewidget.h",不用在业务代码里写长路径 target_include_directories(sidewidget PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/third_party/sidewidget/include ) # 组件依赖 Qt Widgets;QT_VERSION_MAJOR 保证 Qt5/Qt6 双栈都能编 target_link_libraries(sidewidget PUBLIC Qt${QT_VERSION_MAJOR}::Widgets ) # 组件内部若用了 std::clamp 一类工具,统一抬到 C++17,别让下游猜 target_compile_features(sidewidget PUBLIC cxx_std_17)STATIC与SHARED的取舍:Windows 下SHARED需要处理导出宏(__declspec(dllexport)),组件包一般没帮你写好,容易在链接期报一堆未解析符号;STATIC直接把目标文件链进去,省掉这层。缺点是主工程二进制变大,对界面组件来说可以忽略。
qmake 工程就把路径直接加进.pro,注意INCLUDEPATH +=后面要跟$$PWD,否则在 shadow build 目录下会解析成相对构建目录的路径:
# $$PWD 指向 .pro 所在目录,shadow build 下也不会失效 INCLUDEPATH += $$PWD/third_party/sidewidget/include HEADERS += $$PWD/third_party/sidewidget/include/sidewidget.h SOURCES += $$PWD/third_party/sidewidget/src/sidewidget.cpp # 只有包里带 .qrc 时才需要这一行,没有就删掉,否则 qmake 会报找不到文件 RESOURCES += $$PWD/third_party/sidewidget/res/sidewidget.qrc提示:先把 demo 单独编一遍再合并。demo 编不过说明你的 Qt 版本或编译器标准和包不匹配,这时候合并只会把问题带进主工程。
2.3 最小可运行示例:二十来行挂上去
验证阶段不要动主窗口,新建一个空工程跑通再说。下面这段是我常用的最小验证:
#include <QApplication> #include <QHBoxLayout> #include <QMainWindow> #include <QTextEdit> #include "sidewidget.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); QMainWindow win; auto *central = new QWidget(&win); auto *layout = new QHBoxLayout(central); // 关键:spacing 和 margins 必须清零,否则侧边栏和内容区之间永远有一条缝 layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); auto *side = new SideWidget(central); side->setOrientation(Qt::Horizontal); // 横向展开;贴顶的通知条传 Qt::Vertical side->setExpandedWidth(240); // 展开后的目标宽度,单位是逻辑像素 side->setCollapsedWidth(56); // 收起后只留图标条的宽度,填 0 就是完全隐藏 side->setAnimationDuration(220); // 单程时长,毫秒;展开收起各算一次 auto *content = new QTextEdit(central); layout->addWidget(side); layout->addWidget(content, 1); // 拉伸因子给 1,内容区吃掉剩余空间 // 展开时内容区只读,避免动画期间用户还在输入把光标位置搞乱 QObject::connect(side, &SideWidget::expandedChanged, content, [content](bool on) { content->setReadOnly(on); }); win.setCentralWidget(central); win.resize(1100, 700); win.show(); return app.exec(); }参数的取值逻辑:expandedWidth直接决定主窗口空间分配,240 是图标加两字标题的常见下限,再窄文字就要截断;collapsedWidth决定收起后还剩多少可点击区域,填 0 意味着只能靠悬浮或快捷键唤出,移动端或触屏场景不建议;animationDuration220ms 是我在 60Hz 屏上试出来的手感值,超过 300ms 用户会觉得“卡了一下”。
接口名以头文件为准。有些包对外叫setPanelWidth/setSideWidth,有的把方向和尺寸合并成一个setEdge(Qt::LeftEdge, 240)。别照着记忆敲,打开头文件对着改。
3. 渐入渐出的实现骨架:属性动画与透明度效果怎么配合
组件能不能改,取决于动画是“推着属性走”还是“直接改窗口尺寸”。前者可以随时打断、随时改曲线、随时接第二段动画;后者只能从头播到尾,中途点一下必然跳变。这一节拆开看它是怎么组织的。
3.1 双轨动画:宽度走 Q_PROPERTY,透明度走效果对象
QPropertyAnimation只能驱动声明了Q_PROPERTY的属性。直接调setFixedWidth没有属性变更通知,动画插不进去,所以你会在实现里看到类似这样的声明:
class SideWidget : public QWidget { Q_OBJECT // 宽度属性:动画每一帧都会调 setPanelWidth,内部同时改 min/max Q_PROPERTY(int panelWidth READ panelWidth WRITE setPanelWidth) // 透明度属性:单独拎出来,方便只淡入不位移的场景复用 Q_PROPERTY(qreal fade READ fade WRITE setFade) public: int panelWidth() const { return m_panelWidth; } qreal fade() const { return m_fade; } void setPanelWidth(int w); void setFade(qreal f); // ... };两条轨用QParallelAnimationGroup并起来,保证宽度和透明度同一时刻开始、同一时刻结束。分开start()的话,两个动画的启动时间会差一两帧,肉眼能看出“先变宽再变亮”的割裂感:
void SideWidget::setupAnimations() { m_widthAnim = new QPropertyAnimation(this, "panelWidth", this); m_widthAnim->setDuration(m_duration); // 220ms 左右,展开收起共用 m_widthAnim->setEasingCurve(QEasingCurve::OutCubic); // 先快后慢,收尾不突兀 m_effect = new QGraphicsOpacityEffect(this); m_opacityAnim = new QPropertyAnimation(m_effect, "opacity", this); m_opacityAnim->setDuration(m_duration); m_opacityAnim->setEasingCurve(QEasingCurve::OutCubic); setGraphicsEffect(m_effect); // 挂在 widget 上才生效 // 并行组保证两条轨同起同落,避免"先变宽再变亮"的割裂感 m_group = new QParallelAnimationGroup(this); m_group->addAnimation(m_widthAnim); m_group->addAnimation(m_opacityAnim); // 只在整组结束回调里改状态,中途不判状态,减少竞态 connect(m_group, &QParallelAnimationGroup::finished, this, &SideWidget::onAnimationFinished); }QGraphicsOpacityEffect有一个必须提前知道的边界:它把目标 widget 切到离屏渲染再合成,所以内部含原生窗口的子控件(QOpenGLWidget、QQuickWidget、视频渲染窗口)不会被淡入淡出,表现为“别的都淡了,就那块黑的”。遇到这种内容,我一般改成自己画:
void SideWidget::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); p.setOpacity(m_fade); // 0.0 全透明,1.0 全不透明 // 减 1 像素是给圆角描边留位置,避免右边缘被裁掉半个像素 p.setBrush(m_bgColor); p.setPen(Qt::NoPen); p.drawRoundedRect(rect().adjusted(0, 0, -1, -1), m_radius, m_radius); }代价要说清楚:setOpacity只影响这个 widget 自己的绘制,子控件照常以满不透明度画出来。所以“整块淡入”用效果对象,“背景板淡入、内容常显”用paintEvent,两者不是替代关系,是两种视觉需求。
3.2 时长、曲线与帧率的取值经验
这几个参数没有标准答案,但有一组不容易出错的范围。我把它整理成一张表,调参时直接对着改:
| 参数 | 建议值 | 调整理由 |
|---|---|---|
| 展开时长 | 200–260ms | 低于 180ms 像瞬移,高于 300ms 用户以为没响应 |
| 收起时长 | 160–220ms | 收起比展开快 20% 左右,手感更利落 |
| 缓动曲线 | OutCubic/OutQuart | 起步快、收尾缓,符合“东西滑出来”的直觉 |
| 透明度起点 | 0.0 或 0.2 | 完全从 0 开始在大面积面板上会闪,0.2 更稳 |
| 动画帧间隔 | 16ms(默认) | 手动设更小没意义,Qt 会按屏幕刷新率对齐 |
曲线选择上还有一个容易忽略的点:展开和收起最好别用同一条对称曲线。InOutCubic看着“优雅”,但起步那 30ms 几乎没位移,用户会觉得点了没反应。展开用OutCubic(一上来就有明显位移),收起用InCubic或OutCubic都行,别用InOut系。
帧率这块,QPropertyAnimation默认按 16ms 定时推进,实际帧率受主线程阻塞影响。如果你的主窗口里有同步文件 IO、数据库查询、或者QTableWidget一次塞几万行,动画会明显掉帧。这不是动画的问题,是主线程被占住了,解决方向是把手头那段耗时逻辑挪走或者分批,而不是去调setDuration。
3.3 状态机:为什么必须在动画中处理二次点击
新手最容易踩的一个坑是:动画还在跑的时候点第二下,侧边栏直接跳到底或者卡在中间。根因是没有状态机,靠一个bool m_expanded硬扛。我的做法是显式四态:
enum class State { Collapsed, Expanding, Expanded, Collapsing }; void SideWidget::setExpanded(bool on) { // 用"目标态"去重,不用动画状态去重:动画跑到一半再点同方向,直接忽略 const bool alreadyThere = (on && (m_state == State::Expanded || m_state == State::Expanding)) || (!on && (m_state == State::Collapsed || m_state == State::Collapsing)); if (alreadyThere) return; // 关键一步:先 stop,再读当前值。不 stop 就 setStartValue 会从旧终值起跳,肉眼看到闪一下 m_group->stop(); const int from = m_panelWidth; // 从"此刻真实宽度"接着走,不是从固定终值 const int to = on ? m_expandedWidth : m_collapsedWidth; m_widthAnim->setStartValue(from); m_widthAnim->setEndValue(to); m_opacityAnim->setStartValue(m_fade); m_opacityAnim->setEndValue(on ? 1.0 : 0.0); m_state = on ? State::Expanding : State::Collapsing; m_group->start(); emit expandedChanged(on); // 信号按目标态发,不等动画结束 }三个决定值得说明。第一,去重依据是“目标态”而不是“动画是否在跑”,否则快速连点会被if (running) return;全部吞掉,用户点五下只动一下。第二,stop()必须在读当前值之前,QPropertyAnimation::stop()会把属性留在当前帧的值上,这正是我们想要的起点。第三,expandedChanged在动画开始时发而不是结束时发,因为外部逻辑(比如内容区只读、图标切换)应该跟着意图走,不该等 220ms。
4. 交互细节落地:悬浮唤起、点击外部收起与内容区联动
动画跑通只是第一步,真正让侧边栏“好用”的是交互边界:什么时候自动展开、什么时候自动收起、收起时内容区怎么让位。这三件事做不好,动画再顺也会被用户骂。
4.1 事件过滤器:鼠标移入唤出、移出延迟收起
悬浮唤出不要直接连enterEvent,因为鼠标快速划过时enter和leave会连着触发,侧边栏会抽一下。我一般装一个事件过滤器,配一个单次定时器做延迟:
// 构造函数里:单次触发,延迟收起,鼠标回来就取消 m_collapseTimer = new QTimer(this); m_collapseTimer->setSingleShot(true); m_collapseTimer->setInterval(400); // 300–500ms,太短误触,太长像卡住 connect(m_collapseTimer, &QTimer::timeout, this, [this] { if (!m_pinned) setExpanded(false); // m_pinned 为 true 表示用户手动固定,不自动收 }); // 给需要触发的区域装过滤器(可以是自己,也可以是主窗口的中央 widget) targetWidget->installEventFilter(this); bool SideWidget::eventFilter(QObject *obj, QEvent *ev) { switch (ev->type()) { case QEvent::Enter: m_collapseTimer->stop(); // 鼠标回来了,撤销待执行的收起 if (m_state == State::Collapsed) setExpanded(true); break; case QEvent::Leave: m_collapseTimer->start(); // 只是"排队收起",不是立刻收 break; default: break; } return QWidget::eventFilter(obj, ev); }延迟 400ms 是一个折中值。低于 250ms,用户从侧边栏移到内容区时鼠标路过边界,侧边栏就开始收;高于 600ms,用户已经点完了它才收,显得迟钝。真要做边缘唤醒(鼠标贴到窗口最左侧 4–8px 时唤出),就在主窗口上装过滤器监听QEvent::MouseMove判断event->pos().x() < 8,但要注意别在有画布、有拖拽选区的界面里抢鼠标,那种场景应该做成快捷键或者顶部按钮触发。
4.2 主窗口布局重排:为什么不能直接setFixedWidth
setMinimumWidth和setMaximumWidth会各自触发一次布局失效,动画每帧调两回,复杂布局(嵌套QTabWidget、QScrollArea、多层QSplitter)下就会出现明显掉帧。而且setFixedWidth把 min 和 max 同时设死,父布局的sizeHint缓存不会自动刷新,表现为“动画走完了但内容区没让位”。
正确做法是把宽度收敛到一个 setter 里,成对修改并主动通知:
void SideWidget::setPanelWidth(int w) { if (w == m_panelWidth) return; // 每帧都会被调,值没变就直接返回,省一次布局失效 m_panelWidth = w; setMinimumWidth(w); setMaximumWidth(w); updateGeometry(); // 通知父布局重新算 sizeHint,内容区才会跟着让位 update(); // 触发重绘,圆角和背景随宽度变化 }内容区如果在动画期间要重排大量子控件(QTableView的 viewport、QTextEdit的文档视图),建议在m_group->start()之前setUpdatesEnabled(false),在finished回调里再打开。这一步能把动画期间的绘制次数砍掉一半,视觉上反而更顺,因为中间帧你本来也看不清。
另外一个选型判断:如果产品要求“用户可以拖拽调宽度”,别在 SideWidget 上自己写鼠标拖拽。用QSplitter托管尺寸,再对splitter->setSizes()做动画,省掉一大堆边界处理。
4.3 高 DPI、缩放与主题切换下的适配
高 DPI 是这类组件最容易翻车的地方,因为它同时影响三件事:逻辑像素换算、绘制取整、图片资源加载。
int main(int argc, char *argv[]) { // Qt5 必须写在 QApplication 构造之前;Qt6 默认开启,写了也无害 QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QApplication app(argc, argv); // ... }缩放比例 1.25、1.5 这类非整数倍下,动画宽度如果是int,qRound每帧的结果会在两个值之间来回跳,表现为侧边栏边缘有 1px 的抖动或者一条细缝。根治办法是内部用qreal存宽度,只在真正落到setMinimumWidth时才取整;绘制圆角矩形时用QRectF并做半像素对齐(adjusted(0.5, 0.5, -0.5, -0.5)),边框就不会糊。
主题切换是另一个隐蔽点。如果 QSS 里给侧边栏写了min-width/max-width,样式重新 polish 的时候会把动画刚设好的值覆盖回去,现象是“切了深色主题,侧边栏自己弹回展开宽度”。规避方式有两种:QSS 里不写宽度,只写颜色和内边距;或者在changeEvent里接住QEvent::StyleChange后,把当前宽度重新写一遍:
void SideWidget::changeEvent(QEvent *e) { if (e->type() == QEvent::StyleChange) { // QSS 的 min-width/max-width 会覆盖动画结果,样式变更后强制回写一次 const int keep = m_panelWidth; m_panelWidth = -1; // 骗过 setter 里的相等判断 setPanelWidth(keep); } QWidget::changeEvent(e); }5. 避坑与排查:五个真机上翻过车的现场
调通 demo 和在真实项目里跑稳之间,隔着大概五个坑。下面这几条都是我或者同事实际遇到过的,按“现象 → 原因 → 解决”记下来,遇到类似的直接对号入座。
5.1 展开第一帧闪一下黑底或白底
现象:点击展开按钮,侧边栏先整块变黑(浅色主题下变白),然后才正常显示内容。
原因:QGraphicsOpacityEffect会让 widget 走离屏渲染,第一帧合成时如果 widget 自身没有填充背景,就会透出父窗口的清理色。子控件多的时候尤其明显,因为离屏 pixmap 初始是未定义内容。
解决:给侧边栏显式开背景填充,并在 QSS 里写死颜色,别依赖继承:setAutoFillBackground(true),同时QPalette::Window设成和主题一致的色值。另外在m_group->start()之前给内容区setUpdatesEnabled(false),finished里恢复,能顺带减少首帧的绘制压力。
5.2 收起之后原位留一条两三个像素的白边
现象:动画播完了,侧边栏确实收起来了,但原来位置还留着一条细白缝,鼠标移上去还能触发悬浮展开。
原因:三种可能,按概率排——布局的setSpacing没清零;contentsMargins留了默认值;或者setMinimumWidth改了但setMaximumWidth忘改(或者反过来),min > max 时 Qt 会取 min,导致宽度收不到位。
解决:先把layout->setSpacing(0)和setContentsMargins(0,0,0,0)补上;再检查宽度 setter 里 min/max 是不是成对修改;最后确认sizePolicy没被外部改成Fixed。三步排查完基本都能定位。
5.3 快速连点展开按钮,侧边栏卡在半开状态
现象:连点五下,侧边栏停在 120px 左右不动了,再点一下才继续。
原因:动画没停就重新start(),setStartValue用的是上一次的终值而不是当前实际宽度,属性在旧终值和当前值之间来回拉扯;或者代码里写了if (m_group->state() == QAbstractAnimation::Running) return;,把后续点击全吞了。
解决:按 3.3 的写法,先m_group->stop(),再用m_panelWidth作为起点;去重条件改成判断目标态,不判断动画运行状态。改完之后连点十下也不会卡住,只是最后一下的方向生效。
5.4 高 DPI 屏上动画边缘抖动或出现细条纹
现象:在 125% 或 150% 缩放的屏幕上,宽度动画过程中侧边栏边缘一直抖,停下来之后偶尔残留一条 1px 的浅色竖线。
原因:宽度属性是int,1.25 倍缩放下每帧的取整结果在两个值之间跳;圆角矩形的绘制矩形没有做半像素对齐,抗锯齿把边缘摊到了两个像素上。
解决:内部存储改成qreal,只在调setMinimumWidth/setMaximumWidth的时候qRound;paintEvent里用QRectF并adjusted(0.5, 0.5, -0.5, -0.5);setPen(Qt::NoPen)而不是画一个透明的笔。
5.5 接进主工程后动画完全失效,或者宽度被弹回原样
现象:demo 里好好的,接进项目之后点按钮没反应,或者刚展开就被打回收起状态,日志里看不到任何报错。
原因:最常见的是 QSS 覆盖——样式表里给这个控件写了min-width/max-width/width,样式 polish 会把动画设的值冲掉;其次是把它塞进了QSplitter,QSplitter自己托管子控件的尺寸,会无视你的 min/max 设置;第三种是父布局用了QStackedLayout加固定尺寸,父级约束优先级更高。
解决:先全局搜 QSS 里的sidewidget和min-width,把宽度相关声明删掉;确认父级不是QSplitter,如果是,改成对setSizes()做QPropertyAnimation;最后用qDebug() << sizePolicy()打印一下,看看有没有被外部改过。
注意:这五条里有三条跟动画逻辑本身无关,而是样式和布局在打架。排错顺序建议固定为「先看 QSS,再看父布局,最后才怀疑动画代码」,反过来的话会在动画参数上白折腾半天。
6. 收口:抽成动画基类,再走一遍上线前自检
一个项目里通常不止一个侧边栏,还有底部抽屉、顶部通知条、右侧属性面板,动画逻辑几乎一模一样。我的做法是把slide、duration、easing提到一个AnimatedPanel基类,子类只实现“进度值怎么映射成几何”。
// animatedpanel.h —— 只放会动的东西,不碰业务内容 class AnimatedPanel : public QWidget { Q_OBJECT Q_PROPERTY(qreal slide READ slide WRITE setSlide) public: void setDuration(int ms); // 统一节奏,别让每个面板各走各的 void setEasing(const QEasingCurve &c); void animateTo(qreal target); // 0.0 全收起,1.0 全展开 protected: virtual void applySlide(qreal t) = 0; // 子类决定 t 映射成宽度还是垂直位移 qreal m_slide = 0.0; };抽完之后节奏参数改一次全项目生效,再也不会出现“侧边栏 220ms、抽屉 400ms”的参差感。上线前我会固定走一遍这张表,缺一项都不发版:
| 检查项 | 判定标准 |
|---|---|
| 连点五次 | 停在两端之一,无半开状态 |
| 100% / 125% / 200% 缩放 | 边缘无 1px 缝隙、无抖动 |
| 深色浅色来回切三次 | 宽度不被 QSS 打回,颜色跟随 |
| 收起后 | 原位置无空白、无残留点击热区 |
| 内含 OpenGL/视频控件 | 淡入淡出不出现黑块,必要时改自绘 |
最后一条教训是血泪换来的:早期我图省事,动画期间没关内容区刷新,QTableView一万行数据每帧重排一次 viewport,动画看着像幻灯片。从那以后我每次接面板类动画,都会先setUpdatesEnabled(false),finished里再恢复,这个习惯一直留到现在。希望帮到你。
本文还有配套的精品资源,点击获取