Skip to content

Commit 2f1dbfa

Browse files
jeanplevesquedr1rrb
authored andcommitted
feat(swipecontrol): Add support for simulated inertia.
1 parent e460f34 commit 2f1dbfa

File tree

3 files changed

+94
-31
lines changed

3 files changed

+94
-31
lines changed

src/Uno.UI/UI/Xaml/Controls/SwipeControl/SwipeControl.Uno.cs

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.Linq;
45
using System.Numerics;
56
using System.Runtime.CompilerServices;
67
using System.Text;
@@ -57,13 +58,13 @@ private void InitializeInteractionTracker()
5758

5859
private void UnoAttachEventHandlers()
5960
{
60-
m_content.ManipulationMode = ManipulationModes.TranslateX /*| ManipulationModes.TranslateInertia*/;
61+
m_content.ManipulationMode = m_isHorizontal ? ManipulationModes.TranslateX : ManipulationModes.TranslateY /*| ManipulationModes.TranslateInertia*/;
6162
m_content.ManipulationStarting += OnSwipeManipulationStarting;
6263
m_content.ManipulationStarted += OnSwipeManipulationStarted;
6364
m_content.ManipulationDelta += OnSwipeManipulationDelta;
6465
//m_content.ManipulationInertiaStarting += OnSwipeManipulationInertiaStarting;
6566
m_content.ManipulationCompleted += OnSwipeManipulationCompleted;
66-
}
67+
}
6768

6869
private void UnoDetachEventHandlers()
6970
{
@@ -102,6 +103,7 @@ private void OnSwipeManipulationStarted(object sender, ManipulationStartedRouted
102103

103104
_positionWhenCaptured = new Vector2((float)_transform.X, (float)_transform.Y);
104105
_grabbedTimer.Start();
106+
_lastMoves.Clear();
105107
}
106108

107109
private void OnSwipeManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
@@ -179,16 +181,21 @@ void RecordMovements(ManipulationDeltaRoutedEventArgs e)
179181
private void UpdateDesiredPosition(ManipulationDeltaRoutedEventArgs e)
180182
{
181183
var rawDesiredPosition = _positionWhenCaptured + e.Cumulative.Translation.ToVector2();
184+
var clampedPosition = GetClampedPosition(rawDesiredPosition);
185+
var attenuatedPosition = GetAttenuatedPosition(clampedPosition);
186+
187+
_desiredPosition = attenuatedPosition;
188+
}
182189

190+
private Vector2 GetClampedPosition(Vector2 rawDesiredPosition)
191+
{
183192
var harNearContent = _hasLeftContent || _hasTopContent;
184193
var hasFarContent = _hasRightContent || _hasBottomContent;
185194
var min = _isNearOpen || !hasFarContent ? 0 : -10000;
186195
var max = _isFarOpen || !harNearContent ? 0 : 10000;
187196

188197
var clampedPosition = Vector2.Max(Vector2.Min(rawDesiredPosition, Vector2.One * max), Vector2.One * min);
189-
var attenuatedPosition = GetAttenuatedPosition(clampedPosition);
190-
191-
_desiredPosition = attenuatedPosition;
198+
return clampedPosition;
192199
}
193200

194201
private void UpdateStackPanelDesiredPosition()
@@ -279,12 +286,37 @@ private async void OnSwipeManipulationCompleted(object sender, ManipulationCompl
279286

280287
private async Task SimulateInertia()
281288
{
282-
// TODO: Use the recorded speeds to ajust the _desiredPosition.
289+
const float speedThreshold = 500f; // pixel/s
290+
const float simulatedInertiaDuration = 0.5f; // in seconds.
291+
292+
var unit = m_isHorizontal ? Vector2.UnitX : Vector2.UnitY;
293+
var estimatedSpeed = m_isHorizontal ? GetSpeed().X : GetSpeed().Y;
294+
var useAfterInertiaPosition = false;
295+
var estimatedPositionAfterInertia = _desiredPosition;
296+
297+
if (Math.Abs(estimatedSpeed) > speedThreshold)
298+
{
299+
estimatedPositionAfterInertia = _desiredPosition + unit * estimatedSpeed * simulatedInertiaDuration; // What would be the position if we let the movement go at the current speed during 'simulatedInertiaDuration'.
300+
estimatedPositionAfterInertia = GetClampedPosition(estimatedPositionAfterInertia);
301+
302+
// If inertia would flip sign of the transform, we close instead.
303+
var flickToOppositeSideCheck = _desiredPosition * estimatedPositionAfterInertia;
304+
// If the stackpanel IsMeasureDirty or IsArrangeDirty are true, it means we can't use its ActualSize, so we close the content.
305+
// This solves a problem when the user swipes the content from one side to the other quickly and the stackpanel doesn't have time to measure and arrange itself before the inertia starts.
306+
// When that happens, the content can open using the previous stackpanel size, causing an invalid behavior.
307+
if (m_swipeContentStackPanel.IsMeasureDirty || m_swipeContentStackPanel.IsArrangeDirty || (m_isHorizontal ? flickToOppositeSideCheck.X < 0 : flickToOppositeSideCheck.Y < 0))
308+
{
309+
CloseWithoutAnimation();
310+
return;
311+
}
312+
313+
_desiredPosition = estimatedPositionAfterInertia;
314+
useAfterInertiaPosition = true;
315+
}
283316

284317
var displacement = m_isHorizontal ? _desiredPosition.X : _desiredPosition.Y;
285318
var absoluteDisplacement = Math.Abs(displacement);
286319
var effectiveStackSize = m_isHorizontal ? m_swipeContentStackPanel.ActualWidth : m_swipeContentStackPanel.ActualHeight;
287-
var unit = m_isHorizontal ? Vector2.UnitX : Vector2.UnitY;
288320

289321
if (m_isOpen)
290322
{
@@ -309,9 +341,17 @@ private async Task SimulateInertia()
309341
}
310342
}
311343

344+
var displacementFromInertiaVector = _desiredPosition - estimatedPositionAfterInertia;
345+
var displacementFromInertia = m_isHorizontal ? displacementFromInertiaVector.X : displacementFromInertiaVector.Y;
346+
if (displacementFromInertia * estimatedSpeed < 0)
347+
{
348+
// If the inertia speed and the direction to the final position are opposite, we don't use the inertia speed.
349+
useAfterInertiaPosition = false;
350+
}
351+
312352
UpdateStackPanelDesiredPosition();
313353

314-
await AnimateTransforms();
354+
await AnimateTransforms(useAfterInertiaPosition, estimatedSpeed);
315355

316356
m_isInteracting = false;
317357

@@ -325,22 +365,7 @@ private async Task SimulateInertia()
325365
}
326366
}
327367

