WPF 使用 RenderTransform 实现高性能平滑滚动的 ScrollViewer
<p>在之前的两篇文章中,我们探讨了 WPF 中实现平滑滚动的不同方案:</p><ol>
<li>WPF 如何流畅地滚动ScrollViewer 简单实现下:基于 <code>DoubleAnimation</code> 的动画方案。</li>
<li>WPF 使用CompositionTarget.Rendering实现平滑流畅滚动的ScrollViewer:基于 <code>CompositionTarget.Rendering</code> 的每帧布局更新方案。</li>
</ol>
<p>虽然第二版方案解决了触控板和物理惯性的问题,但它引入了一个新的性能瓶颈:每帧调用 <code>ScrollToVerticalOffset</code>。这会导致 WPF 在每一帧都进行布局计算,在高负载场景下会直接卡死整个UI线程,造成掉帧或其他UI组件无响应。</p>
<p>为了解决这个问题,我进行了第三版(v3)设计,核心思路是:视觉层与逻辑层分离。</p>
<h2 id="三种方案对比">三种方案对比</h2>
<table>
<thead>
<tr><th align="left">方案</th><th align="left">实现方式</th><th align="left">优点</th><th align="left">缺点</th></tr>
</thead>
<tbody>
<tr>
<td align="left">v1 (动画版)</td>
<td align="left"><code>DoubleAnimation</code> 驱动 <code>VerticalOffset</code></td>
<td align="left">实现简单,代码量少</td>
<td align="left">无法保留惯性速度(动画打断);触控板体验差;不支持触摸/笔。</td>
</tr>
<tr>
<td align="left">v2 (布局驱动)</td>
<td align="left"><code>Rendering</code> 事件每帧调用 <code>ScrollToVerticalOffset</code></td>
<td align="left">物理模型更真实;支持多种输入设备</td>
<td align="left">性能差:每帧触发 Layout 计算,高负载下掉帧严重。</td>
</tr>
<tr>
<td align="left">v3 (视觉分离)</td>
<td align="left"><code>RenderTransform</code> 驱动视觉,低频同步逻辑位置</td>
<td align="left">高性能:视觉满帧运行,逻辑低频更新;物理模型完善。</td>
<td align="left">实现相对复杂,需要处理坐标系转换和帧同步。</td>
</tr>
</tbody>
</table>
<hr>
<p>现在来看看v3的效果(gif帧率好低):</p>
<p><img src="https://img2024.cnblogs.com/blog/1188749/202512/1188749-20251222170546134-1859722965.gif"></p>
<p>接下来,我们详细介绍 v3 版本的设计与实现原理。</p>
<h2 id="一视觉与逻辑分离">一、视觉与逻辑分离</h2>
<p>v3 的核心在于将“用户看到的滚动”(视觉层)和“控件实际的滚动”(逻辑层)分离。</p>
<ol>
<li>
<p>视觉层:</p>
<ul>
<li>使用 <code>TranslateTransform</code> 对 <code>Content</code> 进行位移。</li>
<li>在 <code>CompositionTarget.Rendering</code> 中以屏幕刷新率(如 60Hz 或 144Hz)更新 <code>Transform.Y</code>。</li>
<li>因为 <code>RenderTransform</code> 只影响渲染而不触发布局(Measure/Arrange),所以性能极高,完全由 GPU 加速。</li>
</ul>
</li>
<li>
<p>逻辑层:</p>
<ul>
<li>维护实际的 <code>ScrollViewer.VerticalOffset</code>。</li>
<li>降频更新:不再每帧调用 <code>ScrollToVerticalOffset</code>,而是以较低的频率(如 24Hz)同步逻辑位置。</li>
<li>这保证了滚动条的位置更新和虚拟化加载新内容,同时避免了频繁的布局计算。</li>
</ul>
</li>
</ol>
<h3 id="渲染循环逻辑">渲染循环逻辑</h3>
<p>只需遵守一条坐标系变换规则:逻辑位置 = 视觉位置 + 视觉偏差。<br>当用户滚动时,视觉层将以“插帧”方式在逻辑层低帧率更新之间平滑过渡。</p>
<p>在每一帧的渲染回调中:</p>
<ol>
<li>计算物理模型的当前位置 <code>_currentVisualOffset</code>。</li>
<li>计算视觉偏差 <code>_visualDelta = _currentVisualOffset - _logicalOffset</code>。</li>
<li>应用 <code>_transform.Y = -_visualDelta</code>,实现视觉上的平滑移动,不会触发布局重置。</li>
<li>累加时间,如果超过 <code>ScrollBarUpdateInterval</code> (1/24s),则调用 <code>ScrollToVerticalOffset</code> 同步逻辑位置,触发布局更新,从而允许滚动条同步和虚拟化等功能生效。</li>
</ol>
<h2 id="二物理模型设计">二、物理模型设计</h2>
<p>v3 版本的物理模型沿用了 v2 的设计,但做了一些改进以提升滚动体验。以下介绍完整的物理模型。</p>
<h3 id="21-缓动模型">2.1 缓动模型</h3>
<p>适用于鼠标滚轮。该模型包含两个核心部分:动态速度因子(决定滚多快)和物理衰减(决定滚多久)。</p>
<h4 id="动态速度因子-dynamic-velocity-factor">动态速度因子 (Dynamic Velocity Factor)</h4>
<p>在 v2 版本中,我们发现简单的线性速度叠加无法平衡缓慢滚动和快速滚动的体验。因此,v3 引入了一个基于时间间隔的动态速度因子。当用户快速连续滚动时,速度因子会呈指数级增长,从而产生更大的加速度。</p>
<p><span class="katex"><span class="katex-mathml">v<sub>f</sub>=(Vmax−Vmin)⋅e^(−Δt/20)+Vmin<span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mrel"><span class="mspace"><span class="base"><span class="strut"><span class="mopen"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mbin"><span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mclose"><span class="mspace"><span class="mbin"><span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight"><span class="mord mtight"><span class="sizing reset-size3 nulldelimiter size6 mopen"><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size3 size1"><span class="mord mtight"><span class="mord mtight"><span class="pstrut"><span class="mtight frac-line"><span class="pstrut"><span class="mtight sizing reset-size3 size1"><span class="mord mtight"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="sizing reset-size3 nulldelimiter size6 mclose"><span class="mspace"><span class="mbin"><span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="vlist-s"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></p>
<ul>
<li><span class="katex"><span class="katex-mathml">Vmax<span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist">:最大速度倍率,固定为 2.5。</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></li>
<li><span class="katex"><span class="katex-mathml">Vmin<span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist">:最小速度倍率 (<code>MinVelocityFactor</code>),默认为 1.2。</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></li>
<li><span class="katex"><span class="katex-mathml">Δt<span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal">:两次滚动事件的时间间隔 (ms)。</span></span></span></span></span></span></span></li>
</ul>
<p>这意味着:如果你慢慢滚动,每次滚动的距离约为原始值的 1.2 倍;如果你疯狂拨动滚轮,这个倍率会迅速逼近 2.5 倍,与真实的物理滚动手感更接近。</p>
<h4 id="物理衰减">物理衰减</h4>
<p>模拟物理摩擦力,使滚动速度随时间自然衰减。</p>
<ul>
<li>速度衰减:<span class="katex"><span class="katex-mathml">v<sub>new</sub>=v<sub>old</sub>⋅f^t<sub>f</sub><span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mrel"><span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mbin"><span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size3 size1"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></li>
<li>位置更新:<span class="katex"><span class="katex-mathml">x<sub>new</sub>=x<sub>old</sub>+v<sub>new</sub>⋅(t<sub>f</sub>/24)<span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mrel"><span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mbin"><span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mbin"><span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mopen nulldelimiter"><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight"><span class="pstrut"><span class="frac-line"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size3 size1"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="vlist-s"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></li>
</ul>
<p>其中:</p>
<ul>
<li><span class="katex"><span class="katex-mathml">f<span class="katex-html"><span class="base"><span class="strut"><span class="mord mathnormal">:速率衰减系数,默认为 0.92。数值越小,停得越快。</span></span></span></span></span></span></li>
<li><span class="katex"><span class="katex-mathml">t<sub>f</sub><span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist">:时间标准化因子,<span class="katex"><span class="katex-mathml">dt/TargetFrameTime<span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mopen nulldelimiter"><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="pstrut"><span class="frac-line"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mclose nulldelimiter"> (基准帧率为 144Hz),dt为绘制两帧之间的间隔时间。</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></li>
<li>常数 24 是一个经验值,用于调整速度到位移的映射比例。</li>
</ul>
<h3 id="22-精确模型">2.2 精确模型</h3>
<p>适用于触控板。触控板本身提供了高精度的 <code>Delta</code> 值,我们不需要模拟惯性(系统已处理),只需要平滑地过渡到目标位置,避免画面撕裂或抖动。</p>
<ul>
<li>插值计算:<span class="base"><span class="mord"><span class="mord mathnormal">x<span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight"><sub>n</sub><span class="mord mathnormal mtight"><sub>e</sub><span class="mord mathnormal mtight"><sub>w</sub><span class="vlist-s"><sub></sub><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mrel">=<span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal">x<span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight"><sub>o</sub><span class="mord mathnormal mtight"><sub>l</sub><span class="mord mathnormal mtight"><sub>d</sub><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mbin">+<span class="mspace"><span class="base"><span class="strut"><span class="mopen">(<span class="mord"><span class="mord mathnormal">x<span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight"><sub>t</sub><span class="mord mathnormal mtight"><sub>a</sub><span class="mord mathnormal mtight"><sub>r</sub><span class="mord mathnormal mtight"><sub>g</sub><span class="mord mathnormal mtight"><sub>e</sub><span class="mord mathnormal mtight"><sub>t</sub><span class="vlist-s"><sub></sub><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mbin">−<span class="mspace"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal">x<span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mathnormal mtight"><sub>o</sub><span class="mord mathnormal mtight"><sub>l</sub><span class="mord mathnormal mtight"><sub>d</sub><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mclose">)<span class="mspace"><span class="mbin">⋅<span class="mspace"><span class="base"><span class="strut"><span class="mopen">(<span class="mord">1<span class="mspace"><span class="mbin">−<span class="mspace"><span class="base"><span class="strut"><span class="mopen">(<span class="mord">1<span class="mspace"><span class="mbin">−<span class="mspace"><span class="base"><span class="strut"><span class="mord mathnormal">l<span class="mclose"><span class="mclose">)^<span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight"><span class="mord mathnormal mtight">t<sub>f</sub><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="sizing reset-size3 size1 mtight"><span class="mord mtight"><span class="mord mathnormal mtight"><span class="mord mathnormal mtight"><span class="mord mathnormal mtight"><span class="mord mathnormal mtight"><span class="mord mathnormal mtight"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mclose">)</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span><span class="katex"><span class="katex-mathml"><br></span></span></li>
</ul>
<p>其中:</p>
<ul>
<li><span class="katex"><span class="katex-mathml"><span class="katex-html"><span class="base"><span class="strut"><span class="mord mathnormal">l:插值系数 (<code>LerpFactor</code>),默认为 0.5。数值越大,跟随越紧密。</span></span></span></span></span></span></li>
<li><span class="katex"><span class="katex-mathml"><span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist">t<sub>f</sub>:时间标准化因子。<span class="katex"><span class="katex-mathml"><br></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></li>
</ul>
<h2 id="三快速开始">三、快速开始</h2>
<p>在项目中引入FluentWpfCore包,然后使用:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">Window </span><span style="color: rgba(255, 0, 0, 1)">xmlns:fluent</span><span style="color: rgba(0, 0, 255, 1)">="clr-namespace:FluentWpf.Controls;assembly=FluentWpfCore"</span><span style="color: rgba(255, 0, 0, 1)">
...</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">fluent:SmoothScrollViewer</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 128, 0, 1)"><!--</span><span style="color: rgba(0, 128, 0, 1)">可选 自定义模型及其参数</span><span style="color: rgba(0, 128, 0, 1)">--></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">fluent:SmoothScrollViewer.Physics</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">fluent:DefaultScrollPhysics </span><span style="color: rgba(255, 0, 0, 1)">MinVelocityFactor</span><span style="color: rgba(0, 0, 255, 1)">="1.2"</span> <span style="color: rgba(0, 0, 255, 1)">/></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">fluent:SmoothScrollViewer.Physics</span><span style="color: rgba(0, 0, 255, 1)">></span><span style="color: rgba(0, 0, 0, 1)">
...
</span><span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">fluent:SmoothScrollViewer</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">Window</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<table>
<thead>
<tr><th>属性</th><th>类型</th><th>默认值</th><th>说明</th></tr>
</thead>
<tbody>
<tr>
<td><code>IsEnableSmoothScrolling</code></td>
<td><code>bool</code></td>
<td><code>true</code></td>
<td>启用或禁用平滑滚动动画(实际上会控制所有SmoothScrolling相关功能)</td>
</tr>
<tr>
<td><code>PreferredScrollOrientation</code></td>
<td><code>Orientation</code></td>
<td><code>Vertical</code></td>
<td>首选滚动方向:<code>Vertical</code> 或 <code>Horizontal</code></td>
</tr>
<tr>
<td><code>AllowTogglePreferredScrollOrientationByShiftKey</code></td>
<td><code>bool</code></td>
<td><code>true</code></td>
<td>允许通过按住 Shift 键切换滚动方向</td>
</tr>
<tr>
<td><code>Physics</code></td>
<td><code>IScrollPhysics</code></td>
<td><code>DefaultScrollPhysics</code></td>
<td>控制滚动动画行为的物理模型</td>
</tr>
</tbody>
</table>
<p>默认模型(<code>DefaultScrollPhysics</code>)可选参数:</p>
<table>
<thead>
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
</thead>
<tbody>
<tr>
<td><code>MinVelocityFactor</code></td>
<td><code>double</code></td>
<td><code>1.2</code></td>
<td>鼠标滚轮的最小速度倍率</td>
</tr>
<tr>
<td><code>Friction</code></td>
<td><code>double</code></td>
<td><code>0.92</code></td>
<td>鼠标滚轮的速度衰减系数</td>
</tr>
<tr>
<td><code>LerpFactor</code></td>
<td><code>double</code></td>
<td><code>0.5</code></td>
<td>触控板滚动的插值系数</td>
</tr>
</tbody>
</table>
<h2 id="四了解更多">四、了解更多</h2>
<h3 id="1-为什么要视觉满帧逻辑低频">1) 为什么要“视觉满帧、逻辑低频”</h3>
<p>在 WPF 里,<code>ScrollToVerticalOffset/ScrollToHorizontalOffset</code> 不是一个“只改数值”的轻量操作。它往往会驱动:</p>
<ul>
<li>滚动条位置与 <code>ScrollChanged</code> 事件</li>
<li>布局与渲染链路(尤其是内容复杂时)</li>
<li>虚拟化容器的生成/回收(例如 <code>VirtualizingStackPanel</code>)</li>
</ul>
<p>v2的实现把它放到 <code>CompositionTarget.Rendering</code> 的每一帧里调用,意味着UI线程必须在每帧都完成布局计算,这在内容复杂或CPU负载高时会直接卡死UI线程,导致掉帧或其他UI组件无响应。</p>
<p>v3 的分层策略是:</p>
<ul>
<li>视觉层:用 <code>TranslateTransform</code> 做位移补偿,只影响渲染,不触发布局。</li>
<li>逻辑层:用 <code>ScrollTo*Offset</code> 推进真实偏移,但频率降低到 24Hz。</li>
</ul>
<p>相当于把“高频的连续运动”交给 GPU(RenderTransform),把“低频但必要的状态推进”交给布局系统(ScrollTo)。或者理解为:视觉层做“动画”,逻辑层做“状态更新”,以低帧率推进布局计算,然后由视觉层平滑过渡,显著提升性能。</p>
<h3 id="2-关键状态视觉差值visual-delta">2) 关键状态:视觉差值(Visual Delta)</h3>
<p>在 v3 里,始终存在两个 offset:</p>
<ul>
<li>逻辑 offset:<code>ScrollViewer</code> 真正的 <code>VerticalOffset/HorizontalOffset</code>,决定滚动条与虚拟化。</li>
<li>视觉 offset:物理模型在每帧计算出的“应该看到的位置”。</li>
</ul>
<p>两者的差值就是视觉补偿量:</p>
<p><span class="katex"><span class="katex-mathml">Δ= visual offset − logical offset<span class="katex-html"><span class="base"><span class="strut"><span class="mord"><span class="mspace"><span class="mrel"><span class="mspace"><span class="base"><span class="strut"><span class="mord mathnormal"><span class="mord mathnormal"><span class="mord mathnormal"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"><span class="vlist-r"><span class="vlist"><span class="mspace"><span class="mbin"><span class="mspace"><span class="base"><span class="strut"><span class="mord mathnormal"><span class="mord mathnormal"><span class="mord mathnormal"><span class="mord"><span class="mord mathnormal"><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist"><span class="pstrut"><span class="mtight sizing reset-size6 size3"><span class="mord mtight"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="mord mtight mathnormal"><span class="vlist-s"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></p>
<p>视觉层每帧做的事情非常纯粹:把这个差值通过 Transform 反向抵消掉,让用户“看到”的内容位置跟随视觉 offset。</p>
<ul>
<li>垂直滚动:<code>Transform.Y = -Δ</code></li>
<li>水平滚动:<code>Transform.X = -Δ</code></li>
</ul>
<p>这样带来两个好处:</p>
<ol>
<li>逻辑层什么时候同步(调用 <code>ScrollTo*Offset</code>)可以自由选择频率,不会影响视觉连续性。</li>
<li>当逻辑层因为外部原因突变(拖动滚动条、代码调用 <code>ScrollTo...</code>、键盘导航)时,只要立刻重算差值,视觉层就仍然能保持连续。</li>
</ol>
<h3 id="3-为什么要用真实-dt而不是假设固定帧率">3) 为什么要用真实 <code>dt</code>(而不是假设固定帧率)</h3>
<p><code>CompositionTarget.Rendering</code> 的触发并不严格等间隔:后台负载、窗口被遮挡、显示器刷新率、系统节能策略都会让帧间隔波动。</p>
<p>因此实现中用 <code>Stopwatch.GetTimestamp()</code> 计算真实 <code>dt</code>,并把 <code>dt</code> 传给物理模型。这意味着:</p>
<ul>
<li>低帧率时不会“走慢动作”或突然“加速冲刺”</li>
<li>高刷屏(120/144Hz)不会因为更高帧数而滚得更远</li>
</ul>
<p>配合 <code>DefaultScrollPhysics</code> 中的 <code>timeFactor = dt / TargetFrameTime</code>,滚动手感可以在不同帧率下保持一致。</p>
<h3 id="4-关于逻辑同步频率">4) 关于逻辑同步频率</h3>
<p>实现把逻辑同步频率设为 24Hz(<code>ScrollBarUpdateInterval = 1/24s</code>),这是一个折中:</p>
<ul>
<li>频率更高:滚动条更“实时”,虚拟化更及时,但布局压力上升。</li>
<li>频率更低:性能更好,但滚动条会有视觉滞后,虚拟化加载可能会出现空白频闪。</li>
</ul>
<blockquote>
<p>一个可能的缓解思路是让虚拟化容器提前加载,会增加一点内存开销,但能减少空白频闪。</p>
</blockquote>
<p>一般来说:</p>
<ul>
<li>内容很重(大量图片、复杂控件、阴影/模糊多):可以把同步频率调低一点。</li>
<li>列表虚拟化强依赖“及时生成下一屏”(例如聊天列表/文件列表):可以适当提高,但要观察 CPU。</li>
</ul>
<h3 id="5-注意-scrollchanged-状态变更">5) 注意 ScrollChanged 状态变更</h3>
<p>只靠渲染循环还不够,因为用户可以通过滚动条拖动来改变 offset。实现里在 <code>OnScrollChanged</code> 中做了两件重要的事情:</p>
<ol>
<li>更新逻辑 offset(垂直/水平各自维护)。</li>
<li>如果当前正在平滑滚动,并且变化来自“当前激活方向”,就立刻更新 Transform,让画面位置保持连续。</li>
</ol>
<h3 id="6-横向滚动与-shift-切换">6) 横向滚动与 Shift 切换</h3>
<p>v3 支持横向与纵向两种滚动方向,并且提供了按住 Shift 切换滚动方向的特性。</p>
<h3 id="7-为什么滚动时要临时关闭-hittest">7) 为什么滚动时要临时关闭 HitTest</h3>
<p>渲染循环开始时把 <code>IsHitTestVisible</code> 设为 <code>false</code>,结束时恢复。</p>
<p>高速滚动时,鼠标在大量元素上扫过会触发频繁的命中测试和状态变更(Hover、ToolTip、触发器)。 关闭命中测试能够显著降低这些开销,提升滚动性能。</p>
<p>当然,它也意味着滚动过程中无法点击内容。</p>
<h3 id="8-如何写你自己的物理模型">8) 如何写你自己的物理模型</h3>
<p><code>IScrollPhysics</code> 的接口设计简单:</p>
<ul>
<li><code>OnScroll(...)</code>:只负责接收一次输入意图(delta + 是否精确 + 边界 + 时间间隔)。</li>
<li><code>Update(...)</code>:每帧推进到新位置(帧率无关)。</li>
<li><code>IsStable</code>:告诉外部何时可以退出渲染循环。</li>
</ul>
<h2 id="写在最后">写在最后</h2>
<div class="gc-titlebar">
<div class="gc-titlebar-left">
<div class="gc-owner">
<div id="GC8c94u6-avatar" class="gc-avatar">TwilightLemon/FluentWpfCore: A WPF library providing core Fluent Design controls, materials, and visual effects.</div>
</div>
</div>
</div>
<p>相关组件均开源在 FluentWpfCore 仓库,欢迎 star 和 PR!仓库保持活跃更新。</p>
<p>感谢阅读,文章如有不妥之处,请各位大佬不吝指正!</p>
<p> </p>
<p><img alt="" class="lazyloaded lazyload" loading="lazy" src="https://img2024.cnblogs.com/blog/1188749/202407/1188749-20240702112134529-1920703459.png"></p>
<p> 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名TwilightLemon,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。</p><br><br>
来源:https://www.cnblogs.com/TwilightLemon/p/19383555
頁:
[1]