前言

在 WPF 应用开发中,嵌套的 ScrollViewer 是一个常见但棘手的问题。当内层 ScrollViewer 滚动到边界时,用户期望能够自然地继续滚动外层容器,但 WPF 默认并不支持这种行为。今天我们来深入探讨这个问题,并提供一个完整的解决方案:NestedScrollBehavior

问题分析

典型场景

想象一个这样的界面结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
<ScrollViewer Name="OuterScrollViewer">
<StackPanel>
<TextBlock Text="外层内容1"/>
<ScrollViewer Name="InnerScrollViewer" Height="200">
<StackPanel>
<TextBlock Text="内层内容1"/>
<TextBlock Text="内层内容2"/>
<!-- 更多内容... -->
</StackPanel>
</ScrollViewer>
<TextBlock Text="外层内容2"/>
</StackPanel>
</ScrollViewer>

遇到的问题

  1. 滚轮卡住:当内层 ScrollViewer 滚动到底部时,继续向下滚动鼠标滚轮,外层 ScrollViewer 不会响应
  2. 触摸不连贯:在触屏设备上,内层滚动到边界后,触摸滑动无法传递到外层
  3. 边界反馈干扰:某些情况下会出现不必要的边界反馈效果
  4. 用户体验差:用户需要移动鼠标到其他区域才能继续滚动

解决方案设计

核心思路

  1. 事件监听:监听内层控件的鼠标滚轮和触摸事件
  2. 边界检测:判断 ScrollViewer 是否已滚动到顶部或底部
  3. 事件传递:在合适的时机将滚动事件传递给父级容器
  4. 状态管理:维护触摸状态,确保流畅的交互体验

实现架构

1
2
3
4
5
6
7
8
9
10
11
12
13
NestedScrollBehavior
├── 附加属性管理
│ ├── DisableBoundaryFeedback (禁用边界反馈)
│ └── IsEnabled (启用嵌套滚动)
├── 事件处理
│ ├── 鼠标滚轮事件
│ └── 触摸事件 (Down/Move/Up)
├── 状态管理
│ └── TouchScrollState (触摸滚动状态)
└── 辅助方法
├── 边界判断
├── 事件传递
└── 控件查找

核心代码实现

1. 基础结构和附加属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/// <summary>
/// 用于处理嵌套控件的滚动事件传递和边界反馈
/// </summary>
public class NestedScrollBehavior
{
#region 附加属性:禁用边界反馈处理
public static readonly DependencyProperty DisableBoundaryFeedbackProperty =
DependencyProperty.RegisterAttached(
"DisableBoundaryFeedback",
typeof(bool),
typeof(NestedScrollBehavior),
new PropertyMetadata(false, OnDisableBoundaryFeedbackChanged)
);

public static bool GetDisableBoundaryFeedback(DependencyObject obj) =>
(bool)obj.GetValue(DisableBoundaryFeedbackProperty);

public static void SetDisableBoundaryFeedback(DependencyObject obj, bool value) =>
obj.SetValue(DisableBoundaryFeedbackProperty, value);

private static void OnDisableBoundaryFeedbackChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not UIElement element) return;

if ((bool)e.NewValue)
element.ManipulationBoundaryFeedback += HandFeedback;
else
element.ManipulationBoundaryFeedback -= HandFeedback;
}

static void HandFeedback(object? sender, ManipulationBoundaryFeedbackEventArgs e)
{
e.Handled = true; // 阻止边界反馈
}
#endregion

#region 附加属性:启用嵌套滚动传递
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(NestedScrollBehavior),
new PropertyMetadata(false, OnIsEnabledChanged)
);

public static bool GetIsEnabled(DependencyObject obj) =>
(bool)obj.GetValue(IsEnabledProperty);

public static void SetIsEnabled(DependencyObject obj, bool value) =>
obj.SetValue(IsEnabledProperty, value);

private static void OnIsEnabledChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not UIElement element) return;

if ((bool)e.NewValue)
{
element.PreviewMouseWheel += HandleMouseWheel;
element.PreviewTouchMove += HandleTouchMove;
element.PreviewTouchUp += HandleTouchUp;
element.PreviewTouchDown += HandleTouchDown;
}
else
{
// 移除事件处理器
element.PreviewMouseWheel -= HandleMouseWheel;
element.PreviewTouchMove -= HandleTouchMove;
element.PreviewTouchUp -= HandleTouchUp;
element.PreviewTouchDown -= HandleTouchDown;
}
}
#endregion
}

