为什么我只在滚动查看器内的元素的 Y 轴上接收触摸操作增量转换值?
Why am I receiving touch manipulation delta translation values only on the Y axis for elements inside a scrollviewer?
我正在尝试复制默认的 Windows 10 行为,以便在 WPF 中进行触摸和拖动操作与触摸按住并拖动操作。为什么这在 2021 年还不是框架的一部分,只有 MS 霸主可以告诉我们,但是在网上搜索高低、尝试各种实现、尝试在我的 WPF 应用程序中硬塞 UWP 框架等之后,我决定尝试自己实现它。
为了更好地说明我所追求的并确保我们在同一页面上,我附上了以下剪辑来演示:
触摸并立即拖动
触摸、按住,然后拖动
目录内容滚动
触摸的文件夹被拖动
我已经到了看起来我的方法可以工作的地步,但是放置在滚动查看器中的元素的 ManipulationDelta 事件 平移模式 设置为 VerticalOnly 似乎只为 Translation.Y
提供值,Translation.X
始终为 0。显然,我的手指不会在屏幕上完全垂直移动,所以我想接收值对于两个轴。
为了实现这一点,我创建了几个自定义控件,第一个公开了我可以绑定的 ManipulationDelta
属性,以便我可以将值提供给其他控件.它还允许我在 ManipulationDeltaCallback
方法中更新控件的 RenderTransform,以便控件在屏幕上的位置发生变化:
public class Manipulatable : UserControl
{
public static readonly DependencyProperty ManipulationDeltaProperty =
DependencyProperty.Register("ManipulationDelta", typeof(ManipulationDelta), typeof(Manipulatable), new PropertyMetadata(null, new PropertyChangedCallback(ManipulationDeltaCallback)));
private TransformGroup _transformGroup;
private TranslateTransform _translation;
//private ScaleTransform scale;
//private RotateTransform rotation;
public Manipulatable()
{
_transformGroup = new TransformGroup();
_translation = new TranslateTransform(0, 0);
//scale = new ScaleTransform(1, 1);
//rotation = new RotateTransform(0);
_transformGroup.Children.Add(_translation);
//transformGroup.Children.Add(scale);
//transformGroup.Children.Add(rotation);
RenderTransform = _transformGroup;
}
public new ManipulationDelta ManipulationDelta
{
get => (ManipulationDelta)GetValue(ManipulationDeltaProperty);
set => SetValue(ManipulationDeltaProperty, value);
}
private static void ManipulationDeltaCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var manipulationDelta = e.NewValue as ManipulationDelta;
var manipulatable = d as Manipulatable;
manipulatable._translation.X += manipulationDelta.Translation.X;
manipulatable._translation.Y += manipulationDelta.Translation.Y;
}
}
第二个 class 再次公开所有相关触摸操作事件的属性,因此我可以绑定到它们,以及 WPF 缺少的触摸和按住功能,天知道是什么原因:
public class TouchAndHold : Manipulatable
{
public static readonly DependencyProperty TouchedAndHeldProperty =
DependencyProperty.Register("TouchedAndHeld", typeof(RelayCommand), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty TouchedAndHeldParamProperty =
DependencyProperty.Register("TouchedAndHeldParam", typeof(object), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationStartingProperty =
DependencyProperty.Register("ManipulationStarting", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationStartedProperty =
DependencyProperty.Register("ManipulationStarted", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationDeltaChangedProperty =
DependencyProperty.Register("ManipulationDeltaChanged", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationCompletedProperty =
DependencyProperty.Register("ManipulationCompleted", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
private double length;
private bool _overrideTouch;
private bool _held;
private DispatcherTimer _touchHoldTimer;
public TouchAndHold()
{
IsManipulationEnabled = true;
_touchHoldTimer = new DispatcherTimer();
_touchHoldTimer.Tick += _touchHoldTimer_Tick;
_touchHoldTimer.Interval = new TimeSpan(5000000);
}
public RelayCommand TouchedAndHeld
{
get { return (RelayCommand)GetValue(TouchedAndHeldProperty); }
set { SetValue(TouchedAndHeldProperty, value); }
}
public object TouchedAndHeldParam
{
get { return GetValue(TouchedAndHeldParamProperty); }
set { SetValue(TouchedAndHeldParamProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationStarting
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationStartingProperty); }
set { SetValue(ManipulationStartingProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationStarted
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationStartedProperty); }
set { SetValue(ManipulationStartedProperty, value); }
}
public RelayCommand<TouchAndHoldEventArgs> ManipulationDeltaChanged
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationDeltaChangedProperty); }
set { SetValue(ManipulationDeltaChangedProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationCompleted
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationCompletedProperty); }
set { SetValue(ManipulationCompletedProperty, value); }
}
protected override void OnPreviewTouchDown(TouchEventArgs e)
{
length = 0;
_held = false;
_overrideTouch = true;
_touchHoldTimer.Start();
}
protected override void OnPreviewTouchUp(TouchEventArgs e)
{
e.Handled = !_overrideTouch || _held;
_held = false;
_overrideTouch = false;
_touchHoldTimer.Stop();
}
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationStarting?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationStarted?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
{
if (_held)
{
e.Handled = true;
ManipulationDeltaChanged?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
return;
}
length += e.DeltaManipulation.Translation.Length;
if (length >= 10)
{
_overrideTouch = false;
_touchHoldTimer.Stop();
return;
}
e.Handled = !_overrideTouch || _held;
}
protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationCompleted?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
private void _touchHoldTimer_Tick(object sender, EventArgs e)
{
_held = true;
_overrideTouch = false;
_touchHoldTimer.Stop();
TouchedAndHeld?.Execute(TouchedAndHeldParam);
}
public class TouchAndHoldEventArgs
{
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationStartingEventArgs eventArgs)
{
IsTouchOverriden = IsTouchOverriden;
IsHeld = isHeld;
ManipulationStartingEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationStartedEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationStartedEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationDeltaEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationDeltaEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationCompletedEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationCompletedEventArgs = eventArgs;
}
public bool IsTouchOverriden { get; }
public bool IsHeld { get; }
public ManipulationStartingEventArgs ManipulationStartingEventArgs { get; }
public ManipulationStartedEventArgs ManipulationStartedEventArgs { get; }
public ManipulationDeltaEventArgs ManipulationDeltaEventArgs { get; }
public ManipulationCompletedEventArgs ManipulationCompletedEventArgs { get; }
}
}
触摸和按住功能通过一个布尔值工作,当设置为真时,确保所有操作事件都设置为已处理 (e.Handled = true
)。这确保了所有触摸事件都被忽略,例如,滚动查看器不会开始滚动。
调用 OnPreviewTouchDown
时,我将布尔值设置为 true,这样事件就不会传播。我还为触摸和保持部分启动了一个计时器。同时,我监视手指在屏幕上移动的距离。如果它移动得足够远,我假设用户想要滚动,所以我停止做任何事情并让 WPF 按需要处理所有事情。如果在计时器触发之前手指没有移动超过设定的限制,我认为这实际上是一个触摸并保持事件。此时我执行以下操作:
- 执行
TouchedAndHeld
命令让有兴趣的人知道这个事件发生,传递相关信息;
- 开始监控操纵事件并再次执行
ManipulationDeltaChanged
命令,让任何绑定到该命令的人知道操纵事件已经发生并采取适当的行动。值得注意的是,我还将 e.Handled
设置为 true,这样滚动查看器就不会在我移动控件时开始滚动。
最后,我们准备好实际使用这个东西了。因此,我有一个带有触摸和按住控件的滚动查看器:
<ScrollViewer Style="{StaticResource MyScrollViewer}">
<ItemsControl ItemsSource="{Binding SomeButtons}" x:Name="SomeButtonsRoot">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Style="{StaticResource SomeButtonsWrapPanel}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:SomeButton Icon="{Binding Icon}" Text="{Binding Name}"
SelectCmd="{Binding ButtonSelectedCmd}" SelectCmdParam="{Binding SomeParam}"
TouchedAndHeld="{Binding DataContext.SomeButtonDragStartedCmd, ElementName=SomeButtonsRoot}"
ManipulationDeltaChanged="{Binding DataContext.SomeButtonDraggingCmd, ElementName=SomeButtonsRoot}">
<controls:SomeButton.TouchedAndHeldParam>
<MultiBinding Converter="{StaticResource List}">
<Binding />
<Binding RelativeSource="{RelativeSource Self}"/>
<Binding ElementName="CanvasRoot"/>
</MultiBinding>
</controls:SomeButton.TouchedAndHeldParam>
</controls:SomeButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
滚动查看器放在 Canvas
内。在同一个 Canvas
中,我有另一个控件:
<controls:SomeButton Icon="{Binding Icon}" Text="{Binding Name}" Panel.ZIndex="100"
Canvas.Left="{Binding InitialPosition.X}" Canvas.Top="{Binding InitialPosition.Y}"
ManipulationDelta="{Binding Translation}"/>
这是实际将被拖动的控件。滚动查看器中的控件启动触摸并按住拖动,然后我将所有操作数据从启动控件传递到此控件。我没有拖动启动该过程的控件的原因是,根据 this 和堆栈溢出的其他答案,滚动查看器将始终剪辑到边界。因此,我没有弄乱边距、填充和 Z 索引,而是在 canvas 中放置了一个代理控件,我可以将其移动到任何我想移动的地方,而且我知道它是可见的。
到目前为止的代码很有前途。当我触摸并按住滚动查看器中的控件时,另一个控件会在其正上方弹出,当我移动手指时,该控件完美地反映了我的手势 但仅在 Y 轴 上。这是为什么?
我决定看看ScrollViewer到底在做什么,所以我搜索了源代码。幸运的是,我找到了代码 right here.
从第 1645 行开始,在 OnManipulationStarting
覆盖内,代码检查为 ScrollViewer 设置的平移模式,并根据该值更改 ManipulationModes
的 [=14] =].对于 PanningMode.VerticalOnly
,模式设置为 ManipulationModes.TranslateY
。啊哈!这就是为什么我只得到 Y 值。
为了修复这个“功能”,我简单地扩展了 ScrollViewer
class 并覆盖了相同的 OnManipulationStarting
并确保操作模式保持不变:
public class TouchScrollViewer : ScrollViewer
{
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
var initialMode = e.Mode; // Keep note of the original manipulation mode.
base.OnManipulationStarting(e); // Let the ScrollViewer do it's thing.
e.Mode = initialMode; // Ensure the original manipulation mode is used.
}
}
谢天谢地,OnManipulationStarting
实际上是 UIElement
class 的一部分,ScrollViewer
最终扩展了它,并标记为 protected
,所以我们可以在我们认为合适的扩展 class 中覆盖它。
我在 XAML 中改为使用 TouchScrollViewer
而不是原来的 ScrollViewer
,现在一切正常。
我暂时不会将此标记为最终答案,以防万一有人提供更好的解决方案。
万一 link 出现故障或页面上的代码发生变化,这里是相关位:
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
...
PanningMode panningMode = PanningMode;
if (panningMode != PanningMode.None)
{
...
if (ShouldManipulateScroll(e, viewport))
{
// Set Manipulation mode and container
if (panningMode == PanningMode.HorizontalOnly)
{
e.Mode = ManipulationModes.TranslateX;
}
else if (panningMode == PanningMode.VerticalOnly)
{
e.Mode = ManipulationModes.TranslateY;
}
else
{
e.Mode = ManipulationModes.Translate;
}
...
}
...
}
}
我正在尝试复制默认的 Windows 10 行为,以便在 WPF 中进行触摸和拖动操作与触摸按住并拖动操作。为什么这在 2021 年还不是框架的一部分,只有 MS 霸主可以告诉我们,但是在网上搜索高低、尝试各种实现、尝试在我的 WPF 应用程序中硬塞 UWP 框架等之后,我决定尝试自己实现它。
为了更好地说明我所追求的并确保我们在同一页面上,我附上了以下剪辑来演示:
触摸并立即拖动 | 触摸、按住,然后拖动 |
---|---|
目录内容滚动 | 触摸的文件夹被拖动 |
我已经到了看起来我的方法可以工作的地步,但是放置在滚动查看器中的元素的 ManipulationDelta 事件 平移模式 设置为 VerticalOnly 似乎只为 Translation.Y
提供值,Translation.X
始终为 0。显然,我的手指不会在屏幕上完全垂直移动,所以我想接收值对于两个轴。
为了实现这一点,我创建了几个自定义控件,第一个公开了我可以绑定的 ManipulationDelta
属性,以便我可以将值提供给其他控件.它还允许我在 ManipulationDeltaCallback
方法中更新控件的 RenderTransform,以便控件在屏幕上的位置发生变化:
public class Manipulatable : UserControl
{
public static readonly DependencyProperty ManipulationDeltaProperty =
DependencyProperty.Register("ManipulationDelta", typeof(ManipulationDelta), typeof(Manipulatable), new PropertyMetadata(null, new PropertyChangedCallback(ManipulationDeltaCallback)));
private TransformGroup _transformGroup;
private TranslateTransform _translation;
//private ScaleTransform scale;
//private RotateTransform rotation;
public Manipulatable()
{
_transformGroup = new TransformGroup();
_translation = new TranslateTransform(0, 0);
//scale = new ScaleTransform(1, 1);
//rotation = new RotateTransform(0);
_transformGroup.Children.Add(_translation);
//transformGroup.Children.Add(scale);
//transformGroup.Children.Add(rotation);
RenderTransform = _transformGroup;
}
public new ManipulationDelta ManipulationDelta
{
get => (ManipulationDelta)GetValue(ManipulationDeltaProperty);
set => SetValue(ManipulationDeltaProperty, value);
}
private static void ManipulationDeltaCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var manipulationDelta = e.NewValue as ManipulationDelta;
var manipulatable = d as Manipulatable;
manipulatable._translation.X += manipulationDelta.Translation.X;
manipulatable._translation.Y += manipulationDelta.Translation.Y;
}
}
第二个 class 再次公开所有相关触摸操作事件的属性,因此我可以绑定到它们,以及 WPF 缺少的触摸和按住功能,天知道是什么原因:
public class TouchAndHold : Manipulatable
{
public static readonly DependencyProperty TouchedAndHeldProperty =
DependencyProperty.Register("TouchedAndHeld", typeof(RelayCommand), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty TouchedAndHeldParamProperty =
DependencyProperty.Register("TouchedAndHeldParam", typeof(object), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationStartingProperty =
DependencyProperty.Register("ManipulationStarting", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationStartedProperty =
DependencyProperty.Register("ManipulationStarted", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationDeltaChangedProperty =
DependencyProperty.Register("ManipulationDeltaChanged", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
public static readonly DependencyProperty ManipulationCompletedProperty =
DependencyProperty.Register("ManipulationCompleted", typeof(RelayCommand<TouchAndHoldEventArgs>), typeof(TouchAndHold), new PropertyMetadata(null));
private double length;
private bool _overrideTouch;
private bool _held;
private DispatcherTimer _touchHoldTimer;
public TouchAndHold()
{
IsManipulationEnabled = true;
_touchHoldTimer = new DispatcherTimer();
_touchHoldTimer.Tick += _touchHoldTimer_Tick;
_touchHoldTimer.Interval = new TimeSpan(5000000);
}
public RelayCommand TouchedAndHeld
{
get { return (RelayCommand)GetValue(TouchedAndHeldProperty); }
set { SetValue(TouchedAndHeldProperty, value); }
}
public object TouchedAndHeldParam
{
get { return GetValue(TouchedAndHeldParamProperty); }
set { SetValue(TouchedAndHeldParamProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationStarting
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationStartingProperty); }
set { SetValue(ManipulationStartingProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationStarted
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationStartedProperty); }
set { SetValue(ManipulationStartedProperty, value); }
}
public RelayCommand<TouchAndHoldEventArgs> ManipulationDeltaChanged
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationDeltaChangedProperty); }
set { SetValue(ManipulationDeltaChangedProperty, value); }
}
public new RelayCommand<TouchAndHoldEventArgs> ManipulationCompleted
{
get { return (RelayCommand<TouchAndHoldEventArgs>)GetValue(ManipulationCompletedProperty); }
set { SetValue(ManipulationCompletedProperty, value); }
}
protected override void OnPreviewTouchDown(TouchEventArgs e)
{
length = 0;
_held = false;
_overrideTouch = true;
_touchHoldTimer.Start();
}
protected override void OnPreviewTouchUp(TouchEventArgs e)
{
e.Handled = !_overrideTouch || _held;
_held = false;
_overrideTouch = false;
_touchHoldTimer.Stop();
}
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationStarting?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationStarted?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
{
if (_held)
{
e.Handled = true;
ManipulationDeltaChanged?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
return;
}
length += e.DeltaManipulation.Translation.Length;
if (length >= 10)
{
_overrideTouch = false;
_touchHoldTimer.Stop();
return;
}
e.Handled = !_overrideTouch || _held;
}
protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
{
e.Handled = !_overrideTouch || _held;
ManipulationCompleted?.Execute(new TouchAndHoldEventArgs(_overrideTouch, _held, e));
}
private void _touchHoldTimer_Tick(object sender, EventArgs e)
{
_held = true;
_overrideTouch = false;
_touchHoldTimer.Stop();
TouchedAndHeld?.Execute(TouchedAndHeldParam);
}
public class TouchAndHoldEventArgs
{
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationStartingEventArgs eventArgs)
{
IsTouchOverriden = IsTouchOverriden;
IsHeld = isHeld;
ManipulationStartingEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationStartedEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationStartedEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationDeltaEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationDeltaEventArgs = eventArgs;
}
public TouchAndHoldEventArgs(bool isTouchOverriden, bool isHeld, ManipulationCompletedEventArgs eventArgs)
{
IsTouchOverriden = isTouchOverriden;
IsHeld = isHeld;
ManipulationCompletedEventArgs = eventArgs;
}
public bool IsTouchOverriden { get; }
public bool IsHeld { get; }
public ManipulationStartingEventArgs ManipulationStartingEventArgs { get; }
public ManipulationStartedEventArgs ManipulationStartedEventArgs { get; }
public ManipulationDeltaEventArgs ManipulationDeltaEventArgs { get; }
public ManipulationCompletedEventArgs ManipulationCompletedEventArgs { get; }
}
}
触摸和按住功能通过一个布尔值工作,当设置为真时,确保所有操作事件都设置为已处理 (e.Handled = true
)。这确保了所有触摸事件都被忽略,例如,滚动查看器不会开始滚动。
调用 OnPreviewTouchDown
时,我将布尔值设置为 true,这样事件就不会传播。我还为触摸和保持部分启动了一个计时器。同时,我监视手指在屏幕上移动的距离。如果它移动得足够远,我假设用户想要滚动,所以我停止做任何事情并让 WPF 按需要处理所有事情。如果在计时器触发之前手指没有移动超过设定的限制,我认为这实际上是一个触摸并保持事件。此时我执行以下操作:
- 执行
TouchedAndHeld
命令让有兴趣的人知道这个事件发生,传递相关信息; - 开始监控操纵事件并再次执行
ManipulationDeltaChanged
命令,让任何绑定到该命令的人知道操纵事件已经发生并采取适当的行动。值得注意的是,我还将e.Handled
设置为 true,这样滚动查看器就不会在我移动控件时开始滚动。
最后,我们准备好实际使用这个东西了。因此,我有一个带有触摸和按住控件的滚动查看器:
<ScrollViewer Style="{StaticResource MyScrollViewer}">
<ItemsControl ItemsSource="{Binding SomeButtons}" x:Name="SomeButtonsRoot">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Style="{StaticResource SomeButtonsWrapPanel}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:SomeButton Icon="{Binding Icon}" Text="{Binding Name}"
SelectCmd="{Binding ButtonSelectedCmd}" SelectCmdParam="{Binding SomeParam}"
TouchedAndHeld="{Binding DataContext.SomeButtonDragStartedCmd, ElementName=SomeButtonsRoot}"
ManipulationDeltaChanged="{Binding DataContext.SomeButtonDraggingCmd, ElementName=SomeButtonsRoot}">
<controls:SomeButton.TouchedAndHeldParam>
<MultiBinding Converter="{StaticResource List}">
<Binding />
<Binding RelativeSource="{RelativeSource Self}"/>
<Binding ElementName="CanvasRoot"/>
</MultiBinding>
</controls:SomeButton.TouchedAndHeldParam>
</controls:SomeButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
滚动查看器放在 Canvas
内。在同一个 Canvas
中,我有另一个控件:
<controls:SomeButton Icon="{Binding Icon}" Text="{Binding Name}" Panel.ZIndex="100"
Canvas.Left="{Binding InitialPosition.X}" Canvas.Top="{Binding InitialPosition.Y}"
ManipulationDelta="{Binding Translation}"/>
这是实际将被拖动的控件。滚动查看器中的控件启动触摸并按住拖动,然后我将所有操作数据从启动控件传递到此控件。我没有拖动启动该过程的控件的原因是,根据 this 和堆栈溢出的其他答案,滚动查看器将始终剪辑到边界。因此,我没有弄乱边距、填充和 Z 索引,而是在 canvas 中放置了一个代理控件,我可以将其移动到任何我想移动的地方,而且我知道它是可见的。
到目前为止的代码很有前途。当我触摸并按住滚动查看器中的控件时,另一个控件会在其正上方弹出,当我移动手指时,该控件完美地反映了我的手势 但仅在 Y 轴 上。这是为什么?
我决定看看ScrollViewer到底在做什么,所以我搜索了源代码。幸运的是,我找到了代码 right here.
从第 1645 行开始,在 OnManipulationStarting
覆盖内,代码检查为 ScrollViewer 设置的平移模式,并根据该值更改 ManipulationModes
的 [=14] =].对于 PanningMode.VerticalOnly
,模式设置为 ManipulationModes.TranslateY
。啊哈!这就是为什么我只得到 Y 值。
为了修复这个“功能”,我简单地扩展了 ScrollViewer
class 并覆盖了相同的 OnManipulationStarting
并确保操作模式保持不变:
public class TouchScrollViewer : ScrollViewer
{
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
var initialMode = e.Mode; // Keep note of the original manipulation mode.
base.OnManipulationStarting(e); // Let the ScrollViewer do it's thing.
e.Mode = initialMode; // Ensure the original manipulation mode is used.
}
}
谢天谢地,OnManipulationStarting
实际上是 UIElement
class 的一部分,ScrollViewer
最终扩展了它,并标记为 protected
,所以我们可以在我们认为合适的扩展 class 中覆盖它。
我在 XAML 中改为使用 TouchScrollViewer
而不是原来的 ScrollViewer
,现在一切正常。
我暂时不会将此标记为最终答案,以防万一有人提供更好的解决方案。
万一 link 出现故障或页面上的代码发生变化,这里是相关位:
protected override void OnManipulationStarting(ManipulationStartingEventArgs e)
{
...
PanningMode panningMode = PanningMode;
if (panningMode != PanningMode.None)
{
...
if (ShouldManipulateScroll(e, viewport))
{
// Set Manipulation mode and container
if (panningMode == PanningMode.HorizontalOnly)
{
e.Mode = ManipulationModes.TranslateX;
}
else if (panningMode == PanningMode.VerticalOnly)
{
e.Mode = ManipulationModes.TranslateY;
}
else
{
e.Mode = ManipulationModes.Translate;
}
...
}
...
}
}