|
| 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