2. 触摸状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
private static readonly Dictionary<UIElement, TouchScrollState> _touchStates = new();

private class TouchScrollState
{
public TouchDevice? TouchDevice { get; set; }
public Point InitialPosition { get; set; }
public Point LastPosition { get; set; }
public bool IsScrolling { get; set; }
public double AccumulatedDelta { get; set; }
public DateTime LastMoveTime { get; set; }
public DateTime LastPassToParentTime { get; set; }
public double MinDeltaThreshold { get; set; } = 3.0;
}

3. 鼠标滚轮处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void HandleMouseWheel(object sender, MouseWheelEventArgs e)
{
if (sender is not UIElement element) return;

var scrollViewer = GetScrollViewer(element);
if (scrollViewer == null) return;

// 检查是否需要传递到父级
if (ShouldPassToParent(scrollViewer, e.Delta))
{
PassWheelEventToParent(element, e);
}
}

private static bool ShouldPassToParent(ScrollViewer sv, double deltaY)
{
bool atTop = sv.VerticalOffset <= 0;
bool atBottom = sv.VerticalOffset >= sv.ScrollableHeight;
var parentSV = GetParentScrollViewer(sv as DependencyObject);
bool parentCanScroll = parentSV != null && parentSV.ScrollableHeight > 0;

// 只有在父级可滚动时才传递
return parentCanScroll && ((deltaY < 0 && atBottom) || (deltaY > 0 && atTop));
}

4. 触摸事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private static void HandleTouchDown(object? sender, TouchEventArgs e)
{
if (sender is not UIElement element) return;

var state = GetOrCreateTouchState(element);
if (state.TouchDevice != null) return; // 已有触摸设备

state.TouchDevice = e.TouchDevice;
state.InitialPosition = e.GetTouchPoint(element).Position;
state.LastPosition = state.InitialPosition;
state.IsScrolling = false;
state.AccumulatedDelta = 0;
state.LastMoveTime = DateTime.Now;
}

private static void HandleTouchMove(object? sender, TouchEventArgs e)
{
if (sender is not UIElement element) return;

var state = GetTouchState(element);
if (state?.TouchDevice != e.TouchDevice) return;

var current = e.GetTouchPoint(element).Position;
double deltaY = current.Y - state.LastPosition.Y;
state.LastPosition = current;

// 超过阈值才算滚动
if (!state.IsScrolling &&
Math.Abs(current.Y - state.InitialPosition.Y) > 10)
{
state.IsScrolling = true;
}

if (state.IsScrolling)
{
var sv = GetScrollViewer(element);
if (sv != null && ShouldPassToParent(sv, deltaY))
{
PassTouchEventToParent(element, deltaY);
e.Handled = true;
}
}
}

5. 事件传递机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static void PassWheelEventToParent(UIElement element, MouseWheelEventArgs originalEvent)
{
originalEvent.Handled = true;

var parent = GetScrollableParent(element);
if (parent == null) return;

var newEvent = new MouseWheelEventArgs(
originalEvent.MouseDevice,
originalEvent.Timestamp,
originalEvent.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = element,
};

parent.RaiseEvent(newEvent);
}

private static void PassTouchEventToParent(UIElement element, double deltaY)
{
var parentSV = GetParentScrollViewer(element);
if (parentSV == null) return;

double sensitivity = 1.2;
double newOffset = Clamp(
parentSV.VerticalOffset - deltaY * sensitivity,
0,
parentSV.ScrollableHeight);

parentSV.ScrollToVerticalOffset(newOffset);
}

完整代码示例

以下是完整的 NestedScrollBehavior 实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;


