在本文中,我们将深入了解如何使用 InteractionTracker 的 SourceModifier 功能,并通过创建自定义拉取刷新控件来演示其用法。
先决条件
此处,我们假设你熟悉以下文章中讨论的概念:
什么是 SourceModifier,为什么它们很有用?
与 InertiaModifiers 一样,SourceModifiers 提供对 InteractionTracker 运动的精细粒度控制。 但是,与在 InteractionTracker 进入惯性后定义运动的惯性修饰符不同,源修饰符定义 InteractionTracker 仍处于交互状态时的运动。 在这些情况下,你需要一种与传统“手指粘黏”不同的体验。
其中一个经典示例是下拉刷新体验 - 当用户下拉列表以刷新内容时,列表的平移速度与手指移动的速度一致,并在一定距离后停止,这样的动作显得突兀且机械。 在用户主动与列表交互时,引入一种阻力的感觉会带来更自然的体验。 这种细微差别有助于使与列表交互的整体最终用户体验更具动态性和吸引力。 在“示例”部分中,我们更详细地介绍了如何构建此内容。
有 2 种类型的源修饰符:
- DeltaPosition – 是在触摸平移交互过程中,手指在当前帧位置与上一帧位置之间的差值。 此源修饰符允许在发送交互以进一步处理之前修改交互的增量位置。 这是 Vector3 类型参数,开发人员可以选择在将位置的任何 X 或 Y 或 Z 属性传递给 InteractionTracker 之前修改该位置的任何 X 或 Y 或 Z 属性。
- DeltaScale - 是当前帧缩放比例与在触摸缩放交互期间应用的上一帧缩放比例之间的差异。 使用此源修饰符可以修改交互的缩放级别。 这是开发人员在传递给 InteractionTracker 之前可以修改的浮点类型属性。
当 InteractionTracker 处于交互状态时,它会评估分配给它的每个源修饰符,并确定其中是否有任何一个适用。 这意味着你可以创建多个源修饰符并将其分配给 InteractionTracker。 但是,在定义每个项时,需要执行以下操作:
- 定义 Condition – 一个表达式,用于定义何时应应用此特定源修饰符的条件语句。
- 定义 DeltaPosition/DeltaScale – 满足上述定义条件时更改 DeltaPosition 或 DeltaScale 的源修饰符表达式。
示例
现在让我们看看如何使用源修改器通过现有的 WinUI XAML ListView 控件创建自定义的“拉动刷新”体验。 我们将使用 Canvas 作为“刷新面板”,该面板将堆叠在 XAML ListView 之上以生成此体验。
为了提升终端用户体验,我们希望在用户主动触摸平移列表时,创建出“阻力”效果,并在平移位置超过某个点后停止移动。
可以在 GitHub 上的 Window UI 开发实验室存储库中找到此体验的工作代码。 下面是构建该体验的分步演练。 在 XAML 标记代码中,有以下各项:
<StackPanel Height="500" MaxHeight="500" x:Name="ContentPanel" HorizontalAlignment="Left" VerticalAlignment="Top" >
<Canvas Width="400" Height="100" x:Name="RefreshPanel" >
<Image x:Name="FirstGear" Source="ms-appx:///Assets/Loading.png" Width="20" Height="20" Canvas.Left="200" Canvas.Top="70"/>
</Canvas>
<ListView x:Name="ThumbnailList"
MaxWidth="400"
Height="500"
ScrollViewer.VerticalScrollMode="Enabled" ScrollViewer.IsScrollInertiaEnabled="False" ScrollViewer.IsVerticalScrollChainingEnabled="True" >
<ListView.ItemTemplate>
……
</ListView.ItemTemplate>
</ListView>
</StackPanel>
由于 ListView(ThumbnailList) 是一个已经具备滚动功能的 XAML 控件,因此当滚动到达最顶层的项而无法继续滚动时,需要将滚动链接到其父控件(ContentPanel)。 (ContentPanel 是将应用源修饰符的位置。若要执行此操作,需要在 ListView 标记中将 ScrollViewer.IsVerticalScrollChainingEnabled 设置为 true 。 还需要将 VisualInteractionSource 上的链接模式设置为 Always。
需要使用 handledEventsToo 参数将 PointerPressedEvent 处理程序设置为 true。 如果没有此选项,PointerPressedEvent 将不会链接到 ContentPanel,因为 ListView 控件会将这些事件标记为已处理,并且不会将事件发送到视觉链。
//The PointerPressed handler needs to be added using AddHandler method with the //handledEventsToo boolean set to "true"
//instead of the XAML element's "PointerPressed=Window_PointerPressed",
//because the list view needs to chain PointerPressed handled events as well.
ContentPanel.AddHandler(PointerPressedEvent, new PointerEventHandler( Window_PointerPressed), true);
现在,你已准备好将它与 InteractionTracker 联系在一起。 首先设置 InteractionTracker、VisualInteractionSource,以及用于利用 InteractionTracker 的位置的表达式。
// InteractionTracker and VisualInteractionSource setup.
_root = ElementCompositionPreview.GetElementVisual(Root);
_compositor = _root.Compositor;
_tracker = InteractionTracker.Create(_compositor);
_interactionSource = VisualInteractionSource.Create(_root);
_interactionSource.PositionYSourceMode = InteractionSourceMode.EnabledWithInertia;
_interactionSource.PositionYChainingMode = InteractionChainingMode.Always;
_tracker.InteractionSources.Add(_interactionSource);
float refreshPanelHeight = (float)RefreshPanel.ActualHeight;
_tracker.MaxPosition = new Vector3((float)Root.ActualWidth, 0, 0);
_tracker.MinPosition = new Vector3(-(float)Root.ActualWidth, -refreshPanelHeight, 0);
// Use the Tacker's Position (negated) to apply to the Offset of the Image.
// The -{refreshPanelHeight} is to hide the refresh panel
m_positionExpression = _compositor.CreateExpressionAnimation($"-tracker.Position.Y - {refreshPanelHeight} ");
m_positionExpression.SetReferenceParameter("tracker", _tracker);
_contentPanelVisual.StartAnimation("Offset.Y", m_positionExpression);
设置后,刷新面板将处于其起始位置的视区外,并且所有用户看到的都是 listView,当平移到达 ContentPanel 时,将触发 PointerPressed 事件,你要求系统使用 InteractionTracker 来驱动操作体验。
private void Window_PointerPressed(object sender, PointerRoutedEventArgs e)
{
if (e.Pointer.PointerDeviceType == Windows.Devices.Input.PointerDeviceType.Touch) {
// Tell the system to use the gestures from this pointer point (if it can).
_interactionSource.TryRedirectForManipulation(e.GetCurrentPoint(null));
}
}
注释
如果不需要链接 Handled 事件,则可以使用属性(PointerPressed="Window_PointerPressed")通过 XAML 标记直接添加 PointerPressedEvent 处理程序。
下一步是设置源修饰符。 你将使用 2 个源修饰符来获取此行为; 阻力 和 停止。
- 阻力 – 以一半的速度移动 DeltaPosition.Y,直到达到 RefreshPanel 的高度。
CompositionConditionalValue resistanceModifier = CompositionConditionalValue.Create (_compositor);
ExpressionAnimation resistanceCondition = _compositor.CreateExpressionAnimation(
$"-tracker.Position.Y < {pullToRefreshDistance}");
resistanceCondition.SetReferenceParameter("tracker", _tracker);
ExpressionAnimation resistanceAlternateValue = _compositor.CreateExpressionAnimation(
"source.DeltaPosition.Y / 3");
resistanceAlternateValue.SetReferenceParameter("source", _interactionSource);
resistanceModifier.Condition = resistanceCondition;
resistanceModifier.Value = resistanceAlternateValue;
- 停止 – 当整个刷新面板在屏幕上时停止移动。
CompositionConditionalValue stoppingModifier = CompositionConditionalValue.Create (_compositor);
ExpressionAnimation stoppingCondition = _compositor.CreateExpressionAnimation(
$"-tracker.Position.Y >= {pullToRefreshDistance}");
stoppingCondition.SetReferenceParameter("tracker", _tracker);
ExpressionAnimation stoppingAlternateValue = _compositor.CreateExpressionAnimation("0");
stoppingModifier.Condition = stoppingCondition;
stoppingModifier.Value = stoppingAlternateValue;
Now add the 2 source modifiers to the InteractionTracker.
List<CompositionConditionalValue> modifierList = new List<CompositionConditionalValue>()
{ resistanceModifier, stoppingModifier };
_interactionSource.ConfigureDeltaPositionYModifiers(modifierList);
此图提供了 SourceModifiers 设置的可视化效果。
现在,使用 SourceModifiers 时,你会注意到将 ListView 向下平移并到达最顶部的项目时,刷新面板将按平移的一半速度下拉,直到达到 RefreshPanel 高度,然后停止移动。
在完整示例中,关键帧动画用于在 RefreshPanel 画布中的交互过程中旋转图标。 可以用其他内容来代替,或者利用 InteractionTracker 的位置单独驱动动画。