328-
//It is possible that the user has flicked from a negative position to a position that would result in the interaction
329-
//tracker coming to rest at the positive open position (or vise versa). The != zero check does not account for this.
330-
//Instead we check to ensure that the current position and the ModifiedRestingPosition have the same sign (multiply to a positive number)
331-
//If they do not then we are in this situation and want the end result of the interaction to be the closed state, so close without any animation and return
332-
//to prevent further processing of this inertia state.
333-
334-
// TODO:
335-
var positionAfterInertia = _desiredPosition;
336-
var flickToOppositeSideCheck = _desiredPosition * positionAfterInertia;
337-
if (m_isHorizontal ? flickToOppositeSideCheck.X < 0 : flickToOppositeSideCheck.X < 0)
338-
{
339-
CloseWithoutAnimation();
340-
return;
341-
}
342-
343-
UpdateIsOpen(positionAfterInertia != Vector2.Zero);
368+
UpdateIsOpen(_desiredPosition != Vector2.Zero);
344369
// If the user has panned the interaction tracker past 0 in the opposite direction of the previously
345370
// opened swipe items then when we set m_isOpen to true the animations will snap to that value.
346371
// To avoid this we block that side of the animation until the interacting state is entered.
@@ -369,6 +394,18 @@ private async Task SimulateInertia()
369394
}
370395
}
371396

397+
private Vector2 GetSpeed()
398+
{
399+
if (_lastMoves.Any())
400+
{
401+
var totalDelta = _lastMoves.Aggregate((sum, item) => sum + item);
402+
var speed = totalDelta.DeltaXY / (float)totalDelta.DeltaT;
403+
return speed;
404+
}
405+
406+
return Vector2.Zero;
407+
}
408+
372409
private void ConfigurePositionInertiaRestingValues() { }
373410

374411
private void IdleStateEntered(object @null, object @also_null) { }
@@ -395,19 +432,23 @@ private void UpdateTransforms()
395432
}
396433
}
397434