/// <summary>
/// 用于处理嵌套控件的滚动事件传递和边界反馈
/// </summary>
public class NestedScrollBehavior
{
#region 附加属性:禁用边界反馈处理
public static readonly DependencyProperty DisableBoundaryFeedbackProperty =
DependencyProperty.RegisterAttached(
"DisableBoundaryFeedback",
typeof(bool),
typeof(NestedScrollBehavior),
new PropertyMetadata(false, OnDisableBoundaryFeedbackChanged)
); // true 时禁用边界反馈处理 false 时允许边界反馈

public static bool GetDisableBoundaryFeedback(DependencyObject obj) =>
(bool)obj.GetValue(DisableBoundaryFeedbackProperty);

public static void SetDisableBoundaryFeedback(DependencyObject obj, bool value) =>
obj.SetValue(DisableBoundaryFeedbackProperty, value);

private static void OnDisableBoundaryFeedbackChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e
)
{
if (d is not UIElement element)
return;
if ((bool)e.NewValue)
{
// 禁用边界反馈:添加处理程序来阻止反馈
element.ManipulationBoundaryFeedback += HandFeedback;
}
else
{
// 允许边界反馈:移除处理程序
element.ManipulationBoundaryFeedback -= HandFeedback;
}
}

/// <summary>
/// 边界反馈处理程序
/// </summary>
static void HandFeedback(object? sender, ManipulationBoundaryFeedbackEventArgs e)
{
e.Handled = true; // 阻止边界反馈
}
#endregion

#region 附加属性:启用嵌套滚动传递
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(NestedScrollBehavior),
new PropertyMetadata(false, OnIsEnabledChanged)
); // true 时启用嵌套滚动传递 false 时禁用

public static bool GetIsEnabled(DependencyObject obj) => (bool)obj.GetValue(IsEnabledProperty);

public static void SetIsEnabled(DependencyObject obj, bool value) =>
obj.SetValue(IsEnabledProperty, value);

private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not UIElement element)
return;
if ((bool)e.NewValue)
{
element.PreviewMouseWheel += HandleMouseWheel;
element.PreviewTouchMove += HandleTouchMove;
element.PreviewTouchUp += HandleTouchUp;
element.PreviewTouchDown += HandleTouchDown;
}
else
{
element.PreviewMouseWheel -= HandleMouseWheel;
element.PreviewTouchMove -= HandleTouchMove;
element.PreviewTouchUp -= HandleTouchUp;
element.PreviewTouchDown -= HandleTouchDown;
}
}
#endregion

#region 触摸状态跟踪
private static readonly Dictionary<UIElement, TouchScrollState> _touchStates = new();

private class TouchScrollState
{
public TouchDevice? TouchDevice { get; set; }
public Point InitialPosition { get; set; }
public Point LastPosition { get; set; }
public bool IsScrolling { get; set; }
public double AccumulatedDelta { get; set; }
public DateTime LastMoveTime { get; set; }
public DateTime LastPassToParentTime { get; set; }
public double MinDeltaThreshold { get; set; } = 3.0;
}
#endregion

#region 鼠标滚轮处理
/// <summary>
/// 处理鼠标滚轮事件的嵌套传递
/// </summary>
private static void HandleMouseWheel(object sender, MouseWheelEventArgs e)
{
if (sender is not UIElement element)
return;

var scrollViewer = GetScrollViewer(element);
if (scrollViewer == null)
return;

// 检查是否需要传递到父级
if (ShouldPassToParent(scrollViewer, e.Delta))
{
PassWheelEventToParent(element, e);
}
}
#endregion

#region 触摸滑动处理
private static void HandleTouchDown(object? sender, TouchEventArgs e)
{
if (sender is not UIElement element)
return;

var state = GetOrCreateTouchState(element);
if (state.TouchDevice != null)
return;
state.TouchDevice = e.TouchDevice;
state.InitialPosition = e.GetTouchPoint(element).Position;
state.LastPosition = state.InitialPosition;
state.IsScrolling = false;
state.AccumulatedDelta = 0;
state.LastMoveTime = DateTime.Now;
state.LastPassToParentTime = DateTime.MinValue;
}

// 触摸移动里
private static void HandleTouchMove(object? sender, TouchEventArgs e)
{
var element = sender as UIElement;
if (element == null || e.TouchDevice == null)
return;
var state = GetTouchState(element);
var current = e.GetTouchPoint(element).Position;
if (state == null || state.TouchDevice != e.TouchDevice)
return; // 确保是同一个触摸设备
double deltaY = current.Y - state.LastPosition.Y;
state.LastPosition = current;

// 只有超过阈值才算滚动
if (!state.IsScrolling && Math.Abs(current.Y - state.InitialPosition.Y) > 10)
state.IsScrolling = true;

if (state.IsScrolling)
{
var sv = GetScrollViewer(element);
if (sv != null && ShouldPassToParent(sv, deltaY))
{
PassTouchEventToParent(element, deltaY);
e.Handled = true;
}
}
}

