[{"data":1,"prerenderedAt":4098},["ShallowReactive",2],{"page-\u002Fcsharp\u002Fwpf\u002Fscrollviewer实现鼠标的平滑滚动":3},{"id":4,"title":5,"body":6,"description":237,"extension":4092,"meta":4093,"navigation":1934,"path":4094,"seo":4095,"stem":4096,"__hash__":4097},"content\u002Fcsharp\u002FWPF\u002FScrollViewer实现鼠标的平滑滚动.md","WPF ScrollViewer 实现鼠标的平滑滚动",{"type":7,"value":8,"toc":4054},"minimark",[9,13,29,32,37,41,142,146,151,220,224,302,306,309,405,408,494,497,501,526,530,553,556,560,568,571,577,580,612,614,618,621,628,632,635,638,642,649,652,655,669,694,697,700,703,706,709,716,719,722,725,728,731,734,737,743,745,749,753,786,820,824,844,864,870,881,885,888,976,1002,1005,1008,1019,1203,1213,1217,1221,1239,1243,1300,1303,1405,1430,1433,1436,1494,1499,1518,1521,1581,1585,1602,1605,1611,1615,1707,1710,1715,1721,1724,1729,1735,1738,1741,1847,1880,1883,1887,1974,1977,2044,2055,2059,2062,2433,2436,2495,2497,2501,2505,2511,2520,2526,2535,2542,2546,2566,2587,2591,2594,2607,2610,2621,2623,2627,2631,2642,2646,2698,2702,2716,2718,2722,2726,2729,2749,2753,2756,2834,2836,2840,2846,3966,3968,3971,3974,4010,4017,4019,4023,4048,4050],[10,11,5],"h1",{"id":12},"wpf-scrollviewer-实现鼠标的平滑滚动",[14,15,16],"blockquote",{},[17,18,19,23,24,28],"p",{},[20,21,22],"span",{},"!abstract"," 概述\n本文档详细讲解如何使用",[25,26,27],"strong",{},"二阶缓动物理系统","实现一个带有惯性滚动的自定义 ScrollViewer。相比 WPF 默认的 ScrollViewer（基于物理像素，无惯性），这个实现提供了流畅的滚动体验，类似于现代移动应用和 Web 浏览器的滚动效果。",[30,31],"hr",{},[33,34,36],"h2",{"id":35},"wpf-原生-scrollviewer-vs-自定义惯性滚动-scrollviewer","📊 WPF 原生 ScrollViewer vs 自定义惯性滚动 ScrollViewer",[38,39,40],"h3",{"id":40},"对比总览",[42,43,44,60],"table",{},[45,46,47],"thead",{},[48,49,50,54,57],"tr",{},[51,52,53],"th",{},"特性",[51,55,56],{},"WPF 原生 ScrollViewer",[51,58,59],{},"自定义惯性滚动 ScrollViewer",[61,62,63,77,90,103,116,129],"tbody",{},[48,64,65,71,74],{},[66,67,68],"td",{},[25,69,70],{},"滚动方式",[66,72,73],{},"基于物理像素，直接跳转",[66,75,76],{},"基于物理模拟，平滑过渡",[48,78,79,84,87],{},[66,80,81],{},[25,82,83],{},"惯性滚动",[66,85,86],{},"❌ 无惯性，滚动立即停止",[66,88,89],{},"✅ 有惯性，滚动逐渐减速",[48,91,92,97,100],{},[66,93,94],{},[25,95,96],{},"用户体验",[66,98,99],{},"传统桌面应用风格",[66,101,102],{},"现代移动应用风格",[48,104,105,110,113],{},[66,106,107],{},[25,108,109],{},"性能开销",[66,111,112],{},"低（原生实现）",[66,114,115],{},"中等（需要物理计算）",[48,117,118,123,126],{},[66,119,120],{},[25,121,122],{},"可定制性",[66,124,125],{},"有限",[66,127,128],{},"高（可调整物理参数）",[48,130,131,136,139],{},[66,132,133],{},[25,134,135],{},"边界处理",[66,137,138],{},"硬停止",[66,140,141],{},"弹性反弹（可选）",[38,143,145],{"id":144},"wpf-原生-scrollviewer-的优缺点","WPF 原生 ScrollViewer 的优缺点",[147,148,150],"h4",{"id":149},"优点","✅ 优点",[152,153,154,172,188,204],"ol",{},[155,156,157,160],"li",{},[25,158,159],{},"性能优异",[161,162,163,166,169],"ul",{},[155,164,165],{},"原生实现，性能开销极低",[155,167,168],{},"无需额外的物理计算",[155,170,171],{},"内存占用小",[155,173,174,177],{},[25,175,176],{},"简单直接",[161,178,179,182,185],{},[155,180,181],{},"滚动行为可预测",[155,183,184],{},"滚动距离精确（1:1 对应像素）",[155,186,187],{},"适合需要精确控制的场景",[155,189,190,193],{},[25,191,192],{},"稳定性高",[161,194,195,198,201],{},[155,196,197],{},"经过长期测试和优化",[155,199,200],{},"兼容性好，无意外行为",[155,202,203],{},"适合企业级应用",[155,205,206,209],{},[25,207,208],{},"资源占用低",[161,210,211,214,217],{},[155,212,213],{},"不需要额外的动画循环",[155,215,216],{},"不需要高精度时间测量",[155,218,219],{},"适合资源受限的环境",[147,221,223],{"id":222},"缺点","❌ 缺点",[152,225,226,254,270,286],{},[155,227,228,231],{},[25,229,230],{},"无惯性滚动",[232,233,238],"pre",{"className":234,"code":235,"language":236,"meta":237,"style":237},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F 原生 ScrollViewer：滚轮滚动后立即停止\n\u002F\u002F 用户需要多次滚动才能浏览长内容\n","csharp","",[239,240,241,248],"code",{"__ignoreMap":237},[20,242,245],{"class":243,"line":244},"line",1,[20,246,247],{},"\u002F\u002F 原生 ScrollViewer：滚轮滚动后立即停止\n",[20,249,251],{"class":243,"line":250},2,[20,252,253],{},"\u002F\u002F 用户需要多次滚动才能浏览长内容\n",[155,255,256,259],{},[25,257,258],{},"交互体验落后",[161,260,261,264,267],{},[155,262,263],{},"不符合现代用户习惯（移动应用、Web 浏览器都有惯性滚动）",[155,265,266],{},"滚动感觉\"生硬\"、\"机械\"",[155,268,269],{},"无法通过快速拖拽产生惯性滚动",[155,271,272,275],{},[25,273,274],{},"可定制性有限",[161,276,277,280,283],{},[155,278,279],{},"无法调整滚动速度曲线",[155,281,282],{},"无法实现边界反弹效果",[155,284,285],{},"无法自定义滚动动画",[155,287,288,291],{},[25,289,290],{},"长内容浏览困难",[161,292,293,296,299],{},[155,294,295],{},"需要频繁操作滚轮",[155,297,298],{},"无法通过快速拖拽快速浏览",[155,300,301],{},"用户体验较差",[38,303,305],{"id":304},"自定义惯性滚动-scrollviewer-的优缺点","自定义惯性滚动 ScrollViewer 的优缺点",[147,307,150],{"id":308},"优点-1",[152,310,311,342,357,373,389],{},[155,312,313,316,331],{},[25,314,315],{},"流畅的用户体验",[232,317,319],{"className":234,"code":318,"language":236,"meta":237,"style":237},"\u002F\u002F 自定义实现：滚轮滚动后继续惯性滚动\n\u002F\u002F 用户可以快速浏览长内容，体验更自然\n",[239,320,321,326],{"__ignoreMap":237},[20,322,323],{"class":243,"line":244},[20,324,325],{},"\u002F\u002F 自定义实现：滚轮滚动后继续惯性滚动\n",[20,327,328],{"class":243,"line":250},[20,329,330],{},"\u002F\u002F 用户可以快速浏览长内容，体验更自然\n",[161,332,333,336,339],{},[155,334,335],{},"符合现代用户习惯",[155,337,338],{},"滚动感觉\"流畅\"、\"自然\"",[155,340,341],{},"类似移动应用和 Web 浏览器的体验",[155,343,344,346],{},[25,345,83],{},[161,347,348,351,354],{},[155,349,350],{},"滚轮滚动后继续滚动，逐渐减速",[155,352,353],{},"快速拖拽后产生惯性滚动",[155,355,356],{},"减少用户操作次数",[155,358,359,362],{},[25,360,361],{},"高度可定制",[161,363,364,367,370],{},[155,365,366],{},"可调整物理参数（阻尼系数、弹性系数等）",[155,368,369],{},"可实现边界反弹效果",[155,371,372],{},"可自定义滚动动画曲线",[155,374,375,378],{},[25,376,377],{},"物理模拟",[161,379,380,383,386],{},[155,381,382],{},"基于真实的物理模型",[155,384,385],{},"滚动行为符合物理直觉",[155,387,388],{},"提供视觉反馈（边界反弹）",[155,390,391,394],{},[25,392,393],{},"长内容浏览友好",[161,395,396,399,402],{},[155,397,398],{},"快速滚动浏览大量内容",[155,400,401],{},"减少滚轮操作次数",[155,403,404],{},"提高浏览效率",[147,406,223],{"id":407},"缺点-1",[152,409,410,429,445,465,481],{},[155,411,412,414],{},[25,413,109],{},[161,415,416,419,426],{},[155,417,418],{},"需要每帧进行物理计算",[155,420,421,422,425],{},"需要订阅 ",[239,423,424],{},"CompositionTarget.Rendering"," 事件",[155,427,428],{},"在低性能设备上可能影响流畅度",[155,430,431,434],{},[25,432,433],{},"实现复杂度",[161,435,436,439,442],{},[155,437,438],{},"需要理解物理模型",[155,440,441],{},"需要处理边界情况",[155,443,444],{},"需要调试和优化参数",[155,446,447,450],{},[25,448,449],{},"资源占用",[161,451,452,459,462],{},[155,453,454,455,458],{},"需要高精度时间测量（",[239,456,457],{},"Stopwatch","）",[155,460,461],{},"需要维护状态变量",[155,463,464],{},"需要事件订阅\u002F取消订阅管理",[155,466,467,470],{},[25,468,469],{},"可能的不稳定性",[161,471,472,475,478],{},[155,473,474],{},"自定义实现可能有意外的边界情况",[155,476,477],{},"需要充分测试",[155,479,480],{},"可能与其他控件有兼容性问题",[155,482,483,486],{},[25,484,485],{},"精确度权衡",[161,487,488,491],{},[155,489,490],{},"滚动距离不是精确的 1:1 像素对应",[155,492,493],{},"可能不适合需要精确控制的场景（如 CAD 软件）",[38,495,496],{"id":496},"使用场景建议",[147,498,500],{"id":499},"适合使用原生-scrollviewer-的场景","适合使用原生 ScrollViewer 的场景",[14,502,503,509],{},[17,504,505,508],{},[20,506,507],{},"!tip"," 推荐场景",[161,510,511,514,517,520,523],{},[155,512,513],{},"✅ 企业级应用（需要稳定性和可预测性）",[155,515,516],{},"✅ 需要精确控制滚动位置的场景（如 CAD、图像编辑）",[155,518,519],{},"✅ 资源受限的环境（嵌入式系统、低性能设备）",[155,521,522],{},"✅ 简单的列表展示（不需要复杂交互）",[155,524,525],{},"✅ 需要与现有 WPF 应用保持一致性的场景",[147,527,529],{"id":528},"适合使用自定义惯性滚动-scrollviewer-的场景","适合使用自定义惯性滚动 ScrollViewer 的场景",[14,531,532,536],{},[17,533,534,508],{},[20,535,507],{},[161,537,538,541,544,547,550],{},[155,539,540],{},"✅ 现代桌面应用（追求流畅体验）",[155,542,543],{},"✅ 内容浏览类应用（文档阅读器、新闻客户端）",[155,545,546],{},"✅ 需要快速浏览大量内容的场景",[155,548,549],{},"✅ 面向年轻用户群体的应用",[155,551,552],{},"✅ 需要与移动应用保持体验一致性的场景",[38,554,555],{"id":555},"性能对比示例",[147,557,559],{"id":558},"原生-scrollviewer","原生 ScrollViewer",[232,561,566],{"className":562,"code":564,"language":565},[563],"language-text","用户操作：滚轮滚动 3 次\n  ↓\n滚动距离：每次 3 行 × 3 次 = 9 行\n  ↓\n结果：立即停止，用户需要继续滚动\n","text",[239,567,564],{"__ignoreMap":237},[147,569,59],{"id":570},"自定义惯性滚动-scrollviewer",[232,572,575],{"className":573,"code":574,"language":565},[563],"用户操作：滚轮快速滚动 3 次\n  ↓\n初始速度：高速度\n  ↓\n惯性滚动：继续滚动 20+ 行，逐渐减速\n  ↓\n结果：一次操作浏览更多内容\n",[239,576,574],{"__ignoreMap":237},[38,578,579],{"id":579},"总结",[14,581,582,588],{},[17,583,584,587],{},[20,585,586],{},"!note"," 选择建议",[161,589,590,596,602,607],{},[155,591,592,595],{},[25,593,594],{},"追求稳定性和精确性"," → 使用原生 ScrollViewer",[155,597,598,601],{},[25,599,600],{},"追求流畅体验和现代感"," → 使用自定义惯性滚动 ScrollViewer",[155,603,604,595],{},[25,605,606],{},"资源受限",[155,608,609,601],{},[25,610,611],{},"需要快速浏览长内容",[30,613],{},[33,615,617],{"id":616},"物理模型二阶缓动系统","📐 物理模型：二阶缓动系统",[38,619,620],{"id":620},"核心概念",[17,622,623,624,627],{},"我们使用",[25,625,626],{},"二阶缓动系统","（Second-Order System）来模拟真实的物理运动。这个系统基于以下物理原理：",[147,629,631],{"id":630},"_1-位置速度加速度的关系","1. 位置、速度、加速度的关系",[17,633,634],{},"在物理学中，位置、速度和加速度之间存在以下关系：",[17,636,637],{},"$$\n\\begin{align}\nv(t) &= \\frac{dx(t)}{dt} \\quad \\text{(速度是位置的导数)} \\\na(t) &= \\frac{dv(t)}{dt} = \\frac{d^2x(t)}{dt^2} \\quad \\text{(加速度是速度的导数)}\n\\end{align}\n$$",[147,639,641],{"id":640},"_2-阻尼振荡系统","2. 阻尼振荡系统",[17,643,644,645,648],{},"我们的滚动系统可以建模为一个",[25,646,647],{},"阻尼振荡器","（Damped Oscillator）：",[17,650,651],{},"$$\nm \\frac{d^2x}{dt^2} + c \\frac{dx}{dt} + kx = F(t)\n$$",[17,653,654],{},"其中：",[161,656,657,660,663,666],{},[155,658,659],{},"$m$ = 质量（在我们的实现中简化为 1）",[155,661,662],{},"$c$ = 阻尼系数（Damping Coefficient）",[155,664,665],{},"$k$ = 弹性系数（Spring Stiffness）",[155,667,668],{},"$F(t)$ = 外力（用户输入）",[14,670,671,676],{},[17,672,673,675],{},[20,674,507],{}," 简化模型\n在我们的实现中，我们简化了这个模型：",[161,677,678,681,684],{},[155,679,680],{},"不考虑质量（$m = 1$）",[155,682,683],{},"不考虑弹性恢复力（$k = 0$，除非在边界处）",[155,685,686,687,690,691],{},"主要关注",[25,688,689],{},"速度衰减","和",[25,692,693],{},"边界反弹",[38,695,696],{"id":696},"离散时间实现",[17,698,699],{},"由于我们使用帧更新（Frame-based Update），需要将连续时间的微分方程转换为离散时间的递推关系：",[147,701,702],{"id":702},"速度衰减公式",[17,704,705],{},"每帧更新时，速度按阻尼系数衰减：",[17,707,708],{},"$$\nv_{n+1} = v_n \\times \\text{DampingCoefficient}\n$$",[17,710,711,712,715],{},"其中 ",[239,713,714],{},"DampingCoefficient = 0.96","，意味着每帧速度衰减到原来的 96%。",[147,717,718],{"id":718},"位置更新公式",[17,720,721],{},"位置根据当前速度和时间步长更新：",[17,723,724],{},"$$\nx_{n+1} = x_n + v_n \\times \\Delta t\n$$",[17,726,727],{},"其中 $\\Delta t$ 是实际的时间步长（秒）。",[147,729,730],{"id":730},"边界反弹公式",[17,732,733],{},"当到达边界时，速度反向并衰减：",[17,735,736],{},"$$\nv_{n+1} = -v_n \\times \\text{SpringStiffness}\n$$",[17,738,711,739,742],{},[239,740,741],{},"SpringStiffness = 0.25","，意味着反弹时速度变为原来的 25%。",[30,744],{},[33,746,748],{"id":747},"实现细节","🔧 实现细节",[38,750,752],{"id":751},"_1-物理参数配置","1. 物理参数配置",[232,754,756],{"className":234,"code":755,"language":236,"meta":237,"style":237},"\u002F\u002F 物理参数\nprivate const double DampingCoefficient = 0.96;  \u002F\u002F 阻尼系数\nprivate const double MinVelocity = 0.1;           \u002F\u002F 最小速度阈值\nprivate const double SpringStiffness = 0.25;     \u002F\u002F 弹性系数\nprivate const double VelocityMultiplier = 16.0;   \u002F\u002F 速度倍数\n",[239,757,758,763,768,774,780],{"__ignoreMap":237},[20,759,760],{"class":243,"line":244},[20,761,762],{},"\u002F\u002F 物理参数\n",[20,764,765],{"class":243,"line":250},[20,766,767],{},"private const double DampingCoefficient = 0.96;  \u002F\u002F 阻尼系数\n",[20,769,771],{"class":243,"line":770},3,[20,772,773],{},"private const double MinVelocity = 0.1;           \u002F\u002F 最小速度阈值\n",[20,775,777],{"class":243,"line":776},4,[20,778,779],{},"private const double SpringStiffness = 0.25;     \u002F\u002F 弹性系数\n",[20,781,783],{"class":243,"line":782},5,[20,784,785],{},"private const double VelocityMultiplier = 16.0;   \u002F\u002F 速度倍数\n",[14,787,788,794],{},[17,789,790,793],{},[20,791,792],{},"!info"," 参数说明",[161,795,796,802,808,814],{},[155,797,798,801],{},[25,799,800],{},"DampingCoefficient (0.96)",": 控制速度衰减速度。值越大，滚动持续时间越长。范围：0-1",[155,803,804,807],{},[25,805,806],{},"MinVelocity (0.1)",": 最小速度阈值。当速度低于此值时停止滚动，避免无限小的微动",[155,809,810,813],{},[25,811,812],{},"SpringStiffness (0.25)",": 边界反弹强度。值越大，反弹越明显",[155,815,816,819],{},[25,817,818],{},"VelocityMultiplier (16.0)",": 速度倍数，用于调整整体滚动速度",[38,821,823],{"id":822},"_2-高精度时间测量","2. 高精度时间测量",[14,825,826],{},[17,827,828,831,832,835,836,839,840,843],{},[20,829,830],{},"!important"," 为什么使用 Stopwatch？\nWPF 的 ",[239,833,834],{},"DispatcherTimer"," 精度有限（通常 15-16ms），且使用固定时间间隔。我们使用 ",[239,837,838],{},"Stopwatch.GetTimestamp()"," 来获得高精度时间戳，计算",[25,841,842],{},"实际的时间差","，确保物理计算的准确性。",[232,845,847],{"className":234,"code":846,"language":236,"meta":237,"style":237},"\u002F\u002F 使用高精度时间戳\nlong currentTime = Stopwatch.GetTimestamp();\ndouble deltaTime = (currentTime - _lastAnimationTime) \u002F (double)Stopwatch.Frequency;\n",[239,848,849,854,859],{"__ignoreMap":237},[20,850,851],{"class":243,"line":244},[20,852,853],{},"\u002F\u002F 使用高精度时间戳\n",[20,855,856],{"class":243,"line":250},[20,857,858],{},"long currentTime = Stopwatch.GetTimestamp();\n",[20,860,861],{"class":243,"line":770},[20,862,863],{},"double deltaTime = (currentTime - _lastAnimationTime) \u002F (double)Stopwatch.Frequency;\n",[17,865,866,869],{},[25,867,868],{},"优势","：",[161,871,872,875,878],{},[155,873,874],{},"自动适配不同的帧率（60Hz、120Hz、144Hz 等）",[155,876,877],{},"处理卡顿情况（时间步长会自动增大）",[155,879,880],{},"更准确的物理模拟",[38,882,884],{"id":883},"_3-事件拦截与处理","3. 事件拦截与处理",[147,886,887],{"id":887},"鼠标滚轮事件",[232,889,891],{"className":234,"code":890,"language":236,"meta":237,"style":237},"private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)\n{\n    \u002F\u002F 计算初始速度（基于滚轮增量）\n    double delta = e.Delta;\n    double velocity = delta * 0.5 * VelocityMultiplier;\n    \n    \u002F\u002F 根据滚轮方向设置速度\n    _velocityY = e.Delta > 0 ? -Math.Abs(velocity) : Math.Abs(velocity);\n    \n    \u002F\u002F 启动惯性滚动\n    StartInertialScroll();\n    \n    \u002F\u002F 阻止默认滚动行为\n    e.Handled = true;\n}\n",[239,892,893,898,903,908,913,918,924,930,936,941,947,953,958,964,970],{"__ignoreMap":237},[20,894,895],{"class":243,"line":244},[20,896,897],{},"private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)\n",[20,899,900],{"class":243,"line":250},[20,901,902],{},"{\n",[20,904,905],{"class":243,"line":770},[20,906,907],{},"    \u002F\u002F 计算初始速度（基于滚轮增量）\n",[20,909,910],{"class":243,"line":776},[20,911,912],{},"    double delta = e.Delta;\n",[20,914,915],{"class":243,"line":782},[20,916,917],{},"    double velocity = delta * 0.5 * VelocityMultiplier;\n",[20,919,921],{"class":243,"line":920},6,[20,922,923],{},"    \n",[20,925,927],{"class":243,"line":926},7,[20,928,929],{},"    \u002F\u002F 根据滚轮方向设置速度\n",[20,931,933],{"class":243,"line":932},8,[20,934,935],{},"    _velocityY = e.Delta > 0 ? -Math.Abs(velocity) : Math.Abs(velocity);\n",[20,937,939],{"class":243,"line":938},9,[20,940,923],{},[20,942,944],{"class":243,"line":943},10,[20,945,946],{},"    \u002F\u002F 启动惯性滚动\n",[20,948,950],{"class":243,"line":949},11,[20,951,952],{},"    StartInertialScroll();\n",[20,954,956],{"class":243,"line":955},12,[20,957,923],{},[20,959,961],{"class":243,"line":960},13,[20,962,963],{},"    \u002F\u002F 阻止默认滚动行为\n",[20,965,967],{"class":243,"line":966},14,[20,968,969],{},"    e.Handled = true;\n",[20,971,973],{"class":243,"line":972},15,[20,974,975],{},"}\n",[14,977,978,983],{},[17,979,980,982],{},[20,981,586],{}," 关键点",[161,984,985,992,995],{},[155,986,987,988,991],{},"使用 ",[239,989,990],{},"PreviewMouseWheel"," 事件在默认处理之前拦截",[155,993,994],{},"将滚轮增量转换为初始速度",[155,996,997,998,1001],{},"设置 ",[239,999,1000],{},"e.Handled = true"," 阻止默认滚动",[147,1003,1004],{"id":1004},"鼠标拖拽事件",[17,1006,1007],{},"拖拽时，我们需要：",[152,1009,1010,1013,1016],{},[155,1011,1012],{},"实时跟踪鼠标位置",[155,1014,1015],{},"计算速度（基于位移和时间差）",[155,1017,1018],{},"直接更新滚动位置",[232,1020,1022],{"className":234,"code":1021,"language":236,"meta":237,"style":237},"private void OnMouseMove(object sender, MouseEventArgs e)\n{\n    if (!_isDragging) return;\n    \n    Point currentPosition = e.GetPosition(this);\n    long currentTime = Stopwatch.GetTimestamp();\n    \n    \u002F\u002F 计算时间差\n    double deltaTime = (currentTime - _lastUpdateTime) \u002F (double)Stopwatch.Frequency;\n    if (deltaTime \u003C 0.0001) deltaTime = 0.0001; \u002F\u002F 防止除零\n    \n    \u002F\u002F 计算位移\n    double deltaX = currentPosition.X - _lastMousePosition.X;\n    double deltaY = currentPosition.Y - _lastMousePosition.Y;\n    \n    \u002F\u002F 计算速度（像素\u002F秒）\n    double rawVelocityX = (deltaX \u002F deltaTime) * VelocityMultiplier;\n    double rawVelocityY = (deltaY \u002F deltaTime) * VelocityMultiplier;\n    \n    \u002F\u002F 直接使用计算的速度（不进行平滑处理，让速度可以更快）\n    _velocityX = rawVelocityX;\n    _velocityY = rawVelocityY;\n    \n    \u002F\u002F 直接移动滚动位置\n    double newOffsetX = HorizontalOffset - deltaX;\n    double newOffsetY = VerticalOffset - deltaY;\n    \n    ScrollToHorizontalOffset(Math.Max(0, Math.Min(ScrollableWidth, newOffsetX)));\n    ScrollToVerticalOffset(Math.Max(0, Math.Min(ScrollableHeight, newOffsetY)));\n    \n    \u002F\u002F 更新状态\n    _lastMousePosition = currentPosition;\n    _lastUpdateTime = currentTime;\n}\n",[239,1023,1024,1029,1033,1038,1042,1047,1052,1056,1061,1066,1071,1075,1080,1085,1090,1094,1100,1106,1112,1117,1123,1129,1135,1140,1146,1152,1158,1163,1169,1175,1180,1186,1192,1198],{"__ignoreMap":237},[20,1025,1026],{"class":243,"line":244},[20,1027,1028],{},"private void OnMouseMove(object sender, MouseEventArgs e)\n",[20,1030,1031],{"class":243,"line":250},[20,1032,902],{},[20,1034,1035],{"class":243,"line":770},[20,1036,1037],{},"    if (!_isDragging) return;\n",[20,1039,1040],{"class":243,"line":776},[20,1041,923],{},[20,1043,1044],{"class":243,"line":782},[20,1045,1046],{},"    Point currentPosition = e.GetPosition(this);\n",[20,1048,1049],{"class":243,"line":920},[20,1050,1051],{},"    long currentTime = Stopwatch.GetTimestamp();\n",[20,1053,1054],{"class":243,"line":926},[20,1055,923],{},[20,1057,1058],{"class":243,"line":932},[20,1059,1060],{},"    \u002F\u002F 计算时间差\n",[20,1062,1063],{"class":243,"line":938},[20,1064,1065],{},"    double deltaTime = (currentTime - _lastUpdateTime) \u002F (double)Stopwatch.Frequency;\n",[20,1067,1068],{"class":243,"line":943},[20,1069,1070],{},"    if (deltaTime \u003C 0.0001) deltaTime = 0.0001; \u002F\u002F 防止除零\n",[20,1072,1073],{"class":243,"line":949},[20,1074,923],{},[20,1076,1077],{"class":243,"line":955},[20,1078,1079],{},"    \u002F\u002F 计算位移\n",[20,1081,1082],{"class":243,"line":960},[20,1083,1084],{},"    double deltaX = currentPosition.X - _lastMousePosition.X;\n",[20,1086,1087],{"class":243,"line":966},[20,1088,1089],{},"    double deltaY = currentPosition.Y - _lastMousePosition.Y;\n",[20,1091,1092],{"class":243,"line":972},[20,1093,923],{},[20,1095,1097],{"class":243,"line":1096},16,[20,1098,1099],{},"    \u002F\u002F 计算速度（像素\u002F秒）\n",[20,1101,1103],{"class":243,"line":1102},17,[20,1104,1105],{},"    double rawVelocityX = (deltaX \u002F deltaTime) * VelocityMultiplier;\n",[20,1107,1109],{"class":243,"line":1108},18,[20,1110,1111],{},"    double rawVelocityY = (deltaY \u002F deltaTime) * VelocityMultiplier;\n",[20,1113,1115],{"class":243,"line":1114},19,[20,1116,923],{},[20,1118,1120],{"class":243,"line":1119},20,[20,1121,1122],{},"    \u002F\u002F 直接使用计算的速度（不进行平滑处理，让速度可以更快）\n",[20,1124,1126],{"class":243,"line":1125},21,[20,1127,1128],{},"    _velocityX = rawVelocityX;\n",[20,1130,1132],{"class":243,"line":1131},22,[20,1133,1134],{},"    _velocityY = rawVelocityY;\n",[20,1136,1138],{"class":243,"line":1137},23,[20,1139,923],{},[20,1141,1143],{"class":243,"line":1142},24,[20,1144,1145],{},"    \u002F\u002F 直接移动滚动位置\n",[20,1147,1149],{"class":243,"line":1148},25,[20,1150,1151],{},"    double newOffsetX = HorizontalOffset - deltaX;\n",[20,1153,1155],{"class":243,"line":1154},26,[20,1156,1157],{},"    double newOffsetY = VerticalOffset - deltaY;\n",[20,1159,1161],{"class":243,"line":1160},27,[20,1162,923],{},[20,1164,1166],{"class":243,"line":1165},28,[20,1167,1168],{},"    ScrollToHorizontalOffset(Math.Max(0, Math.Min(ScrollableWidth, newOffsetX)));\n",[20,1170,1172],{"class":243,"line":1171},29,[20,1173,1174],{},"    ScrollToVerticalOffset(Math.Max(0, Math.Min(ScrollableHeight, newOffsetY)));\n",[20,1176,1178],{"class":243,"line":1177},30,[20,1179,923],{},[20,1181,1183],{"class":243,"line":1182},31,[20,1184,1185],{},"    \u002F\u002F 更新状态\n",[20,1187,1189],{"class":243,"line":1188},32,[20,1190,1191],{},"    _lastMousePosition = currentPosition;\n",[20,1193,1195],{"class":243,"line":1194},33,[20,1196,1197],{},"    _lastUpdateTime = currentTime;\n",[20,1199,1201],{"class":243,"line":1200},34,[20,1202,975],{},[14,1204,1205,1210],{},[17,1206,1207,1209],{},[20,1208,507],{}," 速度计算\n速度 = 位移 \u002F 时间差 × 速度倍数",[17,1211,1212],{},"例如：如果鼠标在 0.016 秒内移动了 10 像素，则速度为：\n$v = \\frac{10}{0.016} \\times 16 = 10,000$ 像素\u002F秒",[38,1214,1216],{"id":1215},"_4-动画更新compositiontargetrendering","4. 动画更新：CompositionTarget.Rendering",[147,1218,1220],{"id":1219},"什么是-compositiontargetrendering","什么是 CompositionTarget.Rendering？",[14,1222,1223],{},[17,1224,1225,1227,1228,1230,1231,1234,1235,1238],{},[20,1226,792],{}," CompositionTarget.Rendering 详解\n",[239,1229,424],{}," 是 WPF 渲染管道的核心事件，它在",[25,1232,1233],{},"每一帧渲染之前","被触发。这个事件是",[25,1236,1237],{},"静态事件","（Static Event），意味着它是全局的，所有订阅者都会在每帧被调用。",[1240,1241,1242],"h5",{"id":1242},"工作原理",[232,1244,1248],{"className":1245,"code":1246,"language":1247,"meta":237,"style":237},"language-mermaid shiki shiki-themes github-light github-dark","graph TD\n    A[WPF 渲染循环开始] --> B[触发 CompositionTarget.Rendering 事件]\n    B --> C[所有订阅者执行 OnRendering 方法]\n    C --> D[更新 UI 状态\u002F属性]\n    D --> E[执行布局计算]\n    E --> F[执行渲染]\n    F --> G[显示到屏幕]\n    G --> H{还有下一帧?}\n    H -->|是| A\n    H -->|否| I[结束]\n","mermaid",[239,1249,1250,1255,1260,1265,1270,1275,1280,1285,1290,1295],{"__ignoreMap":237},[20,1251,1252],{"class":243,"line":244},[20,1253,1254],{},"graph TD\n",[20,1256,1257],{"class":243,"line":250},[20,1258,1259],{},"    A[WPF 渲染循环开始] --> B[触发 CompositionTarget.Rendering 事件]\n",[20,1261,1262],{"class":243,"line":770},[20,1263,1264],{},"    B --> C[所有订阅者执行 OnRendering 方法]\n",[20,1266,1267],{"class":243,"line":776},[20,1268,1269],{},"    C --> D[更新 UI 状态\u002F属性]\n",[20,1271,1272],{"class":243,"line":782},[20,1273,1274],{},"    D --> E[执行布局计算]\n",[20,1276,1277],{"class":243,"line":920},[20,1278,1279],{},"    E --> F[执行渲染]\n",[20,1281,1282],{"class":243,"line":926},[20,1283,1284],{},"    F --> G[显示到屏幕]\n",[20,1286,1287],{"class":243,"line":932},[20,1288,1289],{},"    G --> H{还有下一帧?}\n",[20,1291,1292],{"class":243,"line":938},[20,1293,1294],{},"    H -->|是| A\n",[20,1296,1297],{"class":243,"line":943},[20,1298,1299],{},"    H -->|否| I[结束]\n",[1240,1301,1302],{"id":1302},"关键特性",[152,1304,1305,1328,1348,1367],{},[155,1306,1307,1310],{},[25,1308,1309],{},"UI 线程执行",[161,1311,1312,1319,1325],{},[155,1313,1314,1315,1318],{},"✅ 所有事件处理都在 ",[25,1316,1317],{},"UI 线程","上执行",[155,1320,1321,1322],{},"✅ 可以直接修改 UI 元素，无需 ",[239,1323,1324],{},"Dispatcher.Invoke",[155,1326,1327],{},"✅ 线程安全（因为都在同一线程）",[155,1329,1330,1333],{},[25,1331,1332],{},"与渲染同步",[161,1334,1335,1342,1345],{},[155,1336,1337,1338,1341],{},"✅ 在",[25,1339,1340],{},"每帧渲染之前","调用",[155,1343,1344],{},"✅ 确保动画更新在渲染前完成",[155,1346,1347],{},"✅ 避免视觉撕裂和不一致",[155,1349,1350,1353],{},[25,1351,1352],{},"自动适配帧率",[161,1354,1355,1358,1361,1364],{},[155,1356,1357],{},"✅ 60Hz 显示器：每秒调用 60 次",[155,1359,1360],{},"✅ 120Hz 显示器：每秒调用 120 次",[155,1362,1363],{},"✅ 144Hz 显示器：每秒调用 144 次",[155,1365,1366],{},"✅ 自动适配，无需手动配置",[155,1368,1369,1372],{},[25,1370,1371],{},"静态事件机制",[232,1373,1375],{"className":234,"code":1374,"language":236,"meta":237,"style":237},"\u002F\u002F CompositionTarget 是静态类\npublic static class CompositionTarget\n{\n    \u002F\u002F Rendering 是静态事件\n    public static event EventHandler Rendering;\n}\n",[239,1376,1377,1382,1387,1391,1396,1401],{"__ignoreMap":237},[20,1378,1379],{"class":243,"line":244},[20,1380,1381],{},"\u002F\u002F CompositionTarget 是静态类\n",[20,1383,1384],{"class":243,"line":250},[20,1385,1386],{},"public static class CompositionTarget\n",[20,1388,1389],{"class":243,"line":770},[20,1390,902],{},[20,1392,1393],{"class":243,"line":776},[20,1394,1395],{},"    \u002F\u002F Rendering 是静态事件\n",[20,1397,1398],{"class":243,"line":782},[20,1399,1400],{},"    public static event EventHandler Rendering;\n",[20,1402,1403],{"class":243,"line":920},[20,1404,975],{},[14,1406,1407,1413],{},[17,1408,1409,1412],{},[20,1410,1411],{},"!warning"," 重要提示\n由于是静态事件，所有订阅者都会在每帧被调用。如果订阅者很多或执行耗时操作，会影响整体性能。因此：",[161,1414,1415,1421,1427],{},[155,1416,1417,1418,458],{},"✅ 只在需要时订阅（如我们的 ",[239,1419,1420],{},"StartInertialScroll",[155,1422,1423,1424,458],{},"✅ 及时取消订阅（如我们的 ",[239,1425,1426],{},"StopInertialScroll",[155,1428,1429],{},"✅ 保持事件处理函数高效",[147,1431,1432],{"id":1432},"订阅机制详解",[1240,1434,1435],{"id":1435},"订阅过程",[232,1437,1439],{"className":234,"code":1438,"language":236,"meta":237,"style":237},"private void StartInertialScroll()\n{\n    if (!_isScrolling)\n    {\n        _isScrolling = true;\n        _lastAnimationTime = Stopwatch.GetTimestamp();\n        \n        \u002F\u002F 订阅事件：将 OnRendering 方法添加到事件处理列表\n        CompositionTarget.Rendering += OnRendering;\n    }\n}\n",[239,1440,1441,1446,1450,1455,1460,1465,1470,1475,1480,1485,1490],{"__ignoreMap":237},[20,1442,1443],{"class":243,"line":244},[20,1444,1445],{},"private void StartInertialScroll()\n",[20,1447,1448],{"class":243,"line":250},[20,1449,902],{},[20,1451,1452],{"class":243,"line":770},[20,1453,1454],{},"    if (!_isScrolling)\n",[20,1456,1457],{"class":243,"line":776},[20,1458,1459],{},"    {\n",[20,1461,1462],{"class":243,"line":782},[20,1463,1464],{},"        _isScrolling = true;\n",[20,1466,1467],{"class":243,"line":920},[20,1468,1469],{},"        _lastAnimationTime = Stopwatch.GetTimestamp();\n",[20,1471,1472],{"class":243,"line":926},[20,1473,1474],{},"        \n",[20,1476,1477],{"class":243,"line":932},[20,1478,1479],{},"        \u002F\u002F 订阅事件：将 OnRendering 方法添加到事件处理列表\n",[20,1481,1482],{"class":243,"line":938},[20,1483,1484],{},"        CompositionTarget.Rendering += OnRendering;\n",[20,1486,1487],{"class":243,"line":943},[20,1488,1489],{},"    }\n",[20,1491,1492],{"class":243,"line":949},[20,1493,975],{},[17,1495,1496],{},[25,1497,1498],{},"发生了什么？",[152,1500,1501,1506,1512],{},[155,1502,1503,1505],{},[239,1504,424],{}," 是静态事件，所有订阅者共享",[155,1507,1508,1511],{},[239,1509,1510],{},"+= OnRendering"," 将我们的方法添加到事件处理列表",[155,1513,1514,1515],{},"从下一帧开始，WPF 会在每帧渲染前调用 ",[239,1516,1517],{},"OnRendering",[1240,1519,1520],{"id":1520},"取消订阅过程",[232,1522,1524],{"className":234,"code":1523,"language":236,"meta":237,"style":237},"private void StopInertialScroll()\n{\n    if (_isScrolling)\n    {\n        _isScrolling = false;\n        _velocityX = 0;\n        _velocityY = 0;\n        \n        \u002F\u002F 取消订阅：从事件处理列表中移除 OnRendering\n        CompositionTarget.Rendering -= OnRendering;\n    }\n}\n",[239,1525,1526,1531,1535,1540,1544,1549,1554,1559,1563,1568,1573,1577],{"__ignoreMap":237},[20,1527,1528],{"class":243,"line":244},[20,1529,1530],{},"private void StopInertialScroll()\n",[20,1532,1533],{"class":243,"line":250},[20,1534,902],{},[20,1536,1537],{"class":243,"line":770},[20,1538,1539],{},"    if (_isScrolling)\n",[20,1541,1542],{"class":243,"line":776},[20,1543,1459],{},[20,1545,1546],{"class":243,"line":782},[20,1547,1548],{},"        _isScrolling = false;\n",[20,1550,1551],{"class":243,"line":920},[20,1552,1553],{},"        _velocityX = 0;\n",[20,1555,1556],{"class":243,"line":926},[20,1557,1558],{},"        _velocityY = 0;\n",[20,1560,1561],{"class":243,"line":932},[20,1562,1474],{},[20,1564,1565],{"class":243,"line":938},[20,1566,1567],{},"        \u002F\u002F 取消订阅：从事件处理列表中移除 OnRendering\n",[20,1569,1570],{"class":243,"line":943},[20,1571,1572],{},"        CompositionTarget.Rendering -= OnRendering;\n",[20,1574,1575],{"class":243,"line":949},[20,1576,1489],{},[20,1578,1579],{"class":243,"line":955},[20,1580,975],{},[17,1582,1583],{},[25,1584,1498],{},[152,1586,1587,1593,1599],{},[155,1588,1589,1592],{},[239,1590,1591],{},"-= OnRendering"," 将我们的方法从事件处理列表中移除",[155,1594,1595,1596,1598],{},"从下一帧开始，",[239,1597,1517],{}," 不再被调用",[155,1600,1601],{},"节省 CPU 资源（不再执行不必要的计算）",[147,1603,1604],{"id":1604},"事件调用时机",[232,1606,1609],{"className":1607,"code":1608,"language":565},[563],"帧 N 开始\n  ↓\n触发 CompositionTarget.Rendering 事件\n  ↓\n执行所有订阅者的处理函数（包括我们的 OnRendering）\n  ↓\n  ├─ 计算物理状态（位置、速度）\n  ├─ 更新 ScrollViewer 的滚动位置\n  └─ 检查是否需要停止滚动\n  ↓\n执行布局计算（Layout）\n  ↓\n执行渲染（Render）\n  ↓\n显示到屏幕\n  ↓\n帧 N 结束，等待下一帧（帧 N+1）\n",[239,1610,1608],{"__ignoreMap":237},[147,1612,1614],{"id":1613},"为什么比-dispatchertimer-更好","为什么比 DispatcherTimer 更好？",[42,1616,1617,1627],{},[45,1618,1619],{},[48,1620,1621,1623,1625],{},[51,1622,53],{},[51,1624,424],{},[51,1626,834],{},[61,1628,1629,1642,1655,1668,1681,1694],{},[48,1630,1631,1636,1639],{},[66,1632,1633],{},[25,1634,1635],{},"调用时机",[66,1637,1638],{},"每帧渲染前",[66,1640,1641],{},"固定时间间隔",[48,1643,1644,1649,1652],{},[66,1645,1646],{},[25,1647,1648],{},"帧率适配",[66,1650,1651],{},"自动适配（60\u002F120\u002F144Hz）",[66,1653,1654],{},"固定间隔（通常 16ms）",[48,1656,1657,1662,1665],{},[66,1658,1659],{},[25,1660,1661],{},"同步性",[66,1663,1664],{},"与渲染完全同步",[66,1666,1667],{},"可能不同步",[48,1669,1670,1675,1678],{},[66,1671,1672],{},[25,1673,1674],{},"平滑度",[66,1676,1677],{},"更平滑，无卡顿感",[66,1679,1680],{},"可能卡顿",[48,1682,1683,1688,1691],{},[66,1684,1685],{},[25,1686,1687],{},"性能",[66,1689,1690],{},"更高效（与渲染管道集成）",[66,1692,1693],{},"额外线程开销",[48,1695,1696,1701,1704],{},[66,1697,1698],{},[25,1699,1700],{},"精度",[66,1702,1703],{},"高精度（基于实际帧时间）",[66,1705,1706],{},"固定精度",[1240,1708,1709],{"id":1709},"实际对比示例",[17,1711,1712,869],{},[25,1713,1714],{},"DispatcherTimer（固定间隔）",[232,1716,1719],{"className":1717,"code":1718,"language":565},[563],"时间轴: 0ms    16ms    32ms    48ms    64ms\n更新:   ✓      ✓       ✓       ✓       ✓\n渲染:   ?      ?       ?       ?       ?\n",[239,1720,1718],{"__ignoreMap":237},[17,1722,1723],{},"❌ 更新和渲染可能不同步，导致视觉不连贯",[17,1725,1726,869],{},[25,1727,1728],{},"CompositionTarget.Rendering（与渲染同步）",[232,1730,1733],{"className":1731,"code":1732,"language":565},[563],"时间轴: 0ms    16.67ms 33.33ms 50ms    66.67ms\n更新:   ✓      ✓       ✓       ✓       ✓\n渲染:   ✓      ✓       ✓       ✓       ✓\n",[239,1734,1732],{"__ignoreMap":237},[17,1736,1737],{},"✅ 更新和渲染完全同步，视觉连贯",[147,1739,1740],{"id":1740},"在我们的实现中的作用",[232,1742,1744],{"className":234,"code":1743,"language":236,"meta":237,"style":237},"private void OnRendering(object? sender, EventArgs e)\n{\n    if (!_isScrolling) return; \u002F\u002F 安全检查\n    \n    \u002F\u002F 1. 计算实际时间差（基于高精度时间戳）\n    long currentTime = Stopwatch.GetTimestamp();\n    double deltaTime = (currentTime - _lastAnimationTime) \u002F (double)Stopwatch.Frequency;\n    _lastAnimationTime = currentTime;\n    \n    \u002F\u002F 2. 执行物理计算（位置、速度、边界检测）\n    \u002F\u002F ... 物理更新逻辑 ...\n    \n    \u002F\u002F 3. 更新 UI（滚动位置）\n    ScrollToHorizontalOffset(newX);\n    ScrollToVerticalOffset(newY);\n    \n    \u002F\u002F 4. 检查停止条件\n    if (Math.Abs(_velocityX) \u003C MinVelocity && Math.Abs(_velocityY) \u003C MinVelocity)\n    {\n        StopInertialScroll(); \u002F\u002F 自动取消订阅\n    }\n}\n",[239,1745,1746,1751,1755,1760,1764,1769,1773,1778,1783,1787,1792,1797,1801,1806,1811,1816,1820,1825,1830,1834,1839,1843],{"__ignoreMap":237},[20,1747,1748],{"class":243,"line":244},[20,1749,1750],{},"private void OnRendering(object? sender, EventArgs e)\n",[20,1752,1753],{"class":243,"line":250},[20,1754,902],{},[20,1756,1757],{"class":243,"line":770},[20,1758,1759],{},"    if (!_isScrolling) return; \u002F\u002F 安全检查\n",[20,1761,1762],{"class":243,"line":776},[20,1763,923],{},[20,1765,1766],{"class":243,"line":782},[20,1767,1768],{},"    \u002F\u002F 1. 计算实际时间差（基于高精度时间戳）\n",[20,1770,1771],{"class":243,"line":920},[20,1772,1051],{},[20,1774,1775],{"class":243,"line":926},[20,1776,1777],{},"    double deltaTime = (currentTime - _lastAnimationTime) \u002F (double)Stopwatch.Frequency;\n",[20,1779,1780],{"class":243,"line":932},[20,1781,1782],{},"    _lastAnimationTime = currentTime;\n",[20,1784,1785],{"class":243,"line":938},[20,1786,923],{},[20,1788,1789],{"class":243,"line":943},[20,1790,1791],{},"    \u002F\u002F 2. 执行物理计算（位置、速度、边界检测）\n",[20,1793,1794],{"class":243,"line":949},[20,1795,1796],{},"    \u002F\u002F ... 物理更新逻辑 ...\n",[20,1798,1799],{"class":243,"line":955},[20,1800,923],{},[20,1802,1803],{"class":243,"line":960},[20,1804,1805],{},"    \u002F\u002F 3. 更新 UI（滚动位置）\n",[20,1807,1808],{"class":243,"line":966},[20,1809,1810],{},"    ScrollToHorizontalOffset(newX);\n",[20,1812,1813],{"class":243,"line":972},[20,1814,1815],{},"    ScrollToVerticalOffset(newY);\n",[20,1817,1818],{"class":243,"line":1096},[20,1819,923],{},[20,1821,1822],{"class":243,"line":1102},[20,1823,1824],{},"    \u002F\u002F 4. 检查停止条件\n",[20,1826,1827],{"class":243,"line":1108},[20,1828,1829],{},"    if (Math.Abs(_velocityX) \u003C MinVelocity && Math.Abs(_velocityY) \u003C MinVelocity)\n",[20,1831,1832],{"class":243,"line":1114},[20,1833,1459],{},[20,1835,1836],{"class":243,"line":1119},[20,1837,1838],{},"        StopInertialScroll(); \u002F\u002F 自动取消订阅\n",[20,1840,1841],{"class":243,"line":1125},[20,1842,1489],{},[20,1844,1845],{"class":243,"line":1131},[20,1846,975],{},[14,1848,1849,1853],{},[17,1850,1851,982],{},[20,1852,507],{},[152,1854,1855,1863,1868,1874],{},[155,1856,1857,1860,1861],{},[25,1858,1859],{},"每帧调用","：只要订阅了事件，每帧都会调用 ",[239,1862,1517],{},[155,1864,1865,1867],{},[25,1866,1309],{},"：可以直接修改 UI，无需线程切换",[155,1869,1870,1873],{},[25,1871,1872],{},"自动停止","：当速度低于阈值时，自动取消订阅，停止更新",[155,1875,1876,1879],{},[25,1877,1878],{},"高效","：只在需要时订阅，不需要时立即取消",[147,1881,1882],{"id":1882},"性能考虑",[1240,1884,1886],{"id":1885},"订阅取消订阅的最佳实践","订阅\u002F取消订阅的最佳实践",[232,1888,1890],{"className":234,"code":1889,"language":236,"meta":237,"style":237},"\u002F\u002F ✅ 正确：只在需要时订阅\nprivate void StartInertialScroll()\n{\n    if (!_isScrolling) \u002F\u002F 防止重复订阅\n    {\n        _isScrolling = true;\n        CompositionTarget.Rendering += OnRendering;\n    }\n}\n\n\u002F\u002F ✅ 正确：及时取消订阅\nprivate void StopInertialScroll()\n{\n    if (_isScrolling) \u002F\u002F 防止重复取消订阅\n    {\n        _isScrolling = false;\n        CompositionTarget.Rendering -= OnRendering;\n    }\n}\n",[239,1891,1892,1897,1901,1905,1910,1914,1918,1922,1926,1930,1936,1941,1945,1949,1954,1958,1962,1966,1970],{"__ignoreMap":237},[20,1893,1894],{"class":243,"line":244},[20,1895,1896],{},"\u002F\u002F ✅ 正确：只在需要时订阅\n",[20,1898,1899],{"class":243,"line":250},[20,1900,1445],{},[20,1902,1903],{"class":243,"line":770},[20,1904,902],{},[20,1906,1907],{"class":243,"line":776},[20,1908,1909],{},"    if (!_isScrolling) \u002F\u002F 防止重复订阅\n",[20,1911,1912],{"class":243,"line":782},[20,1913,1459],{},[20,1915,1916],{"class":243,"line":920},[20,1917,1464],{},[20,1919,1920],{"class":243,"line":926},[20,1921,1484],{},[20,1923,1924],{"class":243,"line":932},[20,1925,1489],{},[20,1927,1928],{"class":243,"line":938},[20,1929,975],{},[20,1931,1932],{"class":243,"line":943},[20,1933,1935],{"emptyLinePlaceholder":1934},true,"\n",[20,1937,1938],{"class":243,"line":949},[20,1939,1940],{},"\u002F\u002F ✅ 正确：及时取消订阅\n",[20,1942,1943],{"class":243,"line":955},[20,1944,1530],{},[20,1946,1947],{"class":243,"line":960},[20,1948,902],{},[20,1950,1951],{"class":243,"line":966},[20,1952,1953],{},"    if (_isScrolling) \u002F\u002F 防止重复取消订阅\n",[20,1955,1956],{"class":243,"line":972},[20,1957,1459],{},[20,1959,1960],{"class":243,"line":1096},[20,1961,1548],{},[20,1963,1964],{"class":243,"line":1102},[20,1965,1572],{},[20,1967,1968],{"class":243,"line":1108},[20,1969,1489],{},[20,1971,1972],{"class":243,"line":1114},[20,1973,975],{},[1240,1975,1976],{"id":1976},"避免的问题",[232,1978,1980],{"className":234,"code":1979,"language":236,"meta":237,"style":237},"\u002F\u002F ❌ 错误：忘记取消订阅\nprivate void StartInertialScroll()\n{\n    CompositionTarget.Rendering += OnRendering;\n    \u002F\u002F 没有对应的取消订阅，导致内存泄漏和性能问题\n}\n\n\u002F\u002F ❌ 错误：重复订阅\nprivate void StartInertialScroll()\n{\n    CompositionTarget.Rendering += OnRendering;\n    CompositionTarget.Rendering += OnRendering; \u002F\u002F 订阅了两次！\n    \u002F\u002F 导致 OnRendering 被调用两次，浪费资源\n}\n",[239,1981,1982,1987,1991,1995,2000,2005,2009,2013,2018,2022,2026,2030,2035,2040],{"__ignoreMap":237},[20,1983,1984],{"class":243,"line":244},[20,1985,1986],{},"\u002F\u002F ❌ 错误：忘记取消订阅\n",[20,1988,1989],{"class":243,"line":250},[20,1990,1445],{},[20,1992,1993],{"class":243,"line":770},[20,1994,902],{},[20,1996,1997],{"class":243,"line":776},[20,1998,1999],{},"    CompositionTarget.Rendering += OnRendering;\n",[20,2001,2002],{"class":243,"line":782},[20,2003,2004],{},"    \u002F\u002F 没有对应的取消订阅，导致内存泄漏和性能问题\n",[20,2006,2007],{"class":243,"line":920},[20,2008,975],{},[20,2010,2011],{"class":243,"line":926},[20,2012,1935],{"emptyLinePlaceholder":1934},[20,2014,2015],{"class":243,"line":932},[20,2016,2017],{},"\u002F\u002F ❌ 错误：重复订阅\n",[20,2019,2020],{"class":243,"line":938},[20,2021,1445],{},[20,2023,2024],{"class":243,"line":943},[20,2025,902],{},[20,2027,2028],{"class":243,"line":949},[20,2029,1999],{},[20,2031,2032],{"class":243,"line":955},[20,2033,2034],{},"    CompositionTarget.Rendering += OnRendering; \u002F\u002F 订阅了两次！\n",[20,2036,2037],{"class":243,"line":960},[20,2038,2039],{},"    \u002F\u002F 导致 OnRendering 被调用两次，浪费资源\n",[20,2041,2042],{"class":243,"line":966},[20,2043,975],{},[14,2045,2046],{},[17,2047,2048,2051,2052,2054],{},[20,2049,2050],{},"!success"," 总结\n",[239,2053,424],{}," 是 WPF 渲染管道的核心事件，它在每帧渲染前触发，让我们可以在 UI 线程上执行动画更新。通过订阅这个事件，我们的物理计算和滚动逻辑会在每帧被调用，实现流畅的惯性滚动效果。",[38,2056,2058],{"id":2057},"_5-核心物理更新循环","5. 核心物理更新循环",[17,2060,2061],{},"这是整个系统的核心，每帧都会执行：",[232,2063,2065],{"className":234,"code":2064,"language":236,"meta":237,"style":237},"private void OnRendering(object? sender, EventArgs e)\n{\n    if (!_isScrolling) return;\n    \n    long currentTime = Stopwatch.GetTimestamp();\n    \n    \u002F\u002F 1. 计算实际时间差\n    double deltaTime = (currentTime - _lastAnimationTime) \u002F (double)Stopwatch.Frequency;\n    _lastAnimationTime = currentTime;\n    \n    \u002F\u002F 限制异常大的时间步长（防止卡顿）\n    if (deltaTime > 0.1) deltaTime = 0.1;\n    if (deltaTime \u003C 0.0001) return;\n    \n    \u002F\u002F 2. 获取当前滚动位置\n    double currentX = HorizontalOffset;\n    double currentY = VerticalOffset;\n    \n    \u002F\u002F 3. 计算新位置（根据速度和时间步长）\n    double newX = currentX + _velocityX * deltaTime;\n    double newY = currentY + _velocityY * deltaTime;\n    \n    \u002F\u002F 4. 边界检测和反弹\n    bool hitBoundaryX = false;\n    bool hitBoundaryY = false;\n    \n    if (newX \u003C 0)\n    {\n        newX = 0;\n        hitBoundaryX = true;\n    }\n    else if (newX > ScrollableWidth)\n    {\n        newX = ScrollableWidth;\n        hitBoundaryX = true;\n    }\n    \n    if (newY \u003C 0)\n    {\n        newY = 0;\n        hitBoundaryY = true;\n    }\n    else if (newY > ScrollableHeight)\n    {\n        newY = ScrollableHeight;\n        hitBoundaryY = true;\n    }\n    \n    \u002F\u002F 5. 应用边界反弹（弹性效果）\n    if (hitBoundaryX)\n    {\n        _velocityX = -_velocityX * SpringStiffness;\n    }\n    \n    if (hitBoundaryY)\n    {\n        _velocityY = -_velocityY * SpringStiffness;\n    }\n    \n    \u002F\u002F 6. 应用阻尼（速度衰减）\n    _velocityX *= DampingCoefficient;\n    _velocityY *= DampingCoefficient;\n    \n    \u002F\u002F 7. 更新滚动位置\n    ScrollToHorizontalOffset(newX);\n    ScrollToVerticalOffset(newY);\n    \n    \u002F\u002F 8. 检查是否应该停止滚动\n    if (Math.Abs(_velocityX) \u003C MinVelocity && Math.Abs(_velocityY) \u003C MinVelocity)\n    {\n        StopInertialScroll();\n    }\n}\n",[239,2066,2067,2071,2075,2080,2084,2088,2092,2097,2101,2105,2109,2114,2119,2124,2128,2133,2138,2143,2147,2152,2157,2162,2166,2171,2176,2181,2185,2190,2194,2199,2204,2208,2213,2217,2222,2227,2232,2237,2243,2248,2254,2260,2265,2271,2276,2282,2287,2292,2297,2303,2309,2314,2320,2325,2330,2336,2341,2347,2352,2357,2363,2369,2375,2380,2386,2391,2396,2401,2407,2412,2417,2423,2428],{"__ignoreMap":237},[20,2068,2069],{"class":243,"line":244},[20,2070,1750],{},[20,2072,2073],{"class":243,"line":250},[20,2074,902],{},[20,2076,2077],{"class":243,"line":770},[20,2078,2079],{},"    if (!_isScrolling) return;\n",[20,2081,2082],{"class":243,"line":776},[20,2083,923],{},[20,2085,2086],{"class":243,"line":782},[20,2087,1051],{},[20,2089,2090],{"class":243,"line":920},[20,2091,923],{},[20,2093,2094],{"class":243,"line":926},[20,2095,2096],{},"    \u002F\u002F 1. 计算实际时间差\n",[20,2098,2099],{"class":243,"line":932},[20,2100,1777],{},[20,2102,2103],{"class":243,"line":938},[20,2104,1782],{},[20,2106,2107],{"class":243,"line":943},[20,2108,923],{},[20,2110,2111],{"class":243,"line":949},[20,2112,2113],{},"    \u002F\u002F 限制异常大的时间步长（防止卡顿）\n",[20,2115,2116],{"class":243,"line":955},[20,2117,2118],{},"    if (deltaTime > 0.1) deltaTime = 0.1;\n",[20,2120,2121],{"class":243,"line":960},[20,2122,2123],{},"    if (deltaTime \u003C 0.0001) return;\n",[20,2125,2126],{"class":243,"line":966},[20,2127,923],{},[20,2129,2130],{"class":243,"line":972},[20,2131,2132],{},"    \u002F\u002F 2. 获取当前滚动位置\n",[20,2134,2135],{"class":243,"line":1096},[20,2136,2137],{},"    double currentX = HorizontalOffset;\n",[20,2139,2140],{"class":243,"line":1102},[20,2141,2142],{},"    double currentY = VerticalOffset;\n",[20,2144,2145],{"class":243,"line":1108},[20,2146,923],{},[20,2148,2149],{"class":243,"line":1114},[20,2150,2151],{},"    \u002F\u002F 3. 计算新位置（根据速度和时间步长）\n",[20,2153,2154],{"class":243,"line":1119},[20,2155,2156],{},"    double newX = currentX + _velocityX * deltaTime;\n",[20,2158,2159],{"class":243,"line":1125},[20,2160,2161],{},"    double newY = currentY + _velocityY * deltaTime;\n",[20,2163,2164],{"class":243,"line":1131},[20,2165,923],{},[20,2167,2168],{"class":243,"line":1137},[20,2169,2170],{},"    \u002F\u002F 4. 边界检测和反弹\n",[20,2172,2173],{"class":243,"line":1142},[20,2174,2175],{},"    bool hitBoundaryX = false;\n",[20,2177,2178],{"class":243,"line":1148},[20,2179,2180],{},"    bool hitBoundaryY = false;\n",[20,2182,2183],{"class":243,"line":1154},[20,2184,923],{},[20,2186,2187],{"class":243,"line":1160},[20,2188,2189],{},"    if (newX \u003C 0)\n",[20,2191,2192],{"class":243,"line":1165},[20,2193,1459],{},[20,2195,2196],{"class":243,"line":1171},[20,2197,2198],{},"        newX = 0;\n",[20,2200,2201],{"class":243,"line":1177},[20,2202,2203],{},"        hitBoundaryX = true;\n",[20,2205,2206],{"class":243,"line":1182},[20,2207,1489],{},[20,2209,2210],{"class":243,"line":1188},[20,2211,2212],{},"    else if (newX > ScrollableWidth)\n",[20,2214,2215],{"class":243,"line":1194},[20,2216,1459],{},[20,2218,2219],{"class":243,"line":1200},[20,2220,2221],{},"        newX = ScrollableWidth;\n",[20,2223,2225],{"class":243,"line":2224},35,[20,2226,2203],{},[20,2228,2230],{"class":243,"line":2229},36,[20,2231,1489],{},[20,2233,2235],{"class":243,"line":2234},37,[20,2236,923],{},[20,2238,2240],{"class":243,"line":2239},38,[20,2241,2242],{},"    if (newY \u003C 0)\n",[20,2244,2246],{"class":243,"line":2245},39,[20,2247,1459],{},[20,2249,2251],{"class":243,"line":2250},40,[20,2252,2253],{},"        newY = 0;\n",[20,2255,2257],{"class":243,"line":2256},41,[20,2258,2259],{},"        hitBoundaryY = true;\n",[20,2261,2263],{"class":243,"line":2262},42,[20,2264,1489],{},[20,2266,2268],{"class":243,"line":2267},43,[20,2269,2270],{},"    else if (newY > ScrollableHeight)\n",[20,2272,2274],{"class":243,"line":2273},44,[20,2275,1459],{},[20,2277,2279],{"class":243,"line":2278},45,[20,2280,2281],{},"        newY = ScrollableHeight;\n",[20,2283,2285],{"class":243,"line":2284},46,[20,2286,2259],{},[20,2288,2290],{"class":243,"line":2289},47,[20,2291,1489],{},[20,2293,2295],{"class":243,"line":2294},48,[20,2296,923],{},[20,2298,2300],{"class":243,"line":2299},49,[20,2301,2302],{},"    \u002F\u002F 5. 应用边界反弹（弹性效果）\n",[20,2304,2306],{"class":243,"line":2305},50,[20,2307,2308],{},"    if (hitBoundaryX)\n",[20,2310,2312],{"class":243,"line":2311},51,[20,2313,1459],{},[20,2315,2317],{"class":243,"line":2316},52,[20,2318,2319],{},"        _velocityX = -_velocityX * SpringStiffness;\n",[20,2321,2323],{"class":243,"line":2322},53,[20,2324,1489],{},[20,2326,2328],{"class":243,"line":2327},54,[20,2329,923],{},[20,2331,2333],{"class":243,"line":2332},55,[20,2334,2335],{},"    if (hitBoundaryY)\n",[20,2337,2339],{"class":243,"line":2338},56,[20,2340,1459],{},[20,2342,2344],{"class":243,"line":2343},57,[20,2345,2346],{},"        _velocityY = -_velocityY * SpringStiffness;\n",[20,2348,2350],{"class":243,"line":2349},58,[20,2351,1489],{},[20,2353,2355],{"class":243,"line":2354},59,[20,2356,923],{},[20,2358,2360],{"class":243,"line":2359},60,[20,2361,2362],{},"    \u002F\u002F 6. 应用阻尼（速度衰减）\n",[20,2364,2366],{"class":243,"line":2365},61,[20,2367,2368],{},"    _velocityX *= DampingCoefficient;\n",[20,2370,2372],{"class":243,"line":2371},62,[20,2373,2374],{},"    _velocityY *= DampingCoefficient;\n",[20,2376,2378],{"class":243,"line":2377},63,[20,2379,923],{},[20,2381,2383],{"class":243,"line":2382},64,[20,2384,2385],{},"    \u002F\u002F 7. 更新滚动位置\n",[20,2387,2389],{"class":243,"line":2388},65,[20,2390,1810],{},[20,2392,2394],{"class":243,"line":2393},66,[20,2395,1815],{},[20,2397,2399],{"class":243,"line":2398},67,[20,2400,923],{},[20,2402,2404],{"class":243,"line":2403},68,[20,2405,2406],{},"    \u002F\u002F 8. 检查是否应该停止滚动\n",[20,2408,2410],{"class":243,"line":2409},69,[20,2411,1829],{},[20,2413,2415],{"class":243,"line":2414},70,[20,2416,1459],{},[20,2418,2420],{"class":243,"line":2419},71,[20,2421,2422],{},"        StopInertialScroll();\n",[20,2424,2426],{"class":243,"line":2425},72,[20,2427,1489],{},[20,2429,2431],{"class":243,"line":2430},73,[20,2432,975],{},[147,2434,2435],{"id":2435},"更新流程详解",[232,2437,2439],{"className":1245,"code":2438,"language":1247,"meta":237,"style":237},"graph TD\n    A[开始帧更新] --> B[计算时间差 Δt]\n    B --> C[计算新位置: x = x + v × Δt]\n    C --> D{是否到达边界?}\n    D -->|是| E[应用反弹: v = -v × SpringStiffness]\n    D -->|否| F[应用阻尼: v = v × DampingCoefficient]\n    E --> F\n    F --> G[更新滚动位置]\n    G --> H{速度 \u003C MinVelocity?}\n    H -->|是| I[停止滚动]\n    H -->|否| A\n",[239,2440,2441,2445,2450,2455,2460,2465,2470,2475,2480,2485,2490],{"__ignoreMap":237},[20,2442,2443],{"class":243,"line":244},[20,2444,1254],{},[20,2446,2447],{"class":243,"line":250},[20,2448,2449],{},"    A[开始帧更新] --> B[计算时间差 Δt]\n",[20,2451,2452],{"class":243,"line":770},[20,2453,2454],{},"    B --> C[计算新位置: x = x + v × Δt]\n",[20,2456,2457],{"class":243,"line":776},[20,2458,2459],{},"    C --> D{是否到达边界?}\n",[20,2461,2462],{"class":243,"line":782},[20,2463,2464],{},"    D -->|是| E[应用反弹: v = -v × SpringStiffness]\n",[20,2466,2467],{"class":243,"line":920},[20,2468,2469],{},"    D -->|否| F[应用阻尼: v = v × DampingCoefficient]\n",[20,2471,2472],{"class":243,"line":926},[20,2473,2474],{},"    E --> F\n",[20,2476,2477],{"class":243,"line":932},[20,2478,2479],{},"    F --> G[更新滚动位置]\n",[20,2481,2482],{"class":243,"line":938},[20,2483,2484],{},"    G --> H{速度 \u003C MinVelocity?}\n",[20,2486,2487],{"class":243,"line":943},[20,2488,2489],{},"    H -->|是| I[停止滚动]\n",[20,2491,2492],{"class":243,"line":949},[20,2493,2494],{},"    H -->|否| A\n",[30,2496],{},[33,2498,2500],{"id":2499},"关键设计决策","🎯 关键设计决策",[38,2502,2504],{"id":2503},"_1-为什么移除速度平滑处理","1. 为什么移除速度平滑处理？",[17,2506,2507,2510],{},[25,2508,2509],{},"原始实现","（会限制速度）：",[232,2512,2514],{"className":234,"code":2513,"language":236,"meta":237,"style":237},"_velocityX = _velocityX * 0.8 + _lastDeltaX * 0.2; \u002F\u002F 平滑处理\n",[239,2515,2516],{"__ignoreMap":237},[20,2517,2518],{"class":243,"line":244},[20,2519,2513],{},[17,2521,2522,2525],{},[25,2523,2524],{},"优化后","（不限制速度）：",[232,2527,2529],{"className":234,"code":2528,"language":236,"meta":237,"style":237},"_velocityX = rawVelocityX; \u002F\u002F 直接使用计算的速度\n",[239,2530,2531],{"__ignoreMap":237},[20,2532,2533],{"class":243,"line":244},[20,2534,2528],{},[14,2536,2537],{},[17,2538,2539,2541],{},[20,2540,1411],{}," 原因\n平滑处理会限制速度的快速变化，导致快速拖拽时速度无法达到预期值。移除平滑处理后，速度可以更快，响应更灵敏。",[38,2543,2545],{"id":2544},"_2-时间步长限制策略","2. 时间步长限制策略",[232,2547,2549],{"className":234,"code":2548,"language":236,"meta":237,"style":237},"\u002F\u002F 只限制异常大的时间步长（防止卡顿），但不限制正常的速度\nif (deltaTime > 0.1) deltaTime = 0.1; \u002F\u002F 允许更大的时间步长\nif (deltaTime \u003C 0.0001) return; \u002F\u002F 跳过过小的步长\n",[239,2550,2551,2556,2561],{"__ignoreMap":237},[20,2552,2553],{"class":243,"line":244},[20,2554,2555],{},"\u002F\u002F 只限制异常大的时间步长（防止卡顿），但不限制正常的速度\n",[20,2557,2558],{"class":243,"line":250},[20,2559,2560],{},"if (deltaTime > 0.1) deltaTime = 0.1; \u002F\u002F 允许更大的时间步长\n",[20,2562,2563],{"class":243,"line":770},[20,2564,2565],{},"if (deltaTime \u003C 0.0001) return; \u002F\u002F 跳过过小的步长\n",[14,2567,2568,2573],{},[17,2569,2570,2572],{},[20,2571,507],{}," 设计思路",[161,2574,2575,2581],{},[155,2576,2577,2580],{},[25,2578,2579],{},"上限 (0.1s)","：防止卡顿导致的异常大值破坏物理模拟",[155,2582,2583,2586],{},[25,2584,2585],{},"下限 (0.0001s)","：跳过过小的步长，避免无效计算",[38,2588,2590],{"id":2589},"_3-边界处理策略","3. 边界处理策略",[17,2592,2593],{},"当滚动到达边界时，我们有两个选择：",[152,2595,2596,2601],{},[155,2597,2598,2600],{},[25,2599,138],{},"：速度直接设为 0",[155,2602,2603,2606],{},[25,2604,2605],{},"弹性反弹","：速度反向并衰减（当前实现）",[17,2608,2609],{},"我们选择弹性反弹，因为：",[161,2611,2612,2615,2618],{},[155,2613,2614],{},"更自然的交互体验",[155,2616,2617],{},"符合物理直觉",[155,2619,2620],{},"提供视觉反馈",[30,2622],{},[33,2624,2626],{"id":2625},"性能优化","📊 性能优化",[38,2628,2630],{"id":2629},"_1-使用高精度时间戳","1. 使用高精度时间戳",[161,2632,2633,2636,2639],{},[155,2634,2635],{},"✅ 自动适配不同帧率",[155,2637,2638],{},"✅ 处理卡顿情况",[155,2640,2641],{},"✅ 更准确的物理计算",[38,2643,2645],{"id":2644},"_2-compositiontargetrendering-vs-dispatchertimer","2. CompositionTarget.Rendering vs DispatcherTimer",[42,2647,2648,2658],{},[45,2649,2650],{},[48,2651,2652,2654,2656],{},[51,2653,53],{},[51,2655,424],{},[51,2657,834],{},[61,2659,2660,2669,2679,2688],{},[48,2661,2662,2664,2666],{},[66,2663,1661],{},[66,2665,1332],{},[66,2667,2668],{},"固定间隔",[48,2670,2671,2673,2676],{},[66,2672,1648],{},[66,2674,2675],{},"自动适配",[66,2677,2678],{},"固定 60 FPS",[48,2680,2681,2683,2686],{},[66,2682,1674],{},[66,2684,2685],{},"更平滑",[66,2687,1680],{},[48,2689,2690,2692,2695],{},[66,2691,1687],{},[66,2693,2694],{},"更好",[66,2696,2697],{},"一般",[38,2699,2701],{"id":2700},"_3-避免不必要的计算","3. 避免不必要的计算",[161,2703,2704,2710,2713],{},[155,2705,2706,2707,458],{},"只在滚动时更新（",[239,2708,2709],{},"if (!_isScrolling) return;",[155,2711,2712],{},"跳过过小的时间步长",[155,2714,2715],{},"及时停止滚动（速度低于阈值时）",[30,2717],{},[33,2719,2721],{"id":2720},"调试技巧","🔍 调试技巧",[38,2723,2725],{"id":2724},"_1-调整物理参数","1. 调整物理参数",[17,2727,2728],{},"如果滚动太快或太慢，可以调整：",[232,2730,2732],{"className":234,"code":2731,"language":236,"meta":237,"style":237},"private const double DampingCoefficient = 0.96;  \u002F\u002F 增大 = 滚动更持久\nprivate const double MinVelocity = 0.1;         \u002F\u002F 降低 = 滚动持续更久\nprivate const double VelocityMultiplier = 16.0;  \u002F\u002F 调整整体速度\n",[239,2733,2734,2739,2744],{"__ignoreMap":237},[20,2735,2736],{"class":243,"line":244},[20,2737,2738],{},"private const double DampingCoefficient = 0.96;  \u002F\u002F 增大 = 滚动更持久\n",[20,2740,2741],{"class":243,"line":250},[20,2742,2743],{},"private const double MinVelocity = 0.1;         \u002F\u002F 降低 = 滚动持续更久\n",[20,2745,2746],{"class":243,"line":770},[20,2747,2748],{},"private const double VelocityMultiplier = 16.0;  \u002F\u002F 调整整体速度\n",[38,2750,2752],{"id":2751},"_2-监控性能","2. 监控性能",[17,2754,2755],{},"可以添加性能计数器来监控：",[232,2757,2759],{"className":234,"code":2758,"language":236,"meta":237,"style":237},"private int _frameCount = 0;\nprivate long _lastFpsTime = 0;\n\nprivate void OnRendering(object? sender, EventArgs e)\n{\n    _frameCount++;\n    long currentTime = Stopwatch.GetTimestamp();\n    if ((currentTime - _lastFpsTime) \u002F (double)Stopwatch.Frequency > 1.0)\n    {\n        double fps = _frameCount;\n        Debug.WriteLine($\"FPS: {fps}\");\n        _frameCount = 0;\n        _lastFpsTime = currentTime;\n    }\n    \u002F\u002F ... 其他代码\n}\n",[239,2760,2761,2766,2771,2775,2779,2783,2788,2792,2797,2801,2806,2811,2816,2821,2825,2830],{"__ignoreMap":237},[20,2762,2763],{"class":243,"line":244},[20,2764,2765],{},"private int _frameCount = 0;\n",[20,2767,2768],{"class":243,"line":250},[20,2769,2770],{},"private long _lastFpsTime = 0;\n",[20,2772,2773],{"class":243,"line":770},[20,2774,1935],{"emptyLinePlaceholder":1934},[20,2776,2777],{"class":243,"line":776},[20,2778,1750],{},[20,2780,2781],{"class":243,"line":782},[20,2782,902],{},[20,2784,2785],{"class":243,"line":920},[20,2786,2787],{},"    _frameCount++;\n",[20,2789,2790],{"class":243,"line":926},[20,2791,1051],{},[20,2793,2794],{"class":243,"line":932},[20,2795,2796],{},"    if ((currentTime - _lastFpsTime) \u002F (double)Stopwatch.Frequency > 1.0)\n",[20,2798,2799],{"class":243,"line":938},[20,2800,1459],{},[20,2802,2803],{"class":243,"line":943},[20,2804,2805],{},"        double fps = _frameCount;\n",[20,2807,2808],{"class":243,"line":949},[20,2809,2810],{},"        Debug.WriteLine($\"FPS: {fps}\");\n",[20,2812,2813],{"class":243,"line":955},[20,2814,2815],{},"        _frameCount = 0;\n",[20,2817,2818],{"class":243,"line":960},[20,2819,2820],{},"        _lastFpsTime = currentTime;\n",[20,2822,2823],{"class":243,"line":966},[20,2824,1489],{},[20,2826,2827],{"class":243,"line":972},[20,2828,2829],{},"    \u002F\u002F ... 其他代码\n",[20,2831,2832],{"class":243,"line":1096},[20,2833,975],{},[30,2835],{},[33,2837,2839],{"id":2838},"完整代码结构","📝 完整代码结构",[232,2841,2844],{"className":2842,"code":2843,"language":565},[563],"FluentScrollViewer\n├── 物理参数（常量）\n│   ├── DampingCoefficient\n│   ├── MinVelocity\n│   ├── SpringStiffness\n│   └── VelocityMultiplier\n├── 状态变量\n│   ├── _velocityX, _velocityY\n│   ├── _isScrolling\n│   ├── _isDragging\n│   └── 时间戳变量\n├── 事件处理\n│   ├── OnPreviewMouseWheel（滚轮）\n│   ├── OnMouseLeftButtonDown（按下）\n│   ├── OnMouseMove（移动）\n│   ├── OnMouseLeftButtonUp（释放）\n│   └── OnMouseLeave（离开）\n└── 核心逻辑\n    ├── StartInertialScroll（启动）\n    ├── StopInertialScroll（停止）\n    └── OnRendering（物理更新）\n",[239,2845,2843],{"__ignoreMap":237},[232,2847,2851],{"className":2848,"code":2849,"language":2850,"meta":237,"style":237},"language-cs shiki shiki-themes github-light github-dark","using System;  \nusing System.Diagnostics;  \nusing System.Windows;  \nusing System.Windows.Controls;  \nusing System.Windows.Input;  \nusing System.Windows.Media;  \nusing System.Windows.Threading;  \n  \nnamespace FluentScrollviewerSolution;  \n  \n\u002F\u002F\u002F \u003Csummary>  \n\u002F\u002F\u002F 带有惯性滚动的自定义 ScrollViewer\u002F\u002F\u002F 使用二阶缓动物理系统实现平滑的惯性滚动效果  \n\u002F\u002F\u002F \u003C\u002Fsummary>  \npublic class FluentScrollViewer : ScrollViewer  \n{  \n    \u002F\u002F 物理参数  \n    private const double DampingCoefficient = 0.96; \u002F\u002F 阻尼系数，控制速度衰减速度（0-1，越小衰减越快，增大让滚动更持久）  \n    private const double MinVelocity = 0.1; \u002F\u002F 最小速度阈值，低于此值停止滚动（降低让滚动持续更久）  \n    private const double SpringStiffness = 0.25; \u002F\u002F 弹性系数，控制边界反弹强度  \n    private const double VelocityMultiplier = 16.0; \u002F\u002F 速度倍数，让惯性滚动更快  \n    \u002F\u002F 滚动状态  \n    private double _velocityX;  \n    private double _velocityY;  \n    private bool _isScrolling;  \n    private long _lastAnimationTime; \u002F\u002F 使用高精度时间戳  \n    \u002F\u002F 鼠标\u002F触摸状态  \n    private Point _lastMousePosition;  \n    private bool _isDragging;  \n    private long _lastUpdateTime; \u002F\u002F 使用高精度时间戳  \n    private double _lastDeltaX;  \n    private double _lastDeltaY;  \n    static FluentScrollViewer()  \n    {        DefaultStyleKeyProperty.OverrideMetadata(typeof(FluentScrollViewer),   \nnew FrameworkPropertyMetadata(typeof(FluentScrollViewer)));  \n    }    public FluentScrollViewer()  \n    {        \u002F\u002F 禁用默认的滚动行为  \n        this.PreviewMouseWheel += OnPreviewMouseWheel;  \n        this.MouseLeftButtonDown += OnMouseLeftButtonDown;  \n        this.MouseMove += OnMouseMove;  \n        this.MouseLeftButtonUp += OnMouseLeftButtonUp;  \n        this.MouseLeave += OnMouseLeave;  \n        \u002F\u002F 初始化高精度时间戳  \n        _lastAnimationTime = Stopwatch.GetTimestamp();  \n        _lastUpdateTime = Stopwatch.GetTimestamp();  \n    }    \u002F\u002F\u002F \u003Csummary>  \n    \u002F\u002F\u002F 拦截鼠标滚轮事件，转换为惯性滚动  \n    \u002F\u002F\u002F \u003C\u002Fsummary>  \n    private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)  \n    {        \u002F\u002F 计算初始速度（基于滚轮增量）  \n        double delta = e.Delta;  \n        double velocity = delta * 0.5 * VelocityMultiplier; \u002F\u002F 调整速度系数，应用速度倍数  \n        if (e.Delta > 0)  \n        {            \u002F\u002F 向上滚动  \n            _velocityY = -Math.Abs(velocity);  \n        }        else  \n        {  \n            \u002F\u002F 向下滚动  \n            _velocityY = Math.Abs(velocity);  \n        }        \u002F\u002F 启动惯性滚动  \n        StartInertialScroll();  \n        \u002F\u002F 标记事件已处理，阻止默认滚动  \n        e.Handled = true;  \n    }    \u002F\u002F\u002F \u003Csummary>  \n    \u002F\u002F\u002F 鼠标按下，开始拖拽  \n    \u002F\u002F\u002F \u003C\u002Fsummary>  \n    private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)  \n    {        if (e.ChangedButton != MouseButton.Left) return;  \n        _isDragging = true;  \n        _lastMousePosition = e.GetPosition(this);  \n        _lastUpdateTime = Stopwatch.GetTimestamp();  \n        _lastDeltaX = 0;  \n        _lastDeltaY = 0;  \n        \u002F\u002F 停止惯性滚动  \n        StopInertialScroll();  \n        this.CaptureMouse();  \n        e.Handled = true;  \n    }    \u002F\u002F\u002F \u003Csummary>  \n    \u002F\u002F\u002F 鼠标移动，计算拖拽速度  \n    \u002F\u002F\u002F \u003C\u002Fsummary>  \n    private void OnMouseMove(object sender, MouseEventArgs e)  \n    {        if (!_isDragging) return;  \n        Point currentPosition = e.GetPosition(this);  \n        long currentTime = Stopwatch.GetTimestamp();  \n        \u002F\u002F 计算时间差（秒），使用高精度时间戳  \n        double deltaTime = (currentTime - _lastUpdateTime) \u002F (double)Stopwatch.Frequency;  \n        if (deltaTime \u003C 0.0001) deltaTime = 0.0001; \u002F\u002F 防止除零，使用更小的最小值  \n        \u002F\u002F 计算位移  \n        double deltaX = currentPosition.X - _lastMousePosition.X;  \n        double deltaY = currentPosition.Y - _lastMousePosition.Y;  \n        \u002F\u002F 计算速度（像素\u002F秒），应用速度倍数让惯性滚动更快，不限制速度  \n        double rawVelocityX = (deltaX \u002F deltaTime) * VelocityMultiplier;  \n        double rawVelocityY = (deltaY \u002F deltaTime) * VelocityMultiplier;  \n        \u002F\u002F 直接使用计算的速度，不进行平滑处理，让速度可以更快  \n        _velocityX = rawVelocityX;  \n        _velocityY = rawVelocityY;  \n        \u002F\u002F 直接移动滚动位置  \n        double newOffsetX = HorizontalOffset - deltaX;  \n        double newOffsetY = VerticalOffset - deltaY;  \n        ScrollToHorizontalOffset(Math.Max(0, Math.Min(ScrollableWidth, newOffsetX)));  \n        ScrollToVerticalOffset(Math.Max(0, Math.Min(ScrollableHeight, newOffsetY)));  \n        \u002F\u002F 更新状态  \n        _lastMousePosition = currentPosition;  \n        _lastUpdateTime = currentTime;  \n        _lastDeltaX = _velocityX;  \n        _lastDeltaY = _velocityY;  \n    }    \u002F\u002F\u002F \u003Csummary>  \n    \u002F\u002F\u002F 鼠标释放，开始惯性滚动  \n    \u002F\u002F\u002F \u003C\u002Fsummary>  \n    private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)  \n    {        if (!_isDragging) return;  \n        _isDragging = false;  \n        this.ReleaseMouseCapture();  \n        \u002F\u002F 如果速度足够大，启动惯性滚动  \n        if (Math.Abs(_velocityX) > MinVelocity || Math.Abs(_velocityY) > MinVelocity)  \n        {            StartInertialScroll();  \n        }        e.Handled = true;  \n    }    \u002F\u002F\u002F \u003Csummary>  \n    \u002F\u002F\u002F 鼠标离开，停止拖拽  \n    \u002F\u002F\u002F \u003C\u002Fsummary>  \n    private void OnMouseLeave(object sender, MouseEventArgs e)  \n    {        if (_isDragging)  \n        {            _isDragging = false;  \n            this.ReleaseMouseCapture();  \n            \u002F\u002F 如果速度足够大，启动惯性滚动  \n            if (Math.Abs(_velocityX) > MinVelocity || Math.Abs(_velocityY) > MinVelocity)  \n            {                StartInertialScroll();  \n            }        }    }    \u002F\u002F\u002F \u003Csummary>  \n    \u002F\u002F\u002F 启动惯性滚动  \n    \u002F\u002F\u002F \u003C\u002Fsummary>  \n    private void StartInertialScroll()  \n    {        if (!_isScrolling)  \n        {            _isScrolling = true;  \n            _lastAnimationTime = Stopwatch.GetTimestamp();  \n            CompositionTarget.Rendering += OnRendering;  \n        }    }    \u002F\u002F\u002F \u003Csummary>  \n    \u002F\u002F\u002F 停止惯性滚动  \n    \u002F\u002F\u002F \u003C\u002Fsummary>  \n    private void StopInertialScroll()  \n    {        if (_isScrolling)  \n        {            _isScrolling = false;  \n            _velocityX = 0;  \n            _velocityY = 0;  \n            CompositionTarget.Rendering -= OnRendering;  \n        }    }    \u002F\u002F\u002F \u003Csummary>  \n    \u002F\u002F\u002F 渲染帧更新，实现二阶缓动物理系统（使用CompositionTarget.Rendering获得更平滑的动画）  \n    \u002F\u002F\u002F \u003C\u002Fsummary>  \n    private void OnRendering(object? sender, EventArgs e)  \n    {        if (!_isScrolling) return;  \n        long currentTime = Stopwatch.GetTimestamp();  \n        \u002F\u002F 计算实际时间差（秒），使用高精度时间戳  \n        double deltaTime = (currentTime - _lastAnimationTime) \u002F (double)Stopwatch.Frequency;  \n        _lastAnimationTime = currentTime;  \n        \u002F\u002F 只限制异常大的时间步长（防止卡顿），但不限制正常的速度  \n        if (deltaTime > 0.1) deltaTime = 0.1; \u002F\u002F 允许更大的时间步长，不强制限制为16ms  \n        if (deltaTime \u003C 0.0001) return; \u002F\u002F 跳过过小的步长  \n        \u002F\u002F 获取当前滚动位置  \n        double currentX = HorizontalOffset;  \n        double currentY = VerticalOffset;  \n        \u002F\u002F 计算新位置  \n        double newX = currentX + _velocityX * deltaTime;  \n        double newY = currentY + _velocityY * deltaTime;  \n        \u002F\u002F 边界检测和反弹  \n        bool hitBoundaryX = false;  \n        bool hitBoundaryY = false;  \n        if (newX \u003C 0)  \n        {            newX = 0;  \n            hitBoundaryX = true;  \n        }        else if (newX > ScrollableWidth)  \n        {            newX = ScrollableWidth;  \n            hitBoundaryX = true;  \n        }        if (newY \u003C 0)  \n        {            newY = 0;  \n            hitBoundaryY = true;  \n        }        else if (newY > ScrollableHeight)  \n        {            newY = ScrollableHeight;  \n            hitBoundaryY = true;  \n        }        \u002F\u002F 应用边界反弹（弹性效果）  \n        if (hitBoundaryX)  \n        {            _velocityX = -_velocityX * SpringStiffness; \u002F\u002F 反弹并衰减  \n        }  \n        if (hitBoundaryY)  \n        {            _velocityY = -_velocityY * SpringStiffness; \u002F\u002F 反弹并衰减  \n        }  \n        \u002F\u002F 应用阻尼（速度衰减）  \n        _velocityX *= DampingCoefficient;  \n        _velocityY *= DampingCoefficient;  \n        \u002F\u002F 更新滚动位置  \n        ScrollToHorizontalOffset(newX);  \n        ScrollToVerticalOffset(newY);  \n        \u002F\u002F 检查是否应该停止滚动  \n        if (Math.Abs(_velocityX) \u003C MinVelocity && Math.Abs(_velocityY) \u003C MinVelocity)  \n        {            StopInertialScroll();  \n        }    }    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)  \n    {        base.OnRenderSizeChanged(sizeInfo);  \n        \u002F\u002F 如果尺寸改变时正在滚动，可能需要调整位置  \n        if (_isScrolling)  \n        {            double newX = Math.Max(0, Math.Min(ScrollableWidth, HorizontalOffset));  \n            double newY = Math.Max(0, Math.Min(ScrollableHeight, VerticalOffset));  \n            ScrollToHorizontalOffset(newX);  \n            ScrollToVerticalOffset(newY);  \n        }    }}\n","cs",[239,2852,2853,2858,2863,2868,2873,2878,2883,2888,2893,2898,2902,2907,2912,2917,2922,2927,2932,2937,2942,2947,2952,2957,2962,2967,2972,2977,2982,2987,2992,2997,3002,3007,3012,3017,3022,3027,3032,3037,3042,3047,3052,3057,3062,3067,3072,3077,3082,3087,3092,3097,3102,3107,3112,3117,3122,3127,3132,3137,3142,3147,3152,3157,3162,3166,3171,3175,3180,3185,3190,3195,3199,3204,3209,3214,3220,3226,3231,3236,3242,3247,3253,3259,3265,3271,3277,3283,3289,3295,3301,3307,3313,3319,3325,3331,3337,3343,3349,3355,3361,3367,3373,3379,3385,3391,3397,3403,3408,3414,3419,3425,3430,3436,3442,3448,3454,3460,3466,3471,3477,3482,3488,3494,3500,3506,3512,3518,3524,3530,3536,3541,3547,3553,3559,3565,3571,3577,3583,3588,3594,3600,3606,3612,3618,3624,3629,3635,3640,3646,3652,3657,3663,3669,3675,3681,3687,3693,3699,3705,3711,3717,3723,3729,3735,3741,3747,3753,3759,3765,3771,3777,3782,3788,3794,3800,3806,3812,3817,3823,3829,3835,3841,3847,3853,3858,3864,3870,3876,3882,3888,3894,3900,3906,3912,3918,3924,3930,3936,3942,3948,3954,3960],{"__ignoreMap":237},[20,2854,2855],{"class":243,"line":244},[20,2856,2857],{},"using System;  \n",[20,2859,2860],{"class":243,"line":250},[20,2861,2862],{},"using System.Diagnostics;  \n",[20,2864,2865],{"class":243,"line":770},[20,2866,2867],{},"using System.Windows;  \n",[20,2869,2870],{"class":243,"line":776},[20,2871,2872],{},"using System.Windows.Controls;  \n",[20,2874,2875],{"class":243,"line":782},[20,2876,2877],{},"using System.Windows.Input;  \n",[20,2879,2880],{"class":243,"line":920},[20,2881,2882],{},"using System.Windows.Media;  \n",[20,2884,2885],{"class":243,"line":926},[20,2886,2887],{},"using System.Windows.Threading;  \n",[20,2889,2890],{"class":243,"line":932},[20,2891,2892],{},"  \n",[20,2894,2895],{"class":243,"line":938},[20,2896,2897],{},"namespace FluentScrollviewerSolution;  \n",[20,2899,2900],{"class":243,"line":943},[20,2901,2892],{},[20,2903,2904],{"class":243,"line":949},[20,2905,2906],{},"\u002F\u002F\u002F \u003Csummary>  \n",[20,2908,2909],{"class":243,"line":955},[20,2910,2911],{},"\u002F\u002F\u002F 带有惯性滚动的自定义 ScrollViewer\u002F\u002F\u002F 使用二阶缓动物理系统实现平滑的惯性滚动效果  \n",[20,2913,2914],{"class":243,"line":960},[20,2915,2916],{},"\u002F\u002F\u002F \u003C\u002Fsummary>  \n",[20,2918,2919],{"class":243,"line":966},[20,2920,2921],{},"public class FluentScrollViewer : ScrollViewer  \n",[20,2923,2924],{"class":243,"line":972},[20,2925,2926],{},"{  \n",[20,2928,2929],{"class":243,"line":1096},[20,2930,2931],{},"    \u002F\u002F 物理参数  \n",[20,2933,2934],{"class":243,"line":1102},[20,2935,2936],{},"    private const double DampingCoefficient = 0.96; \u002F\u002F 阻尼系数，控制速度衰减速度（0-1，越小衰减越快，增大让滚动更持久）  \n",[20,2938,2939],{"class":243,"line":1108},[20,2940,2941],{},"    private const double MinVelocity = 0.1; \u002F\u002F 最小速度阈值，低于此值停止滚动（降低让滚动持续更久）  \n",[20,2943,2944],{"class":243,"line":1114},[20,2945,2946],{},"    private const double SpringStiffness = 0.25; \u002F\u002F 弹性系数，控制边界反弹强度  \n",[20,2948,2949],{"class":243,"line":1119},[20,2950,2951],{},"    private const double VelocityMultiplier = 16.0; \u002F\u002F 速度倍数，让惯性滚动更快  \n",[20,2953,2954],{"class":243,"line":1125},[20,2955,2956],{},"    \u002F\u002F 滚动状态  \n",[20,2958,2959],{"class":243,"line":1131},[20,2960,2961],{},"    private double _velocityX;  \n",[20,2963,2964],{"class":243,"line":1137},[20,2965,2966],{},"    private double _velocityY;  \n",[20,2968,2969],{"class":243,"line":1142},[20,2970,2971],{},"    private bool _isScrolling;  \n",[20,2973,2974],{"class":243,"line":1148},[20,2975,2976],{},"    private long _lastAnimationTime; \u002F\u002F 使用高精度时间戳  \n",[20,2978,2979],{"class":243,"line":1154},[20,2980,2981],{},"    \u002F\u002F 鼠标\u002F触摸状态  \n",[20,2983,2984],{"class":243,"line":1160},[20,2985,2986],{},"    private Point _lastMousePosition;  \n",[20,2988,2989],{"class":243,"line":1165},[20,2990,2991],{},"    private bool _isDragging;  \n",[20,2993,2994],{"class":243,"line":1171},[20,2995,2996],{},"    private long _lastUpdateTime; \u002F\u002F 使用高精度时间戳  \n",[20,2998,2999],{"class":243,"line":1177},[20,3000,3001],{},"    private double _lastDeltaX;  \n",[20,3003,3004],{"class":243,"line":1182},[20,3005,3006],{},"    private double _lastDeltaY;  \n",[20,3008,3009],{"class":243,"line":1188},[20,3010,3011],{},"    static FluentScrollViewer()  \n",[20,3013,3014],{"class":243,"line":1194},[20,3015,3016],{},"    {        DefaultStyleKeyProperty.OverrideMetadata(typeof(FluentScrollViewer),   \n",[20,3018,3019],{"class":243,"line":1200},[20,3020,3021],{},"new FrameworkPropertyMetadata(typeof(FluentScrollViewer)));  \n",[20,3023,3024],{"class":243,"line":2224},[20,3025,3026],{},"    }    public FluentScrollViewer()  \n",[20,3028,3029],{"class":243,"line":2229},[20,3030,3031],{},"    {        \u002F\u002F 禁用默认的滚动行为  \n",[20,3033,3034],{"class":243,"line":2234},[20,3035,3036],{},"        this.PreviewMouseWheel += OnPreviewMouseWheel;  \n",[20,3038,3039],{"class":243,"line":2239},[20,3040,3041],{},"        this.MouseLeftButtonDown += OnMouseLeftButtonDown;  \n",[20,3043,3044],{"class":243,"line":2245},[20,3045,3046],{},"        this.MouseMove += OnMouseMove;  \n",[20,3048,3049],{"class":243,"line":2250},[20,3050,3051],{},"        this.MouseLeftButtonUp += OnMouseLeftButtonUp;  \n",[20,3053,3054],{"class":243,"line":2256},[20,3055,3056],{},"        this.MouseLeave += OnMouseLeave;  \n",[20,3058,3059],{"class":243,"line":2262},[20,3060,3061],{},"        \u002F\u002F 初始化高精度时间戳  \n",[20,3063,3064],{"class":243,"line":2267},[20,3065,3066],{},"        _lastAnimationTime = Stopwatch.GetTimestamp();  \n",[20,3068,3069],{"class":243,"line":2273},[20,3070,3071],{},"        _lastUpdateTime = Stopwatch.GetTimestamp();  \n",[20,3073,3074],{"class":243,"line":2278},[20,3075,3076],{},"    }    \u002F\u002F\u002F \u003Csummary>  \n",[20,3078,3079],{"class":243,"line":2284},[20,3080,3081],{},"    \u002F\u002F\u002F 拦截鼠标滚轮事件，转换为惯性滚动  \n",[20,3083,3084],{"class":243,"line":2289},[20,3085,3086],{},"    \u002F\u002F\u002F \u003C\u002Fsummary>  \n",[20,3088,3089],{"class":243,"line":2294},[20,3090,3091],{},"    private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)  \n",[20,3093,3094],{"class":243,"line":2299},[20,3095,3096],{},"    {        \u002F\u002F 计算初始速度（基于滚轮增量）  \n",[20,3098,3099],{"class":243,"line":2305},[20,3100,3101],{},"        double delta = e.Delta;  \n",[20,3103,3104],{"class":243,"line":2311},[20,3105,3106],{},"        double velocity = delta * 0.5 * VelocityMultiplier; \u002F\u002F 调整速度系数，应用速度倍数  \n",[20,3108,3109],{"class":243,"line":2316},[20,3110,3111],{},"        if (e.Delta > 0)  \n",[20,3113,3114],{"class":243,"line":2322},[20,3115,3116],{},"        {            \u002F\u002F 向上滚动  \n",[20,3118,3119],{"class":243,"line":2327},[20,3120,3121],{},"            _velocityY = -Math.Abs(velocity);  \n",[20,3123,3124],{"class":243,"line":2332},[20,3125,3126],{},"        }        else  \n",[20,3128,3129],{"class":243,"line":2338},[20,3130,3131],{},"        {  \n",[20,3133,3134],{"class":243,"line":2343},[20,3135,3136],{},"            \u002F\u002F 向下滚动  \n",[20,3138,3139],{"class":243,"line":2349},[20,3140,3141],{},"            _velocityY = Math.Abs(velocity);  \n",[20,3143,3144],{"class":243,"line":2354},[20,3145,3146],{},"        }        \u002F\u002F 启动惯性滚动  \n",[20,3148,3149],{"class":243,"line":2359},[20,3150,3151],{},"        StartInertialScroll();  \n",[20,3153,3154],{"class":243,"line":2365},[20,3155,3156],{},"        \u002F\u002F 标记事件已处理，阻止默认滚动  \n",[20,3158,3159],{"class":243,"line":2371},[20,3160,3161],{},"        e.Handled = true;  \n",[20,3163,3164],{"class":243,"line":2377},[20,3165,3076],{},[20,3167,3168],{"class":243,"line":2382},[20,3169,3170],{},"    \u002F\u002F\u002F 鼠标按下，开始拖拽  \n",[20,3172,3173],{"class":243,"line":2388},[20,3174,3086],{},[20,3176,3177],{"class":243,"line":2393},[20,3178,3179],{},"    private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)  \n",[20,3181,3182],{"class":243,"line":2398},[20,3183,3184],{},"    {        if (e.ChangedButton != MouseButton.Left) return;  \n",[20,3186,3187],{"class":243,"line":2403},[20,3188,3189],{},"        _isDragging = true;  \n",[20,3191,3192],{"class":243,"line":2409},[20,3193,3194],{},"        _lastMousePosition = e.GetPosition(this);  \n",[20,3196,3197],{"class":243,"line":2414},[20,3198,3071],{},[20,3200,3201],{"class":243,"line":2419},[20,3202,3203],{},"        _lastDeltaX = 0;  \n",[20,3205,3206],{"class":243,"line":2425},[20,3207,3208],{},"        _lastDeltaY = 0;  \n",[20,3210,3211],{"class":243,"line":2430},[20,3212,3213],{},"        \u002F\u002F 停止惯性滚动  \n",[20,3215,3217],{"class":243,"line":3216},74,[20,3218,3219],{},"        StopInertialScroll();  \n",[20,3221,3223],{"class":243,"line":3222},75,[20,3224,3225],{},"        this.CaptureMouse();  \n",[20,3227,3229],{"class":243,"line":3228},76,[20,3230,3161],{},[20,3232,3234],{"class":243,"line":3233},77,[20,3235,3076],{},[20,3237,3239],{"class":243,"line":3238},78,[20,3240,3241],{},"    \u002F\u002F\u002F 鼠标移动，计算拖拽速度  \n",[20,3243,3245],{"class":243,"line":3244},79,[20,3246,3086],{},[20,3248,3250],{"class":243,"line":3249},80,[20,3251,3252],{},"    private void OnMouseMove(object sender, MouseEventArgs e)  \n",[20,3254,3256],{"class":243,"line":3255},81,[20,3257,3258],{},"    {        if (!_isDragging) return;  \n",[20,3260,3262],{"class":243,"line":3261},82,[20,3263,3264],{},"        Point currentPosition = e.GetPosition(this);  \n",[20,3266,3268],{"class":243,"line":3267},83,[20,3269,3270],{},"        long currentTime = Stopwatch.GetTimestamp();  \n",[20,3272,3274],{"class":243,"line":3273},84,[20,3275,3276],{},"        \u002F\u002F 计算时间差（秒），使用高精度时间戳  \n",[20,3278,3280],{"class":243,"line":3279},85,[20,3281,3282],{},"        double deltaTime = (currentTime - _lastUpdateTime) \u002F (double)Stopwatch.Frequency;  \n",[20,3284,3286],{"class":243,"line":3285},86,[20,3287,3288],{},"        if (deltaTime \u003C 0.0001) deltaTime = 0.0001; \u002F\u002F 防止除零，使用更小的最小值  \n",[20,3290,3292],{"class":243,"line":3291},87,[20,3293,3294],{},"        \u002F\u002F 计算位移  \n",[20,3296,3298],{"class":243,"line":3297},88,[20,3299,3300],{},"        double deltaX = currentPosition.X - _lastMousePosition.X;  \n",[20,3302,3304],{"class":243,"line":3303},89,[20,3305,3306],{},"        double deltaY = currentPosition.Y - _lastMousePosition.Y;  \n",[20,3308,3310],{"class":243,"line":3309},90,[20,3311,3312],{},"        \u002F\u002F 计算速度（像素\u002F秒），应用速度倍数让惯性滚动更快，不限制速度  \n",[20,3314,3316],{"class":243,"line":3315},91,[20,3317,3318],{},"        double rawVelocityX = (deltaX \u002F deltaTime) * VelocityMultiplier;  \n",[20,3320,3322],{"class":243,"line":3321},92,[20,3323,3324],{},"        double rawVelocityY = (deltaY \u002F deltaTime) * VelocityMultiplier;  \n",[20,3326,3328],{"class":243,"line":3327},93,[20,3329,3330],{},"        \u002F\u002F 直接使用计算的速度，不进行平滑处理，让速度可以更快  \n",[20,3332,3334],{"class":243,"line":3333},94,[20,3335,3336],{},"        _velocityX = rawVelocityX;  \n",[20,3338,3340],{"class":243,"line":3339},95,[20,3341,3342],{},"        _velocityY = rawVelocityY;  \n",[20,3344,3346],{"class":243,"line":3345},96,[20,3347,3348],{},"        \u002F\u002F 直接移动滚动位置  \n",[20,3350,3352],{"class":243,"line":3351},97,[20,3353,3354],{},"        double newOffsetX = HorizontalOffset - deltaX;  \n",[20,3356,3358],{"class":243,"line":3357},98,[20,3359,3360],{},"        double newOffsetY = VerticalOffset - deltaY;  \n",[20,3362,3364],{"class":243,"line":3363},99,[20,3365,3366],{},"        ScrollToHorizontalOffset(Math.Max(0, Math.Min(ScrollableWidth, newOffsetX)));  \n",[20,3368,3370],{"class":243,"line":3369},100,[20,3371,3372],{},"        ScrollToVerticalOffset(Math.Max(0, Math.Min(ScrollableHeight, newOffsetY)));  \n",[20,3374,3376],{"class":243,"line":3375},101,[20,3377,3378],{},"        \u002F\u002F 更新状态  \n",[20,3380,3382],{"class":243,"line":3381},102,[20,3383,3384],{},"        _lastMousePosition = currentPosition;  \n",[20,3386,3388],{"class":243,"line":3387},103,[20,3389,3390],{},"        _lastUpdateTime = currentTime;  \n",[20,3392,3394],{"class":243,"line":3393},104,[20,3395,3396],{},"        _lastDeltaX = _velocityX;  \n",[20,3398,3400],{"class":243,"line":3399},105,[20,3401,3402],{},"        _lastDeltaY = _velocityY;  \n",[20,3404,3406],{"class":243,"line":3405},106,[20,3407,3076],{},[20,3409,3411],{"class":243,"line":3410},107,[20,3412,3413],{},"    \u002F\u002F\u002F 鼠标释放，开始惯性滚动  \n",[20,3415,3417],{"class":243,"line":3416},108,[20,3418,3086],{},[20,3420,3422],{"class":243,"line":3421},109,[20,3423,3424],{},"    private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)  \n",[20,3426,3428],{"class":243,"line":3427},110,[20,3429,3258],{},[20,3431,3433],{"class":243,"line":3432},111,[20,3434,3435],{},"        _isDragging = false;  \n",[20,3437,3439],{"class":243,"line":3438},112,[20,3440,3441],{},"        this.ReleaseMouseCapture();  \n",[20,3443,3445],{"class":243,"line":3444},113,[20,3446,3447],{},"        \u002F\u002F 如果速度足够大，启动惯性滚动  \n",[20,3449,3451],{"class":243,"line":3450},114,[20,3452,3453],{},"        if (Math.Abs(_velocityX) > MinVelocity || Math.Abs(_velocityY) > MinVelocity)  \n",[20,3455,3457],{"class":243,"line":3456},115,[20,3458,3459],{},"        {            StartInertialScroll();  \n",[20,3461,3463],{"class":243,"line":3462},116,[20,3464,3465],{},"        }        e.Handled = true;  \n",[20,3467,3469],{"class":243,"line":3468},117,[20,3470,3076],{},[20,3472,3474],{"class":243,"line":3473},118,[20,3475,3476],{},"    \u002F\u002F\u002F 鼠标离开，停止拖拽  \n",[20,3478,3480],{"class":243,"line":3479},119,[20,3481,3086],{},[20,3483,3485],{"class":243,"line":3484},120,[20,3486,3487],{},"    private void OnMouseLeave(object sender, MouseEventArgs e)  \n",[20,3489,3491],{"class":243,"line":3490},121,[20,3492,3493],{},"    {        if (_isDragging)  \n",[20,3495,3497],{"class":243,"line":3496},122,[20,3498,3499],{},"        {            _isDragging = false;  \n",[20,3501,3503],{"class":243,"line":3502},123,[20,3504,3505],{},"            this.ReleaseMouseCapture();  \n",[20,3507,3509],{"class":243,"line":3508},124,[20,3510,3511],{},"            \u002F\u002F 如果速度足够大，启动惯性滚动  \n",[20,3513,3515],{"class":243,"line":3514},125,[20,3516,3517],{},"            if (Math.Abs(_velocityX) > MinVelocity || Math.Abs(_velocityY) > MinVelocity)  \n",[20,3519,3521],{"class":243,"line":3520},126,[20,3522,3523],{},"            {                StartInertialScroll();  \n",[20,3525,3527],{"class":243,"line":3526},127,[20,3528,3529],{},"            }        }    }    \u002F\u002F\u002F \u003Csummary>  \n",[20,3531,3533],{"class":243,"line":3532},128,[20,3534,3535],{},"    \u002F\u002F\u002F 启动惯性滚动  \n",[20,3537,3539],{"class":243,"line":3538},129,[20,3540,3086],{},[20,3542,3544],{"class":243,"line":3543},130,[20,3545,3546],{},"    private void StartInertialScroll()  \n",[20,3548,3550],{"class":243,"line":3549},131,[20,3551,3552],{},"    {        if (!_isScrolling)  \n",[20,3554,3556],{"class":243,"line":3555},132,[20,3557,3558],{},"        {            _isScrolling = true;  \n",[20,3560,3562],{"class":243,"line":3561},133,[20,3563,3564],{},"            _lastAnimationTime = Stopwatch.GetTimestamp();  \n",[20,3566,3568],{"class":243,"line":3567},134,[20,3569,3570],{},"            CompositionTarget.Rendering += OnRendering;  \n",[20,3572,3574],{"class":243,"line":3573},135,[20,3575,3576],{},"        }    }    \u002F\u002F\u002F \u003Csummary>  \n",[20,3578,3580],{"class":243,"line":3579},136,[20,3581,3582],{},"    \u002F\u002F\u002F 停止惯性滚动  \n",[20,3584,3586],{"class":243,"line":3585},137,[20,3587,3086],{},[20,3589,3591],{"class":243,"line":3590},138,[20,3592,3593],{},"    private void StopInertialScroll()  \n",[20,3595,3597],{"class":243,"line":3596},139,[20,3598,3599],{},"    {        if (_isScrolling)  \n",[20,3601,3603],{"class":243,"line":3602},140,[20,3604,3605],{},"        {            _isScrolling = false;  \n",[20,3607,3609],{"class":243,"line":3608},141,[20,3610,3611],{},"            _velocityX = 0;  \n",[20,3613,3615],{"class":243,"line":3614},142,[20,3616,3617],{},"            _velocityY = 0;  \n",[20,3619,3621],{"class":243,"line":3620},143,[20,3622,3623],{},"            CompositionTarget.Rendering -= OnRendering;  \n",[20,3625,3627],{"class":243,"line":3626},144,[20,3628,3576],{},[20,3630,3632],{"class":243,"line":3631},145,[20,3633,3634],{},"    \u002F\u002F\u002F 渲染帧更新，实现二阶缓动物理系统（使用CompositionTarget.Rendering获得更平滑的动画）  \n",[20,3636,3638],{"class":243,"line":3637},146,[20,3639,3086],{},[20,3641,3643],{"class":243,"line":3642},147,[20,3644,3645],{},"    private void OnRendering(object? sender, EventArgs e)  \n",[20,3647,3649],{"class":243,"line":3648},148,[20,3650,3651],{},"    {        if (!_isScrolling) return;  \n",[20,3653,3655],{"class":243,"line":3654},149,[20,3656,3270],{},[20,3658,3660],{"class":243,"line":3659},150,[20,3661,3662],{},"        \u002F\u002F 计算实际时间差（秒），使用高精度时间戳  \n",[20,3664,3666],{"class":243,"line":3665},151,[20,3667,3668],{},"        double deltaTime = (currentTime - _lastAnimationTime) \u002F (double)Stopwatch.Frequency;  \n",[20,3670,3672],{"class":243,"line":3671},152,[20,3673,3674],{},"        _lastAnimationTime = currentTime;  \n",[20,3676,3678],{"class":243,"line":3677},153,[20,3679,3680],{},"        \u002F\u002F 只限制异常大的时间步长（防止卡顿），但不限制正常的速度  \n",[20,3682,3684],{"class":243,"line":3683},154,[20,3685,3686],{},"        if (deltaTime > 0.1) deltaTime = 0.1; \u002F\u002F 允许更大的时间步长，不强制限制为16ms  \n",[20,3688,3690],{"class":243,"line":3689},155,[20,3691,3692],{},"        if (deltaTime \u003C 0.0001) return; \u002F\u002F 跳过过小的步长  \n",[20,3694,3696],{"class":243,"line":3695},156,[20,3697,3698],{},"        \u002F\u002F 获取当前滚动位置  \n",[20,3700,3702],{"class":243,"line":3701},157,[20,3703,3704],{},"        double currentX = HorizontalOffset;  \n",[20,3706,3708],{"class":243,"line":3707},158,[20,3709,3710],{},"        double currentY = VerticalOffset;  \n",[20,3712,3714],{"class":243,"line":3713},159,[20,3715,3716],{},"        \u002F\u002F 计算新位置  \n",[20,3718,3720],{"class":243,"line":3719},160,[20,3721,3722],{},"        double newX = currentX + _velocityX * deltaTime;  \n",[20,3724,3726],{"class":243,"line":3725},161,[20,3727,3728],{},"        double newY = currentY + _velocityY * deltaTime;  \n",[20,3730,3732],{"class":243,"line":3731},162,[20,3733,3734],{},"        \u002F\u002F 边界检测和反弹  \n",[20,3736,3738],{"class":243,"line":3737},163,[20,3739,3740],{},"        bool hitBoundaryX = false;  \n",[20,3742,3744],{"class":243,"line":3743},164,[20,3745,3746],{},"        bool hitBoundaryY = false;  \n",[20,3748,3750],{"class":243,"line":3749},165,[20,3751,3752],{},"        if (newX \u003C 0)  \n",[20,3754,3756],{"class":243,"line":3755},166,[20,3757,3758],{},"        {            newX = 0;  \n",[20,3760,3762],{"class":243,"line":3761},167,[20,3763,3764],{},"            hitBoundaryX = true;  \n",[20,3766,3768],{"class":243,"line":3767},168,[20,3769,3770],{},"        }        else if (newX > ScrollableWidth)  \n",[20,3772,3774],{"class":243,"line":3773},169,[20,3775,3776],{},"        {            newX = ScrollableWidth;  \n",[20,3778,3780],{"class":243,"line":3779},170,[20,3781,3764],{},[20,3783,3785],{"class":243,"line":3784},171,[20,3786,3787],{},"        }        if (newY \u003C 0)  \n",[20,3789,3791],{"class":243,"line":3790},172,[20,3792,3793],{},"        {            newY = 0;  \n",[20,3795,3797],{"class":243,"line":3796},173,[20,3798,3799],{},"            hitBoundaryY = true;  \n",[20,3801,3803],{"class":243,"line":3802},174,[20,3804,3805],{},"        }        else if (newY > ScrollableHeight)  \n",[20,3807,3809],{"class":243,"line":3808},175,[20,3810,3811],{},"        {            newY = ScrollableHeight;  \n",[20,3813,3815],{"class":243,"line":3814},176,[20,3816,3799],{},[20,3818,3820],{"class":243,"line":3819},177,[20,3821,3822],{},"        }        \u002F\u002F 应用边界反弹（弹性效果）  \n",[20,3824,3826],{"class":243,"line":3825},178,[20,3827,3828],{},"        if (hitBoundaryX)  \n",[20,3830,3832],{"class":243,"line":3831},179,[20,3833,3834],{},"        {            _velocityX = -_velocityX * SpringStiffness; \u002F\u002F 反弹并衰减  \n",[20,3836,3838],{"class":243,"line":3837},180,[20,3839,3840],{},"        }  \n",[20,3842,3844],{"class":243,"line":3843},181,[20,3845,3846],{},"        if (hitBoundaryY)  \n",[20,3848,3850],{"class":243,"line":3849},182,[20,3851,3852],{},"        {            _velocityY = -_velocityY * SpringStiffness; \u002F\u002F 反弹并衰减  \n",[20,3854,3856],{"class":243,"line":3855},183,[20,3857,3840],{},[20,3859,3861],{"class":243,"line":3860},184,[20,3862,3863],{},"        \u002F\u002F 应用阻尼（速度衰减）  \n",[20,3865,3867],{"class":243,"line":3866},185,[20,3868,3869],{},"        _velocityX *= DampingCoefficient;  \n",[20,3871,3873],{"class":243,"line":3872},186,[20,3874,3875],{},"        _velocityY *= DampingCoefficient;  \n",[20,3877,3879],{"class":243,"line":3878},187,[20,3880,3881],{},"        \u002F\u002F 更新滚动位置  \n",[20,3883,3885],{"class":243,"line":3884},188,[20,3886,3887],{},"        ScrollToHorizontalOffset(newX);  \n",[20,3889,3891],{"class":243,"line":3890},189,[20,3892,3893],{},"        ScrollToVerticalOffset(newY);  \n",[20,3895,3897],{"class":243,"line":3896},190,[20,3898,3899],{},"        \u002F\u002F 检查是否应该停止滚动  \n",[20,3901,3903],{"class":243,"line":3902},191,[20,3904,3905],{},"        if (Math.Abs(_velocityX) \u003C MinVelocity && Math.Abs(_velocityY) \u003C MinVelocity)  \n",[20,3907,3909],{"class":243,"line":3908},192,[20,3910,3911],{},"        {            StopInertialScroll();  \n",[20,3913,3915],{"class":243,"line":3914},193,[20,3916,3917],{},"        }    }    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)  \n",[20,3919,3921],{"class":243,"line":3920},194,[20,3922,3923],{},"    {        base.OnRenderSizeChanged(sizeInfo);  \n",[20,3925,3927],{"class":243,"line":3926},195,[20,3928,3929],{},"        \u002F\u002F 如果尺寸改变时正在滚动，可能需要调整位置  \n",[20,3931,3933],{"class":243,"line":3932},196,[20,3934,3935],{},"        if (_isScrolling)  \n",[20,3937,3939],{"class":243,"line":3938},197,[20,3940,3941],{},"        {            double newX = Math.Max(0, Math.Min(ScrollableWidth, HorizontalOffset));  \n",[20,3943,3945],{"class":243,"line":3944},198,[20,3946,3947],{},"            double newY = Math.Max(0, Math.Min(ScrollableHeight, VerticalOffset));  \n",[20,3949,3951],{"class":243,"line":3950},199,[20,3952,3953],{},"            ScrollToHorizontalOffset(newX);  \n",[20,3955,3957],{"class":243,"line":3956},200,[20,3958,3959],{},"            ScrollToVerticalOffset(newY);  \n",[20,3961,3963],{"class":243,"line":3962},201,[20,3964,3965],{},"        }    }}\n",[30,3967],{},[33,3969,3970],{"id":579},"🎓 总结",[17,3972,3973],{},"这个实现展示了如何：",[152,3975,3976,3983,3992,3999,4005],{},[155,3977,3978,3979,3982],{},"✅ ",[25,3980,3981],{},"使用物理模型","：二阶缓动系统模拟真实运动",[155,3984,3978,3985,3988,3989,3991],{},[25,3986,3987],{},"高精度时间测量","：使用 ",[239,3990,457],{}," 获得准确的时间差",[155,3993,3978,3994,3988,3996,3998],{},[25,3995,1332],{},[239,3997,424],{}," 获得平滑动画",[155,4000,3978,4001,4004],{},[25,4002,4003],{},"事件拦截","：拦截默认滚动行为，实现自定义滚动",[155,4006,3978,4007,4009],{},[25,4008,2625],{},"：避免不必要的计算，及时停止滚动",[14,4011,4012],{},[17,4013,4014,4016],{},[20,4015,2050],{}," 最终效果\n实现了流畅的惯性滚动，类似于现代移动应用和 Web 浏览器的滚动体验，相比 WPF 默认的 ScrollViewer 有显著的改进。",[30,4018],{},[33,4020,4022],{"id":4021},"相关资源","🔗 相关资源",[161,4024,4025,4034,4041],{},[155,4026,4027],{},[4028,4029,4033],"a",{"href":4030,"rel":4031},"https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Fdotnet\u002Fapi\u002Fsystem.windows.media.compositiontarget",[4032],"nofollow","WPF CompositionTarget 文档",[155,4035,4036],{},[4028,4037,4040],{"href":4038,"rel":4039},"https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Fdotnet\u002Fapi\u002Fsystem.diagnostics.stopwatch",[4032],"Stopwatch 高精度计时",[155,4042,4043],{},[4028,4044,4047],{"href":4045,"rel":4046},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSecond-order_system",[4032],"二阶系统理论",[30,4049],{},[4051,4052,4053],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":237,"searchDepth":250,"depth":250,"links":4055},[4056,4064,4068,4075,4080,4085,4089,4090,4091],{"id":35,"depth":250,"text":36,"children":4057},[4058,4059,4060,4061,4062,4063],{"id":40,"depth":770,"text":40},{"id":144,"depth":770,"text":145},{"id":304,"depth":770,"text":305},{"id":496,"depth":770,"text":496},{"id":555,"depth":770,"text":555},{"id":579,"depth":770,"text":579},{"id":616,"depth":250,"text":617,"children":4065},[4066,4067],{"id":620,"depth":770,"text":620},{"id":696,"depth":770,"text":696},{"id":747,"depth":250,"text":748,"children":4069},[4070,4071,4072,4073,4074],{"id":751,"depth":770,"text":752},{"id":822,"depth":770,"text":823},{"id":883,"depth":770,"text":884},{"id":1215,"depth":770,"text":1216},{"id":2057,"depth":770,"text":2058},{"id":2499,"depth":250,"text":2500,"children":4076},[4077,4078,4079],{"id":2503,"depth":770,"text":2504},{"id":2544,"depth":770,"text":2545},{"id":2589,"depth":770,"text":2590},{"id":2625,"depth":250,"text":2626,"children":4081},[4082,4083,4084],{"id":2629,"depth":770,"text":2630},{"id":2644,"depth":770,"text":2645},{"id":2700,"depth":770,"text":2701},{"id":2720,"depth":250,"text":2721,"children":4086},[4087,4088],{"id":2724,"depth":770,"text":2725},{"id":2751,"depth":770,"text":2752},{"id":2838,"depth":250,"text":2839},{"id":579,"depth":250,"text":3970},{"id":4021,"depth":250,"text":4022},"md",{},"\u002Fcsharp\u002Fwpf\u002Fscrollviewer实现鼠标的平滑滚动",{"title":5,"description":237},"csharp\u002FWPF\u002FScrollViewer实现鼠标的平滑滚动","2ENEkMZWsTjLvX7AJi_0kGPVPvWC3lHijr3rmZY6kvQ",1791042465572]