首页 > 解决方案 > 为什么我只在滚动查看器内的元素的 Y 轴上接收触摸操作增量平移值?

问题描述

我正在尝试在 WPF 中复制默认的 Windows 10 行为以进行触摸和拖动与触摸按住并拖动操作。为什么这还不是 2021 年框架的一部分,只有 MS 霸主可以告诉我们,但是在搜索了互联网高低,尝试了各种实现,尝试在我的 WPF 应用程序中硬塞 UWP 框架等之后,我决定尝试自己实施。

为了更好地说明我所追求的并确保我们在同一页面上,我附上了以下剪辑来演示:

触摸并立即拖动 触摸、按住,然后拖动
在此处输入图像描述 在此处输入图像描述
目录内容滚动 被触摸的文件夹被拖动

我已经到了看起来我的方法可以工作的地步,但是对于放置在滚动查看器中且平移模式设置为VerticalOnly的元素的 ManipulationDelta 事件似乎只提供 的值Translation.YTranslation.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;
    }
}

第二个类再次公开了所有相关触摸操作事件的属性,因此我可以绑定到它们,以及 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; }
        }
    }

触摸和保持功能通过一个布尔值来工作,当设置为 true 时,确保所有操作事件都被设置为已处理 ( e.Handled = true)。这确保了所有触摸事件都被忽略,例如,滚动查看器不会开始滚动。

OnPreviewTouchDown被调用时,我将布尔值设置为 true,这样事件就不会传播。我还为触摸和按住部分启动了一个计时器。同时,我监控手指在屏幕上移动的距离。如果它移动得足够远,我假设用户想要滚动,所以我停止做任何事情,让 WPF 处理它想要的一切。如果在计时器触发之前手指没有移动超过设定的限制,我认为这实际上是一个触摸和按住事件。此时我执行以下操作:

最后,我们准备好实际使用这个东西了。因此,我有一个带有触摸和按住控件的滚动查看器:

<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 索引,而是在画布中放置了一个代理控件,我可以将它移动到任何我想要的地方,并且我知道它是可见的。

到目前为止的代码很有希望。当我在滚动查看器中触摸并按住控件时,另一个控件会在其上方弹出,当我移动手指时,该控件完美地反映了我的手势,但仅在 Y 轴上。这是为什么?

标签: wpftouchwpf-controlstranslation

解决方案


我决定看看 ScrollViewer 实际上在做什么,所以我搜索了源代码。幸运的是,我在这里找到了代码。

从第 1645 行开始,在OnManipulationStarting覆盖内,代码检查为 ScrollViewer 设置的平移模式,并根据该值更改ManipulationModes. ManipulationStartingEventArgs对于PanningMode.VerticalOnly,模式设置为ManipulationModes.TranslateY。啊哈!这就是为什么我只得到 Y 值的原因。

为了修复这个“特性”,我只是扩展了ScrollViewer类并覆盖了它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类的一部分,ScrollViewer最终扩展并标记为protected,因此我们可以在我们认为合适的情况下在扩展类中覆盖它。

我切换到使用TouchScrollViewerin XAML 而不是原来的ScrollViewer,现在一切都按预期工作。

我暂时不会将此标记为最终答案,以防万一有人提供更好的解决方案。

如果链接断开或页面上的代码更改,以下是相关位:

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;
            }
            ...
        }
        ...
    }
}

推荐阅读