Browse Source
* Format Carousel sample UI * Added WrapSelection property support * Implement gestures * Update sample adding custom page transitions * More changes * Added swipe velocity * Optimize completion timer * Verify gesture id * Improve CrossFade animation * Fix in swipe gesture getting direction * More changes * Fix mistake * More protections * Remove redundant ItemCount > 0 checks in OnKeyDown * Renamed GestureId to Id in SwipeGestureEventArgs * Remove size parameter from PageTransition Update method * Changes based on feedback * Update VirtualizingCarouselPanel.cs * Refactor and complete swipe gesture (added more tests) * Updated Avalonia.nupkg.xml * Changes based on feedback * Polish carousel snap-back animation * Implement ViewportFractionProperty * Fixed test * Fix FillMode in Rotate3DTransition * Updated comment * Added vertical swipe tests * More changes * Fix interrupted carousel transition lifecyclepull/20937/head
committed by
GitHub
35 changed files with 4869 additions and 221 deletions
@ -0,0 +1,447 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Animation; |
||||
|
using Avalonia.Animation.Easings; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Styling; |
||||
|
|
||||
|
namespace ControlCatalog.Pages.Transitions; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Transitions between two pages with a card-stack effect:
|
||||
|
/// the top page moves/rotates away while the next page scales up underneath.
|
||||
|
/// </summary>
|
||||
|
public class CardStackPageTransition : PageSlide |
||||
|
{ |
||||
|
private const double ViewportLiftScale = 0.03; |
||||
|
private const double ViewportPromotionScale = 0.02; |
||||
|
private const double ViewportDepthOpacityFalloff = 0.08; |
||||
|
private const double SidePeekAngle = 4.0; |
||||
|
private const double FarPeekAngle = 7.0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="CardStackPageTransition"/> class.
|
||||
|
/// </summary>
|
||||
|
public CardStackPageTransition() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="CardStackPageTransition"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="duration">The duration of the animation.</param>
|
||||
|
/// <param name="orientation">The axis on which the animation should occur.</param>
|
||||
|
public CardStackPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal) |
||||
|
: base(duration, orientation) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the maximum rotation angle (degrees) applied to the top card.
|
||||
|
/// </summary>
|
||||
|
public double MaxSwipeAngle { get; set; } = 15.0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the scale reduction applied to the back card (0.05 = 5%).
|
||||
|
/// </summary>
|
||||
|
public double BackCardScale { get; set; } = 0.05; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the vertical offset (pixels) applied to the back card.
|
||||
|
/// </summary>
|
||||
|
public double BackCardOffset { get; set; } = 0.0; |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var tasks = new List<Task>(); |
||||
|
var parent = GetVisualParent(from, to); |
||||
|
var distance = Orientation == PageSlide.SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height; |
||||
|
var translateProperty = Orientation == PageSlide.SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty; |
||||
|
var rotationTarget = Orientation == PageSlide.SlideAxis.Horizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0; |
||||
|
var startScale = 1.0 - BackCardScale; |
||||
|
|
||||
|
if (from != null) |
||||
|
{ |
||||
|
var (rotate, translate) = EnsureTopTransforms(from); |
||||
|
rotate.Angle = 0; |
||||
|
translate.X = 0; |
||||
|
translate.Y = 0; |
||||
|
from.Opacity = 1; |
||||
|
from.ZIndex = 1; |
||||
|
|
||||
|
var animation = new Animation |
||||
|
{ |
||||
|
Easing = SlideOutEasing, |
||||
|
Duration = Duration, |
||||
|
FillMode = FillMode, |
||||
|
Children = |
||||
|
{ |
||||
|
new KeyFrame |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(translateProperty, 0d), |
||||
|
new Setter(RotateTransform.AngleProperty, 0d) |
||||
|
}, |
||||
|
Cue = new Cue(0d) |
||||
|
}, |
||||
|
new KeyFrame |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(translateProperty, forward ? -distance : distance), |
||||
|
new Setter(RotateTransform.AngleProperty, rotationTarget) |
||||
|
}, |
||||
|
Cue = new Cue(1d) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
tasks.Add(animation.RunAsync(from, cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
if (to != null) |
||||
|
{ |
||||
|
var (scale, translate) = EnsureBackTransforms(to); |
||||
|
scale.ScaleX = startScale; |
||||
|
scale.ScaleY = startScale; |
||||
|
translate.X = 0; |
||||
|
translate.Y = BackCardOffset; |
||||
|
to.IsVisible = true; |
||||
|
to.Opacity = 1; |
||||
|
to.ZIndex = 0; |
||||
|
|
||||
|
var animation = new Animation |
||||
|
{ |
||||
|
Easing = SlideInEasing, |
||||
|
Duration = Duration, |
||||
|
FillMode = FillMode, |
||||
|
Children = |
||||
|
{ |
||||
|
new KeyFrame |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(ScaleTransform.ScaleXProperty, startScale), |
||||
|
new Setter(ScaleTransform.ScaleYProperty, startScale), |
||||
|
new Setter(TranslateTransform.YProperty, BackCardOffset) |
||||
|
}, |
||||
|
Cue = new Cue(0d) |
||||
|
}, |
||||
|
new KeyFrame |
||||
|
{ |
||||
|
Setters = |
||||
|
{ |
||||
|
new Setter(ScaleTransform.ScaleXProperty, 1d), |
||||
|
new Setter(ScaleTransform.ScaleYProperty, 1d), |
||||
|
new Setter(TranslateTransform.YProperty, 0d) |
||||
|
}, |
||||
|
Cue = new Cue(1d) |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
tasks.Add(animation.RunAsync(to, cancellationToken)); |
||||
|
} |
||||
|
|
||||
|
await Task.WhenAll(tasks); |
||||
|
|
||||
|
if (from != null && !cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
from.IsVisible = false; |
||||
|
} |
||||
|
|
||||
|
if (!cancellationToken.IsCancellationRequested && to != null) |
||||
|
{ |
||||
|
var (scale, translate) = EnsureBackTransforms(to); |
||||
|
scale.ScaleX = 1; |
||||
|
scale.ScaleY = 1; |
||||
|
translate.X = 0; |
||||
|
translate.Y = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override void Update( |
||||
|
double progress, |
||||
|
Visual? from, |
||||
|
Visual? to, |
||||
|
bool forward, |
||||
|
double pageLength, |
||||
|
IReadOnlyList<PageTransitionItem> visibleItems) |
||||
|
{ |
||||
|
if (visibleItems.Count > 0) |
||||
|
{ |
||||
|
UpdateVisibleItems(progress, from, to, forward, pageLength, visibleItems); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (from is null && to is null) |
||||
|
return; |
||||
|
|
||||
|
var parent = GetVisualParent(from, to); |
||||
|
var size = parent.Bounds.Size; |
||||
|
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal; |
||||
|
var distance = pageLength > 0 |
||||
|
? pageLength |
||||
|
: (isHorizontal ? size.Width : size.Height); |
||||
|
var rotationTarget = isHorizontal ? (forward ? -MaxSwipeAngle : MaxSwipeAngle) : 0.0; |
||||
|
var startScale = 1.0 - BackCardScale; |
||||
|
|
||||
|
if (from != null) |
||||
|
{ |
||||
|
var (rotate, translate) = EnsureTopTransforms(from); |
||||
|
if (isHorizontal) |
||||
|
{ |
||||
|
translate.X = forward ? -distance * progress : distance * progress; |
||||
|
translate.Y = 0; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
translate.X = 0; |
||||
|
translate.Y = forward ? -distance * progress : distance * progress; |
||||
|
} |
||||
|
|
||||
|
rotate.Angle = rotationTarget * progress; |
||||
|
from.IsVisible = true; |
||||
|
from.Opacity = 1; |
||||
|
from.ZIndex = 1; |
||||
|
} |
||||
|
|
||||
|
if (to != null) |
||||
|
{ |
||||
|
var (scale, translate) = EnsureBackTransforms(to); |
||||
|
var currentScale = startScale + (1.0 - startScale) * progress; |
||||
|
var currentOffset = BackCardOffset * (1.0 - progress); |
||||
|
|
||||
|
scale.ScaleX = currentScale; |
||||
|
scale.ScaleY = currentScale; |
||||
|
if (isHorizontal) |
||||
|
{ |
||||
|
translate.X = 0; |
||||
|
translate.Y = currentOffset; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
translate.X = currentOffset; |
||||
|
translate.Y = 0; |
||||
|
} |
||||
|
|
||||
|
to.IsVisible = true; |
||||
|
to.Opacity = 1; |
||||
|
to.ZIndex = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override void Reset(Visual visual) |
||||
|
{ |
||||
|
visual.RenderTransform = null; |
||||
|
visual.RenderTransformOrigin = default; |
||||
|
visual.Opacity = 1; |
||||
|
visual.ZIndex = 0; |
||||
|
} |
||||
|
|
||||
|
private void UpdateVisibleItems( |
||||
|
double progress, |
||||
|
Visual? from, |
||||
|
Visual? to, |
||||
|
bool forward, |
||||
|
double pageLength, |
||||
|
IReadOnlyList<PageTransitionItem> visibleItems) |
||||
|
{ |
||||
|
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal; |
||||
|
var rotationTarget = isHorizontal |
||||
|
? (forward ? -MaxSwipeAngle : MaxSwipeAngle) |
||||
|
: 0.0; |
||||
|
var stackOffset = GetViewportStackOffset(pageLength); |
||||
|
var lift = Math.Sin(Math.Clamp(progress, 0.0, 1.0) * Math.PI); |
||||
|
|
||||
|
foreach (var item in visibleItems) |
||||
|
{ |
||||
|
var visual = item.Visual; |
||||
|
var (rotate, scale, translate) = EnsureViewportTransforms(visual); |
||||
|
var depth = GetViewportDepth(item.ViewportCenterOffset); |
||||
|
var scaleValue = Math.Max(0.84, 1.0 - (BackCardScale * depth)); |
||||
|
var stackValue = stackOffset * depth; |
||||
|
var baseOpacity = Math.Max(0.8, 1.0 - (ViewportDepthOpacityFalloff * depth)); |
||||
|
var restingAngle = isHorizontal ? GetViewportRestingAngle(item.ViewportCenterOffset) : 0.0; |
||||
|
|
||||
|
rotate.Angle = restingAngle; |
||||
|
scale.ScaleX = scaleValue; |
||||
|
scale.ScaleY = scaleValue; |
||||
|
translate.X = 0; |
||||
|
translate.Y = 0; |
||||
|
|
||||
|
if (ReferenceEquals(visual, from)) |
||||
|
{ |
||||
|
rotate.Angle = restingAngle + (rotationTarget * progress); |
||||
|
stackValue -= stackOffset * 0.2 * lift; |
||||
|
baseOpacity = Math.Min(1.0, baseOpacity + 0.08); |
||||
|
} |
||||
|
|
||||
|
if (ReferenceEquals(visual, to)) |
||||
|
{ |
||||
|
var promotedScale = Math.Min(1.0, scaleValue + (ViewportLiftScale * lift) + (ViewportPromotionScale * progress)); |
||||
|
scale.ScaleX = promotedScale; |
||||
|
scale.ScaleY = promotedScale; |
||||
|
rotate.Angle = restingAngle * (1.0 - progress); |
||||
|
stackValue = Math.Max(0.0, stackValue - (stackOffset * (0.45 + (0.2 * lift)) * progress)); |
||||
|
baseOpacity = Math.Min(1.0, baseOpacity + (0.12 * lift)); |
||||
|
} |
||||
|
|
||||
|
if (isHorizontal) |
||||
|
translate.Y = stackValue; |
||||
|
else |
||||
|
translate.X = stackValue; |
||||
|
|
||||
|
visual.IsVisible = true; |
||||
|
visual.Opacity = baseOpacity; |
||||
|
visual.ZIndex = GetViewportZIndex(item.ViewportCenterOffset, visual, from, to); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static (RotateTransform rotate, TranslateTransform translate) EnsureTopTransforms(Visual visual) |
||||
|
{ |
||||
|
if (visual.RenderTransform is TransformGroup group && |
||||
|
group.Children.Count == 2 && |
||||
|
group.Children[0] is RotateTransform rotateTransform && |
||||
|
group.Children[1] is TranslateTransform translateTransform) |
||||
|
{ |
||||
|
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); |
||||
|
return (rotateTransform, translateTransform); |
||||
|
} |
||||
|
|
||||
|
var rotate = new RotateTransform(); |
||||
|
var translate = new TranslateTransform(); |
||||
|
visual.RenderTransform = new TransformGroup |
||||
|
{ |
||||
|
Children = |
||||
|
{ |
||||
|
rotate, |
||||
|
translate |
||||
|
} |
||||
|
}; |
||||
|
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); |
||||
|
return (rotate, translate); |
||||
|
} |
||||
|
|
||||
|
private static (ScaleTransform scale, TranslateTransform translate) EnsureBackTransforms(Visual visual) |
||||
|
{ |
||||
|
if (visual.RenderTransform is TransformGroup group && |
||||
|
group.Children.Count == 2 && |
||||
|
group.Children[0] is ScaleTransform scaleTransform && |
||||
|
group.Children[1] is TranslateTransform translateTransform) |
||||
|
{ |
||||
|
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); |
||||
|
return (scaleTransform, translateTransform); |
||||
|
} |
||||
|
|
||||
|
var scale = new ScaleTransform(); |
||||
|
var translate = new TranslateTransform(); |
||||
|
visual.RenderTransform = new TransformGroup |
||||
|
{ |
||||
|
Children = |
||||
|
{ |
||||
|
scale, |
||||
|
translate |
||||
|
} |
||||
|
}; |
||||
|
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); |
||||
|
return (scale, translate); |
||||
|
} |
||||
|
|
||||
|
private static (RotateTransform rotate, ScaleTransform scale, TranslateTransform translate) EnsureViewportTransforms(Visual visual) |
||||
|
{ |
||||
|
if (visual.RenderTransform is TransformGroup group && |
||||
|
group.Children.Count == 3 && |
||||
|
group.Children[0] is RotateTransform rotateTransform && |
||||
|
group.Children[1] is ScaleTransform scaleTransform && |
||||
|
group.Children[2] is TranslateTransform translateTransform) |
||||
|
{ |
||||
|
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); |
||||
|
return (rotateTransform, scaleTransform, translateTransform); |
||||
|
} |
||||
|
|
||||
|
var rotate = new RotateTransform(); |
||||
|
var scale = new ScaleTransform(1, 1); |
||||
|
var translate = new TranslateTransform(); |
||||
|
visual.RenderTransform = new TransformGroup |
||||
|
{ |
||||
|
Children = |
||||
|
{ |
||||
|
rotate, |
||||
|
scale, |
||||
|
translate |
||||
|
} |
||||
|
}; |
||||
|
visual.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); |
||||
|
return (rotate, scale, translate); |
||||
|
} |
||||
|
|
||||
|
private double GetViewportStackOffset(double pageLength) |
||||
|
{ |
||||
|
if (BackCardOffset > 0) |
||||
|
return BackCardOffset; |
||||
|
|
||||
|
return Math.Clamp(pageLength * 0.045, 10.0, 18.0); |
||||
|
} |
||||
|
|
||||
|
private static double GetViewportDepth(double offsetFromCenter) |
||||
|
{ |
||||
|
var distance = Math.Abs(offsetFromCenter); |
||||
|
|
||||
|
if (distance <= 1.0) |
||||
|
return distance; |
||||
|
|
||||
|
if (distance <= 2.0) |
||||
|
return 1.0 + ((distance - 1.0) * 0.8); |
||||
|
|
||||
|
return 1.8; |
||||
|
} |
||||
|
|
||||
|
private static double GetViewportRestingAngle(double offsetFromCenter) |
||||
|
{ |
||||
|
var sign = Math.Sign(offsetFromCenter); |
||||
|
if (sign == 0) |
||||
|
return 0; |
||||
|
|
||||
|
var distance = Math.Abs(offsetFromCenter); |
||||
|
if (distance <= 1.0) |
||||
|
return sign * Lerp(0.0, SidePeekAngle, distance); |
||||
|
|
||||
|
if (distance <= 2.0) |
||||
|
return sign * Lerp(SidePeekAngle, FarPeekAngle, distance - 1.0); |
||||
|
|
||||
|
return sign * FarPeekAngle; |
||||
|
} |
||||
|
|
||||
|
private static double Lerp(double from, double to, double t) |
||||
|
{ |
||||
|
return from + ((to - from) * Math.Clamp(t, 0.0, 1.0)); |
||||
|
} |
||||
|
|
||||
|
private static int GetViewportZIndex(double offsetFromCenter, Visual visual, Visual? from, Visual? to) |
||||
|
{ |
||||
|
if (ReferenceEquals(visual, from)) |
||||
|
return 5; |
||||
|
|
||||
|
if (ReferenceEquals(visual, to)) |
||||
|
return 4; |
||||
|
|
||||
|
var distance = Math.Abs(offsetFromCenter); |
||||
|
if (distance < 0.5) |
||||
|
return 4; |
||||
|
if (distance < 1.5) |
||||
|
return 3; |
||||
|
return 2; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,380 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Animation; |
||||
|
using Avalonia.Animation.Easings; |
||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace ControlCatalog.Pages.Transitions; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Transitions between two pages using a wave clip that reveals the next page.
|
||||
|
/// </summary>
|
||||
|
public class WaveRevealPageTransition : PageSlide |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="WaveRevealPageTransition"/> class.
|
||||
|
/// </summary>
|
||||
|
public WaveRevealPageTransition() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="WaveRevealPageTransition"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="duration">The duration of the animation.</param>
|
||||
|
/// <param name="orientation">The axis on which the animation should occur.</param>
|
||||
|
public WaveRevealPageTransition(TimeSpan duration, PageSlide.SlideAxis orientation = PageSlide.SlideAxis.Horizontal) |
||||
|
: base(duration, orientation) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the maximum wave bulge (pixels) along the movement axis.
|
||||
|
/// </summary>
|
||||
|
public double MaxBulge { get; set; } = 120.0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the bulge factor along the movement axis (0-1).
|
||||
|
/// </summary>
|
||||
|
public double BulgeFactor { get; set; } = 0.35; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the bulge factor along the cross axis (0-1).
|
||||
|
/// </summary>
|
||||
|
public double CrossBulgeFactor { get; set; } = 0.3; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a cross-axis offset (pixels) to shift the wave center.
|
||||
|
/// </summary>
|
||||
|
public double WaveCenterOffset { get; set; } = 0.0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets how strongly the wave center follows the provided offset.
|
||||
|
/// </summary>
|
||||
|
public double CenterSensitivity { get; set; } = 1.0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the bulge exponent used to shape the wave (1.0 = linear).
|
||||
|
/// Higher values tighten the bulge; lower values broaden it.
|
||||
|
/// </summary>
|
||||
|
public double BulgeExponent { get; set; } = 1.0; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the easing applied to the wave progress (clip only).
|
||||
|
/// </summary>
|
||||
|
public Easing WaveEasing { get; set; } = new CubicEaseOut(); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (to != null) |
||||
|
{ |
||||
|
to.IsVisible = true; |
||||
|
to.ZIndex = 1; |
||||
|
} |
||||
|
|
||||
|
if (from != null) |
||||
|
{ |
||||
|
from.ZIndex = 0; |
||||
|
} |
||||
|
|
||||
|
await AnimateProgress(0.0, 1.0, from, to, forward, cancellationToken); |
||||
|
|
||||
|
if (to != null && !cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
to.Clip = null; |
||||
|
} |
||||
|
|
||||
|
if (from != null && !cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
from.IsVisible = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override void Update( |
||||
|
double progress, |
||||
|
Visual? from, |
||||
|
Visual? to, |
||||
|
bool forward, |
||||
|
double pageLength, |
||||
|
IReadOnlyList<PageTransitionItem> visibleItems) |
||||
|
{ |
||||
|
if (visibleItems.Count > 0) |
||||
|
{ |
||||
|
UpdateVisibleItems(from, to, forward, pageLength, visibleItems); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (from is null && to is null) |
||||
|
return; |
||||
|
var parent = GetVisualParent(from, to); |
||||
|
var size = parent.Bounds.Size; |
||||
|
var centerOffset = WaveCenterOffset * CenterSensitivity; |
||||
|
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal; |
||||
|
|
||||
|
if (to != null) |
||||
|
{ |
||||
|
to.IsVisible = progress > 0.0; |
||||
|
to.ZIndex = 1; |
||||
|
to.Opacity = 1; |
||||
|
|
||||
|
if (progress >= 1.0) |
||||
|
{ |
||||
|
to.Clip = null; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var waveProgress = WaveEasing?.Ease(progress) ?? progress; |
||||
|
var clip = LiquidSwipeClipper.CreateWavePath( |
||||
|
waveProgress, |
||||
|
size, |
||||
|
centerOffset, |
||||
|
forward, |
||||
|
isHorizontal, |
||||
|
MaxBulge, |
||||
|
BulgeFactor, |
||||
|
CrossBulgeFactor, |
||||
|
BulgeExponent); |
||||
|
to.Clip = clip; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (from != null) |
||||
|
{ |
||||
|
from.IsVisible = true; |
||||
|
from.ZIndex = 0; |
||||
|
from.Opacity = 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void UpdateVisibleItems( |
||||
|
Visual? from, |
||||
|
Visual? to, |
||||
|
bool forward, |
||||
|
double pageLength, |
||||
|
IReadOnlyList<PageTransitionItem> visibleItems) |
||||
|
{ |
||||
|
if (from is null && to is null) |
||||
|
return; |
||||
|
|
||||
|
var parent = GetVisualParent(from, to); |
||||
|
var size = parent.Bounds.Size; |
||||
|
var centerOffset = WaveCenterOffset * CenterSensitivity; |
||||
|
var isHorizontal = Orientation == PageSlide.SlideAxis.Horizontal; |
||||
|
var resolvedPageLength = pageLength > 0 |
||||
|
? pageLength |
||||
|
: (isHorizontal ? size.Width : size.Height); |
||||
|
foreach (var item in visibleItems) |
||||
|
{ |
||||
|
var visual = item.Visual; |
||||
|
visual.IsVisible = true; |
||||
|
visual.Opacity = 1; |
||||
|
visual.Clip = null; |
||||
|
visual.ZIndex = ReferenceEquals(visual, to) ? 1 : 0; |
||||
|
|
||||
|
if (!ReferenceEquals(visual, to)) |
||||
|
continue; |
||||
|
|
||||
|
var visibleFraction = GetVisibleFraction(item.ViewportCenterOffset, size, resolvedPageLength, isHorizontal); |
||||
|
if (visibleFraction >= 1.0) |
||||
|
continue; |
||||
|
|
||||
|
visual.Clip = LiquidSwipeClipper.CreateWavePath( |
||||
|
visibleFraction, |
||||
|
size, |
||||
|
centerOffset, |
||||
|
forward, |
||||
|
isHorizontal, |
||||
|
MaxBulge, |
||||
|
BulgeFactor, |
||||
|
CrossBulgeFactor, |
||||
|
BulgeExponent); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static double GetVisibleFraction(double offsetFromCenter, Size viewportSize, double pageLength, bool isHorizontal) |
||||
|
{ |
||||
|
if (pageLength <= 0) |
||||
|
return 1.0; |
||||
|
|
||||
|
var viewportLength = isHorizontal ? viewportSize.Width : viewportSize.Height; |
||||
|
if (viewportLength <= 0) |
||||
|
return 0.0; |
||||
|
|
||||
|
var viewportUnits = viewportLength / pageLength; |
||||
|
var edgePeek = Math.Max(0.0, (viewportUnits - 1.0) / 2.0); |
||||
|
return Math.Clamp(1.0 + edgePeek - Math.Abs(offsetFromCenter), 0.0, 1.0); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public override void Reset(Visual visual) |
||||
|
{ |
||||
|
visual.Clip = null; |
||||
|
visual.ZIndex = 0; |
||||
|
visual.Opacity = 1; |
||||
|
} |
||||
|
|
||||
|
private async Task AnimateProgress( |
||||
|
double from, |
||||
|
double to, |
||||
|
Visual? fromVisual, |
||||
|
Visual? toVisual, |
||||
|
bool forward, |
||||
|
CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var parent = GetVisualParent(fromVisual, toVisual); |
||||
|
var pageLength = Orientation == PageSlide.SlideAxis.Horizontal |
||||
|
? parent.Bounds.Width |
||||
|
: parent.Bounds.Height; |
||||
|
var durationMs = Math.Max(Duration.TotalMilliseconds * Math.Abs(to - from), 50); |
||||
|
var startTicks = Stopwatch.GetTimestamp(); |
||||
|
var tickFreq = Stopwatch.Frequency; |
||||
|
|
||||
|
while (!cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
var elapsedMs = (Stopwatch.GetTimestamp() - startTicks) * 1000.0 / tickFreq; |
||||
|
var t = Math.Clamp(elapsedMs / durationMs, 0.0, 1.0); |
||||
|
var eased = SlideInEasing?.Ease(t) ?? t; |
||||
|
var progress = from + (to - from) * eased; |
||||
|
|
||||
|
Update(progress, fromVisual, toVisual, forward, pageLength, Array.Empty<PageTransitionItem>()); |
||||
|
|
||||
|
if (t >= 1.0) |
||||
|
break; |
||||
|
|
||||
|
await Task.Delay(16, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
if (!cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
Update(to, fromVisual, toVisual, forward, pageLength, Array.Empty<PageTransitionItem>()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static class LiquidSwipeClipper |
||||
|
{ |
||||
|
public static Geometry CreateWavePath( |
||||
|
double progress, |
||||
|
Size size, |
||||
|
double waveCenterOffset, |
||||
|
bool forward, |
||||
|
bool isHorizontal, |
||||
|
double maxBulge, |
||||
|
double bulgeFactor, |
||||
|
double crossBulgeFactor, |
||||
|
double bulgeExponent) |
||||
|
{ |
||||
|
var width = size.Width; |
||||
|
var height = size.Height; |
||||
|
|
||||
|
if (progress <= 0) |
||||
|
return new RectangleGeometry(new Rect(0, 0, 0, 0)); |
||||
|
|
||||
|
if (progress >= 1) |
||||
|
return new RectangleGeometry(new Rect(0, 0, width, height)); |
||||
|
|
||||
|
if (width <= 0 || height <= 0) |
||||
|
return new RectangleGeometry(new Rect(0, 0, 0, 0)); |
||||
|
|
||||
|
var mainLength = isHorizontal ? width : height; |
||||
|
var crossLength = isHorizontal ? height : width; |
||||
|
|
||||
|
var wavePhase = Math.Sin(progress * Math.PI); |
||||
|
var bulgeProgress = bulgeExponent == 1.0 ? wavePhase : Math.Pow(wavePhase, bulgeExponent); |
||||
|
var revealedLength = mainLength * progress; |
||||
|
var bulgeMain = Math.Min(mainLength * bulgeFactor, maxBulge) * bulgeProgress; |
||||
|
bulgeMain = Math.Min(bulgeMain, revealedLength * 0.45); |
||||
|
var bulgeCross = crossLength * crossBulgeFactor; |
||||
|
|
||||
|
var waveCenter = crossLength / 2 + waveCenterOffset; |
||||
|
waveCenter = Math.Clamp(waveCenter, bulgeCross, crossLength - bulgeCross); |
||||
|
|
||||
|
var geometry = new StreamGeometry(); |
||||
|
using (var context = geometry.Open()) |
||||
|
{ |
||||
|
if (isHorizontal) |
||||
|
{ |
||||
|
if (forward) |
||||
|
{ |
||||
|
var waveX = width * (1 - progress); |
||||
|
context.BeginFigure(new Point(width, 0), true); |
||||
|
context.LineTo(new Point(waveX, 0)); |
||||
|
context.CubicBezierTo( |
||||
|
new Point(waveX, waveCenter - bulgeCross), |
||||
|
new Point(waveX - bulgeMain, waveCenter - bulgeCross * 0.5), |
||||
|
new Point(waveX - bulgeMain, waveCenter)); |
||||
|
context.CubicBezierTo( |
||||
|
new Point(waveX - bulgeMain, waveCenter + bulgeCross * 0.5), |
||||
|
new Point(waveX, waveCenter + bulgeCross), |
||||
|
new Point(waveX, height)); |
||||
|
context.LineTo(new Point(width, height)); |
||||
|
context.EndFigure(true); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var waveX = width * progress; |
||||
|
context.BeginFigure(new Point(0, 0), true); |
||||
|
context.LineTo(new Point(waveX, 0)); |
||||
|
context.CubicBezierTo( |
||||
|
new Point(waveX, waveCenter - bulgeCross), |
||||
|
new Point(waveX + bulgeMain, waveCenter - bulgeCross * 0.5), |
||||
|
new Point(waveX + bulgeMain, waveCenter)); |
||||
|
context.CubicBezierTo( |
||||
|
new Point(waveX + bulgeMain, waveCenter + bulgeCross * 0.5), |
||||
|
new Point(waveX, waveCenter + bulgeCross), |
||||
|
new Point(waveX, height)); |
||||
|
context.LineTo(new Point(0, height)); |
||||
|
context.EndFigure(true); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (forward) |
||||
|
{ |
||||
|
var waveY = height * (1 - progress); |
||||
|
context.BeginFigure(new Point(0, height), true); |
||||
|
context.LineTo(new Point(0, waveY)); |
||||
|
context.CubicBezierTo( |
||||
|
new Point(waveCenter - bulgeCross, waveY), |
||||
|
new Point(waveCenter - bulgeCross * 0.5, waveY - bulgeMain), |
||||
|
new Point(waveCenter, waveY - bulgeMain)); |
||||
|
context.CubicBezierTo( |
||||
|
new Point(waveCenter + bulgeCross * 0.5, waveY - bulgeMain), |
||||
|
new Point(waveCenter + bulgeCross, waveY), |
||||
|
new Point(width, waveY)); |
||||
|
context.LineTo(new Point(width, height)); |
||||
|
context.EndFigure(true); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var waveY = height * progress; |
||||
|
context.BeginFigure(new Point(0, 0), true); |
||||
|
context.LineTo(new Point(0, waveY)); |
||||
|
context.CubicBezierTo( |
||||
|
new Point(waveCenter - bulgeCross, waveY), |
||||
|
new Point(waveCenter - bulgeCross * 0.5, waveY + bulgeMain), |
||||
|
new Point(waveCenter, waveY + bulgeMain)); |
||||
|
context.CubicBezierTo( |
||||
|
new Point(waveCenter + bulgeCross * 0.5, waveY + bulgeMain), |
||||
|
new Point(waveCenter + bulgeCross, waveY), |
||||
|
new Point(width, waveY)); |
||||
|
context.LineTo(new Point(width, 0)); |
||||
|
context.EndFigure(true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return geometry; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An <see cref="IPageTransition"/> that supports progress-driven updates.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Transitions implementing this interface can be driven by a normalized progress value
|
||||
|
/// (0.0 to 1.0) during swipe gestures or programmatic animations, rather than running
|
||||
|
/// as a timed animation via <see cref="IPageTransition.Start"/>.
|
||||
|
/// </remarks>
|
||||
|
public interface IProgressPageTransition : IPageTransition |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Updates the transition to reflect the given progress.
|
||||
|
/// </summary>
|
||||
|
/// <param name="progress">The normalized progress value from 0.0 (start) to 1.0 (complete).</param>
|
||||
|
/// <param name="from">The visual being transitioned away from. May be null.</param>
|
||||
|
/// <param name="to">The visual being transitioned to. May be null.</param>
|
||||
|
/// <param name="forward">Whether the transition direction is forward (next) or backward (previous).</param>
|
||||
|
/// <param name="pageLength">The size of a page along the transition axis.</param>
|
||||
|
/// <param name="visibleItems">The currently visible realized pages, if more than one page is visible.</param>
|
||||
|
void Update( |
||||
|
double progress, |
||||
|
Visual? from, |
||||
|
Visual? to, |
||||
|
bool forward, |
||||
|
double pageLength, |
||||
|
IReadOnlyList<PageTransitionItem> visibleItems); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Resets any visual state applied to the given visual by this transition.
|
||||
|
/// </summary>
|
||||
|
/// <param name="visual">The visual to reset.</param>
|
||||
|
void Reset(Visual visual); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Describes a single visible page within a carousel viewport.
|
||||
|
/// </summary>
|
||||
|
public readonly record struct PageTransitionItem( |
||||
|
int Index, |
||||
|
Visual Visual, |
||||
|
double ViewportCenterOffset); |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
namespace Avalonia.Input |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Specifies the direction of a swipe gesture.
|
||||
|
/// </summary>
|
||||
|
public enum SwipeDirection |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The swipe moved to the left.
|
||||
|
/// </summary>
|
||||
|
Left, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The swipe moved to the right.
|
||||
|
/// </summary>
|
||||
|
Right, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The swipe moved upward.
|
||||
|
/// </summary>
|
||||
|
Up, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The swipe moved downward.
|
||||
|
/// </summary>
|
||||
|
Down |
||||
|
} |
||||
|
} |
||||
@ -1,50 +1,81 @@ |
|||||
|
using System; |
||||
|
using System.Threading; |
||||
using Avalonia.Interactivity; |
using Avalonia.Interactivity; |
||||
|
|
||||
namespace Avalonia.Input |
namespace Avalonia.Input |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Specifies the direction of a swipe gesture.
|
/// Provides data for swipe gesture events.
|
||||
/// </summary>
|
/// </summary>
|
||||
public enum SwipeDirection { Left, Right, Up, Down } |
public class SwipeGestureEventArgs : RoutedEventArgs |
||||
|
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Provides data for the <see cref="InputElement.SwipeGestureEvent"/> routed event.
|
/// Initializes a new instance of the <see cref="SwipeGestureEventArgs"/> class.
|
||||
/// </summary>
|
/// </summary>
|
||||
public class SwipeGestureEventArgs : RoutedEventArgs |
/// <param name="id">The unique identifier for this gesture.</param>
|
||||
|
/// <param name="delta">The pixel delta since the last event.</param>
|
||||
|
/// <param name="velocity">The current swipe velocity in pixels per second.</param>
|
||||
|
public SwipeGestureEventArgs(int id, Vector delta, Vector velocity) |
||||
|
: base(InputElement.SwipeGestureEvent) |
||||
{ |
{ |
||||
private static int _nextId = 1; |
Id = id; |
||||
internal static int GetNextFreeId() => _nextId++; |
Delta = delta; |
||||
|
Velocity = velocity; |
||||
|
SwipeDirection = Math.Abs(delta.X) >= Math.Abs(delta.Y) |
||||
|
? (delta.X <= 0 ? SwipeDirection.Right : SwipeDirection.Left) |
||||
|
: (delta.Y <= 0 ? SwipeDirection.Down : SwipeDirection.Up); |
||||
|
} |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets the unique identifier for this swipe gesture instance.
|
/// Gets the unique identifier for this gesture sequence.
|
||||
/// </summary>
|
/// </summary>
|
||||
public int Id { get; } |
public int Id { get; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets the direction of the swipe gesture.
|
/// Gets the pixel delta since the last event.
|
||||
/// </summary>
|
/// </summary>
|
||||
public SwipeDirection SwipeDirection { get; } |
public Vector Delta { get; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets the total translation vector of the swipe gesture.
|
/// Gets the current swipe velocity in pixels per second.
|
||||
/// </summary>
|
/// </summary>
|
||||
public Vector Delta { get; } |
public Vector Velocity { get; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Gets the position, relative to the target element, where the swipe started.
|
/// Gets the direction of the dominant swipe axis.
|
||||
/// </summary>
|
/// </summary>
|
||||
public Point StartPoint { get; } |
public SwipeDirection SwipeDirection { get; } |
||||
|
|
||||
|
private static int s_nextId; |
||||
|
|
||||
|
internal static int GetNextFreeId() => Interlocked.Increment(ref s_nextId); |
||||
|
} |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Initializes a new instance of <see cref="SwipeGestureEventArgs"/>.
|
/// Provides data for the swipe gesture ended event.
|
||||
/// </summary>
|
/// </summary>
|
||||
public SwipeGestureEventArgs(int id, SwipeDirection direction, Vector delta, Point startPoint) |
public class SwipeGestureEndedEventArgs : RoutedEventArgs |
||||
: base(InputElement.SwipeGestureEvent) |
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="SwipeGestureEndedEventArgs"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="id">The unique identifier for this gesture.</param>
|
||||
|
/// <param name="velocity">The swipe velocity at release in pixels per second.</param>
|
||||
|
public SwipeGestureEndedEventArgs(int id, Vector velocity) |
||||
|
: base(InputElement.SwipeGestureEndedEvent) |
||||
{ |
{ |
||||
Id = id; |
Id = id; |
||||
SwipeDirection = direction; |
Velocity = velocity; |
||||
Delta = delta; |
|
||||
StartPoint = startPoint; |
|
||||
} |
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the unique identifier for this gesture sequence.
|
||||
|
/// </summary>
|
||||
|
public int Id { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the swipe velocity at release in pixels per second.
|
||||
|
/// </summary>
|
||||
|
public Vector Velocity { get; } |
||||
} |
} |
||||
} |
} |
||||
|
|||||
File diff suppressed because it is too large
@ -0,0 +1,158 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.GestureRecognizers; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Input; |
||||
|
|
||||
|
public class SwipeGestureRecognizerTests : ScopedTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Does_Not_Raise_Swipe_When_Both_Axes_Are_Disabled() |
||||
|
{ |
||||
|
var (border, root) = CreateTarget(new SwipeGestureRecognizer { Threshold = 1 }); |
||||
|
var touch = new TouchTestHelper(); |
||||
|
var swipeRaised = false; |
||||
|
var endedRaised = false; |
||||
|
|
||||
|
root.AddHandler(InputElement.SwipeGestureEvent, (_, _) => swipeRaised = true); |
||||
|
root.AddHandler(InputElement.SwipeGestureEndedEvent, (_, _) => endedRaised = true); |
||||
|
|
||||
|
touch.Down(border, new Point(50, 50)); |
||||
|
touch.Move(border, new Point(20, 20)); |
||||
|
touch.Up(border, new Point(20, 20)); |
||||
|
|
||||
|
Assert.False(swipeRaised); |
||||
|
Assert.False(endedRaised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Defaults_Disable_Both_Axes() |
||||
|
{ |
||||
|
var recognizer = new SwipeGestureRecognizer(); |
||||
|
|
||||
|
Assert.False(recognizer.CanHorizontallySwipe); |
||||
|
Assert.False(recognizer.CanVerticallySwipe); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Starts_Only_After_Threshold_Is_Exceeded() |
||||
|
{ |
||||
|
var (border, root) = CreateTarget(new SwipeGestureRecognizer |
||||
|
{ |
||||
|
CanHorizontallySwipe = true, |
||||
|
Threshold = 50 |
||||
|
}); |
||||
|
var touch = new TouchTestHelper(); |
||||
|
var deltas = new List<Vector>(); |
||||
|
|
||||
|
root.AddHandler(InputElement.SwipeGestureEvent, (_, e) => deltas.Add(e.Delta)); |
||||
|
|
||||
|
touch.Down(border, new Point(5, 5)); |
||||
|
touch.Move(border, new Point(40, 5)); |
||||
|
|
||||
|
Assert.Empty(deltas); |
||||
|
|
||||
|
touch.Move(border, new Point(80, 5)); |
||||
|
|
||||
|
Assert.Single(deltas); |
||||
|
Assert.NotEqual(Vector.Zero, deltas[0]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Ended_Event_Uses_Same_Id_And_Last_Velocity() |
||||
|
{ |
||||
|
var (border, root) = CreateTarget(new SwipeGestureRecognizer |
||||
|
{ |
||||
|
CanHorizontallySwipe = true, |
||||
|
Threshold = 1 |
||||
|
}); |
||||
|
var touch = new TouchTestHelper(); |
||||
|
var updateIds = new List<int>(); |
||||
|
var velocities = new List<Vector>(); |
||||
|
var endedId = 0; |
||||
|
var endedVelocity = Vector.Zero; |
||||
|
|
||||
|
root.AddHandler(InputElement.SwipeGestureEvent, (_, e) => |
||||
|
{ |
||||
|
updateIds.Add(e.Id); |
||||
|
velocities.Add(e.Velocity); |
||||
|
}); |
||||
|
root.AddHandler(InputElement.SwipeGestureEndedEvent, (_, e) => |
||||
|
{ |
||||
|
endedId = e.Id; |
||||
|
endedVelocity = e.Velocity; |
||||
|
}); |
||||
|
|
||||
|
touch.Down(border, new Point(50, 50)); |
||||
|
touch.Move(border, new Point(40, 50)); |
||||
|
touch.Move(border, new Point(30, 50)); |
||||
|
touch.Up(border, new Point(30, 50)); |
||||
|
|
||||
|
Assert.True(updateIds.Count >= 2); |
||||
|
Assert.All(updateIds, id => Assert.Equal(updateIds[0], id)); |
||||
|
Assert.Equal(updateIds[0], endedId); |
||||
|
Assert.Equal(velocities[^1], endedVelocity); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Mouse_Swipe_Requires_IsMouseEnabled() |
||||
|
{ |
||||
|
var mouse = new MouseTestHelper(); |
||||
|
var (border, root) = CreateTarget(new SwipeGestureRecognizer |
||||
|
{ |
||||
|
CanHorizontallySwipe = true, |
||||
|
Threshold = 1 |
||||
|
}); |
||||
|
var swipeRaised = false; |
||||
|
|
||||
|
root.AddHandler(InputElement.SwipeGestureEvent, (_, _) => swipeRaised = true); |
||||
|
|
||||
|
mouse.Down(border, position: new Point(50, 50)); |
||||
|
mouse.Move(border, new Point(30, 50)); |
||||
|
mouse.Up(border, position: new Point(30, 50)); |
||||
|
|
||||
|
Assert.False(swipeRaised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Mouse_Swipe_Is_Raised_When_Enabled() |
||||
|
{ |
||||
|
var mouse = new MouseTestHelper(); |
||||
|
var (border, root) = CreateTarget(new SwipeGestureRecognizer |
||||
|
{ |
||||
|
CanHorizontallySwipe = true, |
||||
|
Threshold = 1, |
||||
|
IsMouseEnabled = true |
||||
|
}); |
||||
|
var swipeRaised = false; |
||||
|
|
||||
|
root.AddHandler(InputElement.SwipeGestureEvent, (_, _) => swipeRaised = true); |
||||
|
|
||||
|
mouse.Down(border, position: new Point(50, 50)); |
||||
|
mouse.Move(border, new Point(30, 50)); |
||||
|
mouse.Up(border, position: new Point(30, 50)); |
||||
|
|
||||
|
Assert.True(swipeRaised); |
||||
|
} |
||||
|
|
||||
|
private static (Border Border, TestRoot Root) CreateTarget(SwipeGestureRecognizer recognizer) |
||||
|
{ |
||||
|
var border = new Border |
||||
|
{ |
||||
|
Width = 100, |
||||
|
Height = 100 |
||||
|
}; |
||||
|
border.GestureRecognizers.Add(recognizer); |
||||
|
|
||||
|
var root = new TestRoot |
||||
|
{ |
||||
|
Child = border |
||||
|
}; |
||||
|
|
||||
|
return (border, root); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using Avalonia.Input; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Controls.UnitTests; |
||||
|
|
||||
|
public class InputElementGestureTests : ScopedTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void SwipeGestureEnded_PublicEvent_CanBeObserved() |
||||
|
{ |
||||
|
var target = new Border(); |
||||
|
SwipeGestureEndedEventArgs? received = null; |
||||
|
|
||||
|
target.SwipeGestureEnded += (_, e) => received = e; |
||||
|
|
||||
|
var args = new SwipeGestureEndedEventArgs(42, new Vector(12, 34)); |
||||
|
target.RaiseEvent(args); |
||||
|
|
||||
|
Assert.Same(args, received); |
||||
|
Assert.Equal(InputElement.SwipeGestureEndedEvent, args.RoutedEvent); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,127 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Layout; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Styling; |
||||
|
using Avalonia.Themes.Simple; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Xunit; |
||||
|
|
||||
|
#if AVALONIA_SKIA
|
||||
|
namespace Avalonia.Skia.RenderTests |
||||
|
#else
|
||||
|
namespace Avalonia.Direct2D1.RenderTests.Controls |
||||
|
#endif
|
||||
|
{ |
||||
|
public class CarouselRenderTests : TestBase |
||||
|
{ |
||||
|
public CarouselRenderTests() |
||||
|
: base(@"Controls\Carousel") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private static Style FontStyle => new Style(x => x.OfType<TextBlock>()) |
||||
|
{ |
||||
|
Setters = { new Setter(TextBlock.FontFamilyProperty, TestFontFamily) } |
||||
|
}; |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Carousel_ViewportFraction_MiddleItemSelected_ShowsSidePeeks() |
||||
|
{ |
||||
|
var carousel = new Carousel |
||||
|
{ |
||||
|
Background = Brushes.Transparent, |
||||
|
ViewportFraction = 0.8, |
||||
|
SelectedIndex = 1, |
||||
|
HorizontalAlignment = HorizontalAlignment.Stretch, |
||||
|
VerticalAlignment = VerticalAlignment.Stretch, |
||||
|
ItemsSource = new Control[] |
||||
|
{ |
||||
|
CreateCard("One", "#D8574B", "#F7C5BE"), |
||||
|
CreateCard("Two", "#3E7AD9", "#BCD0F7"), |
||||
|
CreateCard("Three", "#3D9B67", "#BEE4CB"), |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 520, |
||||
|
Height = 340, |
||||
|
Background = Brushes.White, |
||||
|
Padding = new Thickness(20), |
||||
|
Child = carousel |
||||
|
}; |
||||
|
|
||||
|
AvaloniaLocator.CurrentMutable.Bind<ICursorFactory>().ToConstant(new CursorFactoryStub()); |
||||
|
target.Styles.Add(new SimpleTheme()); |
||||
|
target.Styles.Add(FontStyle); |
||||
|
await RenderToFile(target); |
||||
|
CompareImages(skipImmediate: true); |
||||
|
} |
||||
|
|
||||
|
private static Control CreateCard(string label, string background, string accent) |
||||
|
{ |
||||
|
return new Border |
||||
|
{ |
||||
|
Margin = new Thickness(14, 12), |
||||
|
CornerRadius = new CornerRadius(18), |
||||
|
ClipToBounds = true, |
||||
|
Background = Brush.Parse(background), |
||||
|
BorderBrush = Brushes.White, |
||||
|
BorderThickness = new Thickness(2), |
||||
|
Child = new Grid |
||||
|
{ |
||||
|
Children = |
||||
|
{ |
||||
|
new Border |
||||
|
{ |
||||
|
Height = 56, |
||||
|
Background = Brush.Parse(accent), |
||||
|
VerticalAlignment = VerticalAlignment.Top |
||||
|
}, |
||||
|
new Border |
||||
|
{ |
||||
|
Width = 88, |
||||
|
Height = 88, |
||||
|
CornerRadius = new CornerRadius(44), |
||||
|
Background = Brushes.White, |
||||
|
Opacity = 0.9, |
||||
|
HorizontalAlignment = HorizontalAlignment.Center, |
||||
|
VerticalAlignment = VerticalAlignment.Center |
||||
|
}, |
||||
|
new Border |
||||
|
{ |
||||
|
Background = new SolidColorBrush(Color.Parse("#80000000")), |
||||
|
VerticalAlignment = VerticalAlignment.Bottom, |
||||
|
Padding = new Thickness(12), |
||||
|
Child = new TextBlock |
||||
|
{ |
||||
|
Text = label, |
||||
|
Foreground = Brushes.White, |
||||
|
HorizontalAlignment = HorizontalAlignment.Center, |
||||
|
FontWeight = FontWeight.SemiBold |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private sealed class CursorFactoryStub : ICursorFactory |
||||
|
{ |
||||
|
public ICursorImpl GetCursor(StandardCursorType cursorType) => new CursorStub(); |
||||
|
|
||||
|
public ICursorImpl CreateCursor(Bitmap cursor, PixelPoint hotSpot) => new CursorStub(); |
||||
|
|
||||
|
private sealed class CursorStub : ICursorImpl |
||||
|
{ |
||||
|
public void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 5.5 KiB |
Loading…
Reference in new issue