398-
private async Task AnimateTransforms()
435+
private async Task AnimateTransforms(bool useInertiaSpeed, double inertiaSpeed)
399436
{
400437
var currentPosition = m_isHorizontal ? _transform.X : _transform.Y;
401438
var desiredPosition = m_isHorizontal ? _desiredPosition.X : _desiredPosition.Y;
402439
var distance = Math.Abs(desiredPosition - currentPosition);
403440
var duration = Math.Min(distance / c_MinimumCloseVelocity, 0.3);
441+
if (useInertiaSpeed)
442+
{
443+
duration = distance / inertiaSpeed;
444+
}
404445

405446
var storyboard = new Storyboard();
406447
var animation = new DoubleAnimation()
407448
{
408449
To = desiredPosition,
409450
Duration = new Duration(TimeSpan.FromSeconds(duration)),
410-
EasingFunction = new QuadraticEase()
451+
EasingFunction = useInertiaSpeed ? (IEasingFunction)LinearEase.Instance : new QuadraticEase()
411452
{
412453
EasingMode = EasingMode.EaseInOut
413454
}
@@ -424,7 +465,7 @@ private async Task AnimateTransforms()
424465
{
425466
To = stackDesiredPosition,
426467
Duration = new Duration(TimeSpan.FromSeconds(duration)),
427-
EasingFunction = new QuadraticEase()
468+
EasingFunction = useInertiaSpeed ? (IEasingFunction)LinearEase.Instance : new QuadraticEase()
428469
{
429470
EasingMode = EasingMode.EaseInOut
430471
}
@@ -465,6 +506,18 @@ private struct MoveUpdate
465506
public double DeltaY { get; set; }
466507

467508
public double DeltaT { get; set; }
509+
510+
public Vector2 DeltaXY => new Vector2((float)DeltaX, (float)DeltaY);
511+
512+
public static MoveUpdate operator +(MoveUpdate left, MoveUpdate right)
513+
{
514+
return new MoveUpdate()
515+
{
516+
DeltaX = left.DeltaX + right.DeltaX,
517+
DeltaY = left.DeltaY + right.DeltaY,
518+
DeltaT = left.DeltaT + right.DeltaT
519+
};
520+
}
468521
}
469522
}
470523

src/Uno.UI/UI/Xaml/Controls/SwipeControl/SwipeControl.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Uno.Disposables;
1717
using Uno.UI.Helpers.WinUI;
1818
using Uno.UI.Extensions;
19+
using System.Threading.Tasks;
1920

2021
namespace Windows.UI.Xaml.Controls
2122
{
@@ -71,8 +72,13 @@ public async void Close()
7172
//Uno workaround:
7273
m_isInteracting = true;
7374
_desiredPosition = Vector2.Zero;
75+
_lastMoves.Clear(); // Clears inertia.
7476
UpdateStackPanelDesiredPosition();
75-
await AnimateTransforms();
77+
78+
// This delay is to allow users to see the fully open state before it closes back.
79+
await Task.Delay(TimeSpan.FromSeconds(0.250));
80+
81+
await AnimateTransforms(false, 0d);
7682
OnSwipeManipulationCompleted(this, null);
7783

7884
//if (!m_isIdle)

src/Uno.UI/UI/Xaml/Controls/SwipeControl/SwipeItem.properties.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,15 +178,15 @@ private static void OnTextPropertyChanged(
178178

179179
// Uno workaround: Added "new" keyword
180180
public static
181-
#if __IOS__
181+
#if __IOS__ || __ANDROID__
182182
new
183183
#endif
184184
DependencyProperty BackgroundProperty { get; } = DependencyProperty.Register(
185185
"Background", typeof(Brush), typeof(SwipeItem), new PropertyMetadata(default(Brush), OnBackgroundPropertyChanged));
186186

187187
// Uno workaround: Added "new" keyword
188188
public
189-
#if __IOS__
189+
#if __IOS__ || __ANDROID__
190190
new
191191
#endif
192192
Brush Background
@@ -277,7 +277,11 @@ public object CommandParameter
277277
public static DependencyProperty ForegroundProperty { get; } = DependencyProperty.Register(
278278
"Foreground", typeof(Brush), typeof(SwipeItem), new PropertyMetadata(default(Brush), OnForegroundPropertyChanged));
279279

280-
public Brush Foreground
280+
public
281+
#if __ANDROID__
282+
new
283+
#endif
284+
Brush Foreground
281285
{
282286
get { return (Brush)GetValue(ForegroundProperty); }
283287
set { SetValue(ForegroundProperty, value); }

0 commit comments

Comments
 (0)