Skip to content

Commit fdfe21d

Browse files
authored
Feature: Added swipe gesture for back/forward navigation (#12043)
1 parent 5b786a0 commit fdfe21d

File tree

3 files changed

+332
-8
lines changed

3 files changed

+332
-8
lines changed

src/Files.App/Views/ModernShellPage.xaml

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
66
xmlns:local="using:Files.App.Views"
77
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
xmlns:wct="using:CommunityToolkit.WinUI.UI"
89
xmlns:wctconverters="using:CommunityToolkit.WinUI.UI.Converters"
910
x:Name="RootPage"
1011
KeyboardAcceleratorPlacementMode="Hidden"
@@ -51,12 +52,52 @@
5152
Modifiers="Menu" />
5253
</local:BaseShellPage.KeyboardAccelerators>
5354

54-
<Frame
55-
x:Name="ItemDisplayFrame"
56-
HorizontalAlignment="Stretch"
57-
x:FieldModifier="public"
58-
BorderBrush="{x:Bind CurrentInstanceBorderBrush, Mode=OneWay}"
59-
BorderThickness="{x:Bind CurrentInstanceBorderThickness, Mode=OneWay}"
60-
Navigated="ItemDisplayFrame_Navigated" />
55+
<Grid HorizontalAlignment="Stretch" wct:UIElementExtensions.ClipToBounds="True">
56+
<Border
57+
x:Name="BackIcon"
58+
Width="48"
59+
Height="48"
60+
Margin="-1,0,0,0"
61+
HorizontalAlignment="Left"
62+
VerticalAlignment="Center"
63+
wct:VisualExtensions.NormalizedCenterPoint="0.5, 0.5"
64+
Background="{ThemeResource AccentFillColorDefaultBrush}"
65+
BorderBrush="{ThemeResource AccentControlElevationBorderBrush}"
66+
BorderThickness="1"
67+
Canvas.ZIndex="64"
68+
CornerRadius="24">
69+
<FontIcon
70+
HorizontalAlignment="Center"
71+
VerticalAlignment="Center"
72+
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
73+
Glyph="&#xE72B;" />
74+
</Border>
75+
<Border
76+
x:Name="ForwardIcon"
77+
Width="48"
78+
Height="48"
79+
Margin="1,0,0,0"
80+
HorizontalAlignment="Right"
81+
VerticalAlignment="Center"
82+
wct:VisualExtensions.NormalizedCenterPoint="0.5, 0.5"
83+
Background="{ThemeResource AccentFillColorDefaultBrush}"
84+
BorderBrush="{ThemeResource AccentControlElevationBorderBrush}"
85+
BorderThickness="1"
86+
Canvas.ZIndex="64"
87+
CornerRadius="24">
88+
<SymbolIcon
89+
HorizontalAlignment="Center"
90+
VerticalAlignment="Center"
91+
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
92+
Symbol="Forward" />
93+
</Border>
94+
<Frame
95+
x:Name="ItemDisplayFrame"
96+
HorizontalAlignment="Stretch"
97+
x:FieldModifier="public"
98+
BorderBrush="{x:Bind CurrentInstanceBorderBrush, Mode=OneWay}"
99+
BorderThickness="{x:Bind CurrentInstanceBorderThickness, Mode=OneWay}"
100+
Navigated="ItemDisplayFrame_Navigated" />
101+
</Grid>
61102

62103
</local:BaseShellPage>

src/Files.App/Views/ModernShellPage.xaml.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public sealed partial class ModernShellPage : BaseShellPage
3333

3434
protected override Frame ItemDisplay => ItemDisplayFrame;
3535

36+
private NavigationInteractionTracker _navigationInteractionTracker;
37+
3638
public Thickness CurrentInstanceBorderThickness
3739
{
3840
get => (Thickness)GetValue(CurrentInstanceBorderThicknessProperty);
@@ -56,8 +58,10 @@ public ModernShellPage() : base(new CurrentInstanceViewModel())
5658
FilesystemViewModel.GitDirectoryUpdated += FilesystemViewModel_GitDirectoryUpdated;
5759

5860
ToolbarViewModel.PathControlDisplayText = "Home".GetLocalizedResource();
59-
6061
ToolbarViewModel.RefreshWidgetsRequested += ModernShellPage_RefreshWidgetsRequested;
62+
63+
_navigationInteractionTracker = new NavigationInteractionTracker(this, BackIcon, ForwardIcon);
64+
_navigationInteractionTracker.NavigationRequested += OverscrollNavigationRequested;
6165
}
6266

6367
private void ModernShellPage_RefreshWidgetsRequested(object sender, EventArgs e)
@@ -179,6 +183,8 @@ private async void ItemDisplayFrame_Navigated(object sender, NavigationEventArgs
179183

180184
if (parameters.IsLayoutSwitch)
181185
FilesystemViewModel_DirectoryInfoUpdated(sender, EventArgs.Empty);
186+
_navigationInteractionTracker.CanNavigateBackward = CanNavigateBackward;
187+
_navigationInteractionTracker.CanNavigateForward = CanNavigateForward;
182188
}
183189

184190
private async void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
@@ -225,6 +231,20 @@ private async void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, Keybo
225231
}
226232
}
227233

