Skip to content

Commit

Permalink
Feature: Added swipe gesture for back/forward navigation (#12043)
Browse files Browse the repository at this point in the history
  • Loading branch information
heftymouse authored Apr 23, 2023
1 parent 5b786a0 commit fdfe21d
Show file tree
Hide file tree
Showing 3 changed files with 332 additions and 8 deletions.
55 changes: 48 additions & 7 deletions src/Files.App/Views/ModernShellPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Files.App.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wct="using:CommunityToolkit.WinUI.UI"
xmlns:wctconverters="using:CommunityToolkit.WinUI.UI.Converters"
x:Name="RootPage"
KeyboardAcceleratorPlacementMode="Hidden"
Expand Down Expand Up @@ -51,12 +52,52 @@
Modifiers="Menu" />
</local:BaseShellPage.KeyboardAccelerators>

<Frame
x:Name="ItemDisplayFrame"
HorizontalAlignment="Stretch"
x:FieldModifier="public"
BorderBrush="{x:Bind CurrentInstanceBorderBrush, Mode=OneWay}"
BorderThickness="{x:Bind CurrentInstanceBorderThickness, Mode=OneWay}"
Navigated="ItemDisplayFrame_Navigated" />
<Grid HorizontalAlignment="Stretch" wct:UIElementExtensions.ClipToBounds="True">
<Border
x:Name="BackIcon"
Width="48"
Height="48"
Margin="-1,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
wct:VisualExtensions.NormalizedCenterPoint="0.5, 0.5"
Background="{ThemeResource AccentFillColorDefaultBrush}"
BorderBrush="{ThemeResource AccentControlElevationBorderBrush}"
BorderThickness="1"
Canvas.ZIndex="64"
CornerRadius="24">
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
Glyph="&#xE72B;" />
</Border>
<Border
x:Name="ForwardIcon"
Width="48"
Height="48"
Margin="1,0,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
wct:VisualExtensions.NormalizedCenterPoint="0.5, 0.5"
Background="{ThemeResource AccentFillColorDefaultBrush}"
BorderBrush="{ThemeResource AccentControlElevationBorderBrush}"
BorderThickness="1"
Canvas.ZIndex="64"
CornerRadius="24">
<SymbolIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ThemeResource TextOnAccentFillColorPrimaryBrush}"
Symbol="Forward" />
</Border>
<Frame
x:Name="ItemDisplayFrame"
HorizontalAlignment="Stretch"
x:FieldModifier="public"
BorderBrush="{x:Bind CurrentInstanceBorderBrush, Mode=OneWay}"
BorderThickness="{x:Bind CurrentInstanceBorderThickness, Mode=OneWay}"
Navigated="ItemDisplayFrame_Navigated" />
</Grid>

</local:BaseShellPage>
24 changes: 23 additions & 1 deletion src/Files.App/Views/ModernShellPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public sealed partial class ModernShellPage : BaseShellPage

protected override Frame ItemDisplay => ItemDisplayFrame;

private NavigationInteractionTracker _navigationInteractionTracker;

public Thickness CurrentInstanceBorderThickness
{
get => (Thickness)GetValue(CurrentInstanceBorderThicknessProperty);
Expand All @@ -56,8 +58,10 @@ public ModernShellPage() : base(new CurrentInstanceViewModel())
FilesystemViewModel.GitDirectoryUpdated += FilesystemViewModel_GitDirectoryUpdated;

ToolbarViewModel.PathControlDisplayText = "Home".GetLocalizedResource();

ToolbarViewModel.RefreshWidgetsRequested += ModernShellPage_RefreshWidgetsRequested;

_navigationInteractionTracker = new NavigationInteractionTracker(this, BackIcon, ForwardIcon);
_navigationInteractionTracker.NavigationRequested += OverscrollNavigationRequested;
}

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

if (parameters.IsLayoutSwitch)
FilesystemViewModel_DirectoryInfoUpdated(sender, EventArgs.Empty);
_navigationInteractionTracker.CanNavigateBackward = CanNavigateBackward;
_navigationInteractionTracker.CanNavigateForward = CanNavigateForward;
}

private async void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
Expand Down Expand Up @@ -225,6 +231,20 @@ private async void KeyboardAccelerator_Invoked(KeyboardAccelerator sender, Keybo
}
}

private void OverscrollNavigationRequested(object? sender, OverscrollNavigationEventArgs e)
{
switch (e)
{
case OverscrollNavigationEventArgs.Forward:
Forward_Click();
break;

case OverscrollNavigationEventArgs.Back:
Back_Click();
break;
}
}

