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; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
/// <summary>
|
|||
/// Specifies the direction of a swipe gesture.
|
|||
/// Provides data for swipe gesture events.
|
|||
/// </summary>
|
|||
public enum SwipeDirection { Left, Right, Up, Down } |
|||
|
|||
public class SwipeGestureEventArgs : RoutedEventArgs |
|||
{ |
|||
/// <summary>
|
|||
/// Provides data for the <see cref="InputElement.SwipeGestureEvent"/> routed event.
|
|||
/// Initializes a new instance of the <see cref="SwipeGestureEventArgs"/> class.
|
|||
/// </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; |
|||
internal static int GetNextFreeId() => _nextId++; |
|||
Id = id; |
|||
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>
|
|||
/// Gets the unique identifier for this swipe gesture instance.
|
|||
/// Gets the unique identifier for this gesture sequence.
|
|||
/// </summary>
|
|||
public int Id { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the direction of the swipe gesture.
|
|||
/// Gets the pixel delta since the last event.
|
|||
/// </summary>
|
|||
public SwipeDirection SwipeDirection { get; } |
|||
public Vector Delta { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the total translation vector of the swipe gesture.
|
|||
/// Gets the current swipe velocity in pixels per second.
|
|||
/// </summary>
|
|||
public Vector Delta { get; } |
|||
public Vector Velocity { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the position, relative to the target element, where the swipe started.
|
|||
/// Gets the direction of the dominant swipe axis.
|
|||
/// </summary>
|
|||
public Point StartPoint { get; } |
|||
public SwipeDirection SwipeDirection { get; } |
|||
|
|||
private static int s_nextId; |
|||
|
|||
internal static int GetNextFreeId() => Interlocked.Increment(ref s_nextId); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of <see cref="SwipeGestureEventArgs"/>.
|
|||
/// Provides data for the swipe gesture ended event.
|
|||
/// </summary>
|
|||
public SwipeGestureEventArgs(int id, SwipeDirection direction, Vector delta, Point startPoint) |
|||
: base(InputElement.SwipeGestureEvent) |
|||
public class SwipeGestureEndedEventArgs : RoutedEventArgs |
|||
{ |
|||
/// <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; |
|||
SwipeDirection = direction; |
|||
Delta = delta; |
|||
StartPoint = startPoint; |
|||
Velocity = velocity; |
|||
} |
|||
|
|||
/// <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