234+
private void OverscrollNavigationRequested(object? sender, OverscrollNavigationEventArgs e)
235+
{
236+
switch (e)
237+
{
238+
case OverscrollNavigationEventArgs.Forward:
239+
Forward_Click();
240+
break;
241+
242+
case OverscrollNavigationEventArgs.Back:
243+
Back_Click();
244+
break;
245+
}
246+
}
247+
228248
public override void Back_Click()
229249
{
230250
ToolbarViewModel.CanGoBack = false;
@@ -289,6 +309,8 @@ public override void Up_Click()
289309
public override void Dispose()
290310
{
291311
ToolbarViewModel.RefreshWidgetsRequested -= ModernShellPage_RefreshWidgetsRequested;
312+
_navigationInteractionTracker.NavigationRequested -= OverscrollNavigationRequested;
313+
_navigationInteractionTracker.Dispose();
292314

293315
base.Dispose();
294316
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using Microsoft.UI.Composition;
5+
using Microsoft.UI.Composition.Interactions;
6+
using Microsoft.UI.Input;
7+
using Microsoft.UI.Xaml;
8+
using Microsoft.UI.Xaml.Hosting;
9+
using Microsoft.UI.Xaml.Input;
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Diagnostics.CodeAnalysis;
13+
using System.Numerics;
14+
15+
namespace Files.App.Views
16+
{
17+
internal class NavigationInteractionTracker : IDisposable
18+
{
19+
public bool CanNavigateForward
20+
{
21+
get
22+
{
23+
_props.TryGetBoolean(nameof(CanNavigateForward), out bool val);
24+
return val;
25+
}
26+
set
27+
{
28+
_props.InsertBoolean(nameof(CanNavigateForward), value);
29+
_tracker.MaxPosition = new(value ? 96f : 0f);
30+
}
31+
}
32+
33+
public bool CanNavigateBackward
34+
{
35+
get
36+
{
37+
_props.TryGetBoolean(nameof(CanNavigateBackward), out bool val);
38+
return val;
39+
}
40+
set
41+
{
42+
_props.InsertBoolean(nameof(CanNavigateBackward), value);
43+
_tracker.MinPosition = new(value ? -96f : 0f);
44+
}
45+
}
46+
47+
private UIElement _rootElement;
48+
private UIElement _backIcon;
49+
private UIElement _forwardIcon;
50+
51+
private PointerEventHandler _pointerPressedHandler;
52+
53+
private Visual _rootVisual;
54+
private Visual _backVisual;
55+
private Visual _forwardVisual;
56+
57+
private InteractionTracker _tracker;
58+
private VisualInteractionSource _source;
59+
private InteractionTrackerOwner _trackerOwner;
60+
private CompositionPropertySet _props;
61+
62+
public event EventHandler<OverscrollNavigationEventArgs>? NavigationRequested;
63+
64+
private bool _disposed;
65+
66+
public NavigationInteractionTracker(UIElement rootElement, UIElement backIcon, UIElement forwardIcon)
67+
{
68+
_rootElement = rootElement;
69+
_backIcon = backIcon;
70+
_forwardIcon = forwardIcon;
71+
72+
ElementCompositionPreview.SetIsTranslationEnabled(_backIcon, true);
73+
ElementCompositionPreview.SetIsTranslationEnabled(_forwardIcon, true);
74+
_rootVisual = ElementCompositionPreview.GetElementVisual(_rootElement);
75+
_backVisual = ElementCompositionPreview.GetElementVisual(_backIcon);
76+
_forwardVisual = ElementCompositionPreview.GetElementVisual(_forwardIcon);
77+
78+
SetupInteractionTracker();
79+
80+
_props = _rootVisual.Compositor.CreatePropertySet();
81+
CanNavigateBackward = false;
82+
CanNavigateForward = false;
83+
84+
SetupAnimations();
85+
86+
_pointerPressedHandler = new(PointerPressed);
87+
_rootElement.AddHandler(UIElement.PointerPressedEvent, _pointerPressedHandler, true);
88+
}
89+
90+
[MemberNotNull(nameof(_tracker), nameof(_source), nameof(_trackerOwner))]
91+
private void SetupInteractionTracker()
92+
{
93+
var compositor = _rootVisual.Compositor;
94+
95+
_trackerOwner = new(this);
96+
_tracker = InteractionTracker.CreateWithOwner(compositor, _trackerOwner);
97+
_tracker.MinPosition = new Vector3(-96f);
98+
_tracker.MaxPosition = new Vector3(96f);
99+
100+
_source = VisualInteractionSource.Create(_rootVisual);
101+
_source.ManipulationRedirectionMode = VisualInteractionSourceRedirectionMode.CapableTouchpadOnly;
102+
_source.PositionXSourceMode = InteractionSourceMode.EnabledWithoutInertia;
103+
_source.PositionXChainingMode = InteractionChainingMode.Always;
104+
_source.PositionYSourceMode = InteractionSourceMode.Disabled;
105+
_tracker.InteractionSources.Add(_source);
106+
}
107+
108+
private void SetupAnimations()
109+
{
110+
var compositor = _rootVisual.Compositor;
111+
112+
var backResistance = CreateResistanceCondition(-96f, 0f);
113+
var forwardResistance = CreateResistanceCondition(0f, 96f);
114+
List<CompositionConditionalValue> conditionalValues = new() { backResistance, forwardResistance };
115+
_source.ConfigureDeltaPositionXModifiers(conditionalValues);
116+
117+
var backAnim = compositor.CreateExpressionAnimation("(-clamp(tracker.Position.X, -96, 0) * 2) - 48");
118+
backAnim.SetReferenceParameter("tracker", _tracker);
119+
backAnim.SetReferenceParameter("props", _props);
120+
_backVisual.StartAnimation("Translation.X", backAnim);
121+
122+
var forwardAnim = compositor.CreateExpressionAnimation("(-clamp(tracker.Position.X, 0, 96) * 2) + 48");
123+
forwardAnim.SetReferenceParameter("tracker", _tracker);
124+
forwardAnim.SetReferenceParameter("props", _props);
125+
_forwardVisual.StartAnimation("Translation.X", forwardAnim);
126+
}
127+
128+
private void PointerPressed(object sender, PointerRoutedEventArgs e)
129+
{
130+
if (e.Pointer.PointerDeviceType == PointerDeviceType.Touch)
131+
{
132+
_source.TryRedirectForManipulation(e.GetCurrentPoint(_rootElement));
133+
}
134+
}
135+
136+
private CompositionConditionalValue CreateResistanceCondition(float minValue, float maxValue)
137+
{
138+
var compositor = _rootVisual.Compositor;
139+
140+
var resistance = CompositionConditionalValue.Create(compositor);
141+
var resistanceCondition = compositor.CreateExpressionAnimation($"tracker.Position.X > {minValue} && tracker.Position.X < {maxValue}");
142+
resistanceCondition.SetReferenceParameter("tracker", _tracker);
143+
var resistanceValue = compositor.CreateExpressionAnimation($"source.DeltaPosition.X * (1 - sqrt(1 - square((tracker.Position.X / {minValue + maxValue}) - 1)))");
144+
resistanceValue.SetReferenceParameter("source", _source);
145+
resistanceValue.SetReferenceParameter("tracker", _tracker);
146+
resistance.Condition = resistanceCondition;
147+
resistance.Value = resistanceValue;
148+
149+
return resistance;
150+
}
151+
152+
~NavigationInteractionTracker()
153+
{
154+
Dispose();
155+
}
156+
157+
public void Dispose()
158+
{
159+
if (_disposed)
160+
return;
161+
162+
_rootElement.RemoveHandler(UIElement.PointerPressedEvent, _pointerPressedHandler);
163+
_backVisual.StopAnimation("Translation.X");
164+
_forwardVisual.StopAnimation("Translation.X");
165+
_tracker.Dispose();
166+
_source.Dispose();
167+
_props.Dispose();
168+
169+
_disposed = true;
170+
GC.SuppressFinalize(this);
171+
}
172+
173+
private class InteractionTrackerOwner : IInteractionTrackerOwner
174+
{
175+
private NavigationInteractionTracker _parent;
176+
private bool _shouldBounceBack;
177+
private bool _shouldAnimate = true;
178+
private Vector3KeyFrameAnimation _scaleAnimation;
179+
private SpringVector3NaturalMotionAnimation _returnAnimation;
180+
181+
public InteractionTrackerOwner(NavigationInteractionTracker parent)
182+
{
183+
_parent = parent;
184+
185+
var compositor = _parent._rootVisual.Compositor;
186+
_scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
187+
_scaleAnimation.InsertKeyFrame(0.5f, new(1.3f));
188+
_scaleAnimation.InsertKeyFrame(1f, new(1f));
189+
_scaleAnimation.Duration = TimeSpan.FromMilliseconds(275);
190+
191+
_returnAnimation = compositor.CreateSpringVector3Animation();
192+
_returnAnimation.FinalValue = new(0f);
193+
_returnAnimation.DampingRatio = 1f;
194+
}
195+
196+
public void IdleStateEntered(InteractionTracker sender, InteractionTrackerIdleStateEnteredArgs args)
197+
{
198+
if (!_shouldBounceBack)
199+
return;
200+
201+
if (Math.Abs(sender.Position.X) > 64)
202+
{
203+
_parent._tracker.TryUpdatePosition(new(0f));
204+
205+
EventHandler<OverscrollNavigationEventArgs>? navEvent = _parent.NavigationRequested;
206+
if (navEvent is not null)
207+
{
208+
if (sender.Position.X > 0 && _parent.CanNavigateForward)
209+
{
210+
navEvent(_parent, OverscrollNavigationEventArgs.Forward);
211+
}
212+
else if (sender.Position.X < 0 && _parent.CanNavigateBackward)
213+
{
214+
navEvent(_parent, OverscrollNavigationEventArgs.Back);
215+
}
216+
}
217+
}
218+
else
219+
{
220+
_parent._tracker.TryUpdatePositionWithAnimation(_returnAnimation);
221+
}
222+
_shouldBounceBack = false;
223+
_shouldAnimate = true;
224+
}
225+
226+
public void InteractingStateEntered(InteractionTracker sender, InteractionTrackerInteractingStateEnteredArgs args)
227+
{
228+
_shouldBounceBack = true;
229+
}
230+
231+
public void ValuesChanged(InteractionTracker sender, InteractionTrackerValuesChangedArgs args)
232+
{
233+
if (!_shouldAnimate)
234+
return;
235+
236+
if (args.Position.X <= -64)
237+
{
238+
_parent._backVisual.StartAnimation("Scale", _scaleAnimation);
239+
_shouldAnimate = false;
240+
}
241+
else if (args.Position.X >= 64)
242+
{
243+
_parent._forwardVisual.StartAnimation("Scale", _scaleAnimation);
244+
_shouldAnimate = false;
245+
}
246+
247+
}
248+
249+
// required to implement IInteractionTrackerOwner
250+
public void CustomAnimationStateEntered(InteractionTracker sender, InteractionTrackerCustomAnimationStateEnteredArgs args) { }
251+
public void InertiaStateEntered(InteractionTracker sender, InteractionTrackerInertiaStateEnteredArgs args) { }
252+
public void RequestIgnored(InteractionTracker sender, InteractionTrackerRequestIgnoredArgs args) { }
253+
}
254+
}
255+
256+
public enum OverscrollNavigationEventArgs
257+
{
258+
Back,
259+
Forward
260+
}
261+
}

0 commit comments

Comments
 (0)