public override void Back_Click()
{
ToolbarViewModel.CanGoBack = false;
Expand Down Expand Up @@ -289,6 +309,8 @@ public override void Up_Click()
public override void Dispose()
{
ToolbarViewModel.RefreshWidgetsRequested -= ModernShellPage_RefreshWidgetsRequested;
_navigationInteractionTracker.NavigationRequested -= OverscrollNavigationRequested;
_navigationInteractionTracker.Dispose();

base.Dispose();
}
Expand Down
261 changes: 261 additions & 0 deletions src/Files.App/Views/NavigationInteractionTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Microsoft.UI.Composition;
using Microsoft.UI.Composition.Interactions;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Input;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;

namespace Files.App.Views
{
internal class NavigationInteractionTracker : IDisposable
{
public bool CanNavigateForward
{
get
{
_props.TryGetBoolean(nameof(CanNavigateForward), out bool val);
return val;
}
set
{
_props.InsertBoolean(nameof(CanNavigateForward), value);
_tracker.MaxPosition = new(value ? 96f : 0f);
}
}

public bool CanNavigateBackward
{
get
{
_props.TryGetBoolean(nameof(CanNavigateBackward), out bool val);
return val;
}
set
{
_props.InsertBoolean(nameof(CanNavigateBackward), value);
_tracker.MinPosition = new(value ? -96f : 0f);
}
}

private UIElement _rootElement;
private UIElement _backIcon;
private UIElement _forwardIcon;

private PointerEventHandler _pointerPressedHandler;

private Visual _rootVisual;
private Visual _backVisual;
private Visual _forwardVisual;

private InteractionTracker _tracker;
private VisualInteractionSource _source;
private InteractionTrackerOwner _trackerOwner;
private CompositionPropertySet _props;

public event EventHandler<OverscrollNavigationEventArgs>? NavigationRequested;

private bool _disposed;

public NavigationInteractionTracker(UIElement rootElement, UIElement backIcon, UIElement forwardIcon)
{
_rootElement = rootElement;
_backIcon = backIcon;
_forwardIcon = forwardIcon;

ElementCompositionPreview.SetIsTranslationEnabled(_backIcon, true);
ElementCompositionPreview.SetIsTranslationEnabled(_forwardIcon, true);
_rootVisual = ElementCompositionPreview.GetElementVisual(_rootElement);
_backVisual = ElementCompositionPreview.GetElementVisual(_backIcon);
_forwardVisual = ElementCompositionPreview.GetElementVisual(_forwardIcon);

SetupInteractionTracker();

_props = _rootVisual.Compositor.CreatePropertySet();
CanNavigateBackward = false;
CanNavigateForward = false;

SetupAnimations();

_pointerPressedHandler = new(PointerPressed);
_rootElement.AddHandler(UIElement.PointerPressedEvent, _pointerPressedHandler, true);
}

[MemberNotNull(nameof(_tracker), nameof(_source), nameof(_trackerOwner))]
private void SetupInteractionTracker()
{
var compositor = _rootVisual.Compositor;

_trackerOwner = new(this);
_tracker = InteractionTracker.CreateWithOwner(compositor, _trackerOwner);
_tracker.MinPosition = new Vector3(-96f);
_tracker.MaxPosition = new Vector3(96f);

_source = VisualInteractionSource.Create(_rootVisual);
_source.ManipulationRedirectionMode = VisualInteractionSourceRedirectionMode.CapableTouchpadOnly;
_source.PositionXSourceMode = InteractionSourceMode.EnabledWithoutInertia;
_source.PositionXChainingMode = InteractionChainingMode.Always;
_source.PositionYSourceMode = InteractionSourceMode.Disabled;
_tracker.InteractionSources.Add(_source);
}

private void SetupAnimations()
{
var compositor = _rootVisual.Compositor;

var backResistance = CreateResistanceCondition(-96f, 0f);
var forwardResistance = CreateResistanceCondition(0f, 96f);
List<CompositionConditionalValue> conditionalValues = new() { backResistance, forwardResistance };
_source.ConfigureDeltaPositionXModifiers(conditionalValues);

var backAnim = compositor.CreateExpressionAnimation("(-clamp(tracker.Position.X, -96, 0) * 2) - 48");
backAnim.SetReferenceParameter("tracker", _tracker);
backAnim.SetReferenceParameter("props", _props);
_backVisual.StartAnimation("Translation.X", backAnim);

var forwardAnim = compositor.CreateExpressionAnimation("(-clamp(tracker.Position.X, 0, 96) * 2) + 48");
forwardAnim.SetReferenceParameter("tracker", _tracker);
forwardAnim.SetReferenceParameter("props", _props);
_forwardVisual.StartAnimation("Translation.X", forwardAnim);
}

private void PointerPressed(object sender, PointerRoutedEventArgs e)
{
if (e.Pointer.PointerDeviceType == PointerDeviceType.Touch)
{
_source.TryRedirectForManipulation(e.GetCurrentPoint(_rootElement));
}
}

private CompositionConditionalValue CreateResistanceCondition(float minValue, float maxValue)
{
var compositor = _rootVisual.Compositor;

var resistance = CompositionConditionalValue.Create(compositor);
var resistanceCondition = compositor.CreateExpressionAnimation($"tracker.Position.X > {minValue} && tracker.Position.X < {maxValue}");
resistanceCondition.SetReferenceParameter("tracker", _tracker);
var resistanceValue = compositor.CreateExpressionAnimation($"source.DeltaPosition.X * (1 - sqrt(1 - square((tracker.Position.X / {minValue + maxValue}) - 1)))");
resistanceValue.SetReferenceParameter("source", _source);
resistanceValue.SetReferenceParameter("tracker", _tracker);
resistance.Condition = resistanceCondition;
resistance.Value = resistanceValue;

return resistance;
}

~NavigationInteractionTracker()
{
Dispose();
}

public void Dispose()
{
if (_disposed)
return;

_rootElement.RemoveHandler(UIElement.PointerPressedEvent, _pointerPressedHandler);
_backVisual.StopAnimation("Translation.X");
_forwardVisual.StopAnimation("Translation.X");
_tracker.Dispose();
_source.Dispose();
_props.Dispose();

_disposed = true;
GC.SuppressFinalize(this);
}

private class InteractionTrackerOwner : IInteractionTrackerOwner
{
private NavigationInteractionTracker _parent;
private bool _shouldBounceBack;
private bool _shouldAnimate = true;
private Vector3KeyFrameAnimation _scaleAnimation;
private SpringVector3NaturalMotionAnimation _returnAnimation;

public InteractionTrackerOwner(NavigationInteractionTracker parent)
{
_parent = parent;

var compositor = _parent._rootVisual.Compositor;
_scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
_scaleAnimation.InsertKeyFrame(0.5f, new(1.3f));
_scaleAnimation.InsertKeyFrame(1f, new(1f));
_scaleAnimation.Duration = TimeSpan.FromMilliseconds(275);

_returnAnimation = compositor.CreateSpringVector3Animation();
_returnAnimation.FinalValue = new(0f);
_returnAnimation.DampingRatio = 1f;
}

public void IdleStateEntered(InteractionTracker sender, InteractionTrackerIdleStateEnteredArgs args)
{
if (!_shouldBounceBack)
return;

if (Math.Abs(sender.Position.X) > 64)
{
_parent._tracker.TryUpdatePosition(new(0f));

EventHandler<OverscrollNavigationEventArgs>? navEvent = _parent.NavigationRequested;
if (navEvent is not null)
{
if (sender.Position.X > 0 && _parent.CanNavigateForward)
{
navEvent(_parent, OverscrollNavigationEventArgs.Forward);
}
else if (sender.Position.X < 0 && _parent.CanNavigateBackward)
{
navEvent(_parent, OverscrollNavigationEventArgs.Back);
}
}
}
else
{
_parent._tracker.TryUpdatePositionWithAnimation(_returnAnimation);
}
_shouldBounceBack = false;
_shouldAnimate = true;
}

public void InteractingStateEntered(InteractionTracker sender, InteractionTrackerInteractingStateEnteredArgs args)
{
_shouldBounceBack = true;
}

public void ValuesChanged(InteractionTracker sender, InteractionTrackerValuesChangedArgs args)
{
if (!_shouldAnimate)
return;

if (args.Position.X <= -64)
{
_parent._backVisual.StartAnimation("Scale", _scaleAnimation);
_shouldAnimate = false;
}
else if (args.Position.X >= 64)
{
_parent._forwardVisual.StartAnimation("Scale", _scaleAnimation);
_shouldAnimate = false;
}

}

// required to implement IInteractionTrackerOwner
public void CustomAnimationStateEntered(InteractionTracker sender, InteractionTrackerCustomAnimationStateEnteredArgs args) { }
public void InertiaStateEntered(InteractionTracker sender, InteractionTrackerInertiaStateEnteredArgs args) { }
public void RequestIgnored(InteractionTracker sender, InteractionTrackerRequestIgnoredArgs args) { }
}
}

public enum OverscrollNavigationEventArgs
{
Back,
Forward
}
}

0 comments on commit fdfe21d

Please sign in to comment.