private static void HandleTouchUp(object? sender, TouchEventArgs e)
{
if (sender is not UIElement element)
return;

var state = GetTouchState(element);
if (state?.TouchDevice != e.TouchDevice)
return;

// 清理状态
_touchStates.Remove(element);
}
#endregion

#region 辅助方法
/// <summary>
/// 判断是否应该将滚动事件传递给父容器
/// </summary>
private static bool ShouldPassToParent(ScrollViewer sv, double deltaY)
{
bool atTop = sv.VerticalOffset <= 0;
bool atBottom = sv.VerticalOffset >= sv.ScrollableHeight;
var parentSV = GetParentScrollViewer(sv as DependencyObject);
bool parentCanScroll = parentSV != null && parentSV.ScrollableHeight > 0;

// 只有在父级可滚动时才传递
return parentCanScroll && ((deltaY < 0 && atBottom) || (deltaY > 0 && atTop));
}

/// <summary>
/// 将鼠标滚轮事件传递给父容器
/// </summary>
private static void PassWheelEventToParent(UIElement element, MouseWheelEventArgs originalEvent)
{
originalEvent.Handled = true;

var parent = GetScrollableParent(element);
if (parent == null)
return;
var newEvent = new MouseWheelEventArgs(
originalEvent.MouseDevice,
originalEvent.Timestamp,
originalEvent.Delta
)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = element,
};

parent.RaiseEvent(newEvent);
}

private static double Clamp(double value, double min, double max)
{
if (value < min)
return min;
if (value > max)
return max;
return value;
}

/// <summary>
/// 将触摸滑动模拟为父容器的滚动
/// </summary>
private static void PassTouchEventToParent(UIElement element, double deltaY)
{
var parentSV = GetParentScrollViewer(element);
if (parentSV == null)
{
return;
}
double sensitivity = 1.2;
double newOffset = Clamp(
parentSV.VerticalOffset - deltaY * sensitivity,
0,
parentSV.ScrollableHeight
);
parentSV.ScrollToVerticalOffset(newOffset);
}

/// <summary>
/// 获取或创建触摸状态
/// </summary>
private static TouchScrollState GetOrCreateTouchState(UIElement element)
{
if (!_touchStates.ContainsKey(element))
{
_touchStates[element] = new TouchScrollState();
}
return _touchStates[element];
}

/// <summary>
/// 获取触摸状态
/// </summary>
private static TouchScrollState? GetTouchState(UIElement element)
{
return _touchStates.ContainsKey(element) ? _touchStates[element] : null;
}

/// <summary>
/// 通用方法:获取任意控件内的 ScrollViewer
/// </summary>
private static ScrollViewer? GetScrollViewer(DependencyObject dependencyObject)
{
if (dependencyObject is ScrollViewer scrollViewer)
return scrollViewer;

for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
{
var child = VisualTreeHelper.GetChild(dependencyObject, i);
var result = GetScrollViewer(child);
if (result != null)
return result;
}
return null;
}

/// <summary>
/// 获取可滚动的父容器
/// </summary>
private static UIElement? GetScrollableParent(DependencyObject element)
{
var parent = VisualTreeHelper.GetParent(element);
while (parent != null)
{
if (parent is UIElement uiElement)
{
var scrollViewer = GetScrollViewer(parent);
if (scrollViewer != null && scrollViewer.ScrollableHeight > 0)
{
return uiElement;
}
}
parent = VisualTreeHelper.GetParent(parent);
}
return null;
}

/// <summary>
/// 从元素自身开始向上,
/// 找到第一个是 ScrollViewer 且正好包含了这个元素的实例
/// </summary>
private static ScrollViewer? GetParentScrollViewer(DependencyObject element)
{
var parent = VisualTreeHelper.GetParent(element);
while (parent != null)
{
if (parent is ScrollViewer sv)
{
return sv;
}
parent = VisualTreeHelper.GetParent(parent);
}
return null;
}

#endregion
}

注意:完整的代码实现包含了所有必要的辅助方法,如 GetScrollViewerGetScrollableParentGetParentScrollViewer 等,这里为了文章篇幅考虑有所省略。

使用方法

1. 添加命名空间

1
2
3
4
<Window x:Class="YourApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="clr-namespace:YourApp.Behaviors">

2. 基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<ScrollViewer Name="OuterScrollViewer">
<StackPanel>
<TextBlock Text="外层内容"/>

<!-- 为内层ScrollViewer启用嵌套滚动 -->
<ScrollViewer Height="200"
behaviors:NestedScrollBehavior.IsEnabled="True">
<StackPanel>
<!-- 内层内容 -->
</StackPanel>
</ScrollViewer>

<TextBlock Text="更多外层内容"/>
</StackPanel>
</ScrollViewer>

3. 禁用边界反馈

1
2
3
4
<ScrollViewer behaviors:NestedScrollBehavior.IsEnabled="True"
behaviors:NestedScrollBehavior.DisableBoundaryFeedback="True">
<!-- 内容 -->
</ScrollViewer>

4. 复杂嵌套场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ScrollViewer Name="MainScroll">
<StackPanel>
<!-- 第一级嵌套 -->
<ScrollViewer Height="300"
behaviors:NestedScrollBehavior.IsEnabled="True">
<StackPanel>
<TextBlock Text="第一级内容"/>

<!-- 第二级嵌套 -->
<ScrollViewer Height="150"
behaviors:NestedScrollBehavior.IsEnabled="True">
<StackPanel>
<TextBlock Text="第二级内容"/>
</StackPanel>
</ScrollViewer>
</StackPanel>
</ScrollViewer>
</StackPanel>
</ScrollViewer>

实际应用场景

场景 1:新闻阅读应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 主滚动区域 -->
<ScrollViewer Name="NewsListScroll">
<ItemsControl ItemsSource="{Binding NewsItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="10" Background="White">
<!-- 新闻内容预览,可独立滚动 -->
<ScrollViewer Height="200"
behaviors:NestedScrollBehavior.IsEnabled="True">
<TextBlock Text="{Binding Content}" TextWrapping="Wrap"/>
</ScrollViewer>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>

场景 2:设置页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<ScrollViewer Name="SettingsScroll">
<StackPanel>
<!-- 通用设置 -->
<GroupBox Header="通用设置">
<StackPanel>...</StackPanel>
</GroupBox>

<!-- 高级设置(内容较多,需要独立滚动) -->
<GroupBox Header="高级设置">
<ScrollViewer Height="300"
behaviors:NestedScrollBehavior.IsEnabled="True"
behaviors:NestedScrollBehavior.DisableBoundaryFeedback="True">
<StackPanel>
<!-- 大量设置项 -->
</StackPanel>
</ScrollViewer>
</GroupBox>
</StackPanel>
</ScrollViewer>

注意事项和最佳实践

1. 性能考虑

  • 在大量嵌套滚动的场景下,注意监控内存使用
  • 适当设置触摸阈值,避免过于敏感的响应
  • 在控件销毁时确保事件处理器被正确移除

2. 用户体验

  • 不要过度嵌套,一般不超过 3 层
  • 为内层 ScrollViewer 设置合适的高度
  • 考虑添加视觉指示器,让用户知道当前滚动区域

3. 兼容性

  • 在不同 DPI 设置下测试触摸响应
  • 确保在高分辨率显示器上的正确表现
  • 测试鼠标滚轮的加速度设置影响

总结

NestedScrollBehavior 为 WPF 开发者提供了一个强大而灵活的嵌套滚动解决方案。它不仅解决了传统 ScrollViewer 嵌套的问题,还提供了出色的触摸支持和边界反馈控制。

主要优势:

  1. 完整的事件支持:同时支持鼠标滚轮和触摸操作
  2. 智能边界检测:精确判断滚动边界,避免误触发
  3. 流畅的用户体验:无缝的滚动传递,符合用户直觉
  4. 高度可配置:支持禁用边界反馈等自定义选项
  5. 性能优异:优化的状态管理和事件处理

通过这个解决方案,我们可以构建出更加自然、流畅的滚动交互体验,特别适合移动设备和触屏应用。


开发建议:在实际项目中,建议将 NestedScrollBehavior 作为基础组件库的一部分,并根据具体需求调整触摸阈值和敏感度参数。