diff --git a/Material.Ripple/Ripple.cs b/Material.Ripple/Ripple.cs index b0b4ca90..67ae1c2d 100644 --- a/Material.Ripple/Ripple.cs +++ b/Material.Ripple/Ripple.cs @@ -1,101 +1,9 @@ using System; -using Avalonia; -using Avalonia.Animation; using Avalonia.Animation.Easings; -using Avalonia.Controls; -using Avalonia.Controls.Shapes; -using Avalonia.Input; -using Avalonia.Layout; namespace Material.Ripple { - public class Ripple : Ellipse { - public static Transitions? RippleTransitions; - - private static Easing _easing = new CircularEaseOut(); - private static TimeSpan _duration = new(0, 0, 0, 0, 500); - - private readonly double _endX; - private readonly double _endY; - - - - private readonly double _maxDiam; - - static Ripple() { - UpdateTransitions(); - } - - public Ripple(double outerWidth, double outerHeight, bool transitions = true) { - Width = 0; - Height = 0; - - _maxDiam = Math.Sqrt(Math.Pow(outerWidth, 2) + Math.Pow(outerHeight, 2)); - _endY = _maxDiam - outerHeight; - _endX = _maxDiam - outerWidth; - HorizontalAlignment = HorizontalAlignment.Left; - VerticalAlignment = VerticalAlignment.Top; - Opacity = 1; - - if (!transitions) - return; - - Transitions = RippleTransitions; - } - - public static Easing Easing { - get => _easing; - set { - _easing = value; - UpdateTransitions(); - } - } - - public static TimeSpan Duration { - get => _duration; - set { - _duration = value; - UpdateTransitions(); - } - } - - public void SetupInitialValues(PointerPressedEventArgs e, Control parent) { - var pointer = e.GetPosition(parent); - Margin = new Thickness(pointer.X, pointer.Y, 0, 0); - } - - public void RunFirstStep() { - Width = _maxDiam; - Height = _maxDiam; - Margin = new Thickness(-_endX / 2, -_endY / 2, 0, 0); - } - - public void RunSecondStep() { - Opacity = 0; - } - - private static void UpdateTransitions() { - RippleTransitions = new Transitions { - new ThicknessTransition { - Duration = Duration, - Easing = Easing, - Property = MarginProperty - }, - new DoubleTransition { - Duration = Duration, - Easing = Easing, - Property = WidthProperty - }, - new DoubleTransition { - Duration = Duration, - Easing = Easing, - Property = HeightProperty - }, - new DoubleTransition { - Duration = Duration, - Easing = Easing, - Property = OpacityProperty - } - }; - } + public static class Ripple { + public static Easing Easing { get; set; } = new CircularEaseOut(); + public static TimeSpan Duration { get; set; } = new(0, 0, 0, 0, 500); } } \ No newline at end of file diff --git a/Material.Ripple/RippleEffect.cs b/Material.Ripple/RippleEffect.cs index ea2c3f82..9c277247 100644 --- a/Material.Ripple/RippleEffect.cs +++ b/Material.Ripple/RippleEffect.cs @@ -1,24 +1,24 @@ -using System.Threading.Tasks; -using Avalonia; +using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Rendering.Composition; using Avalonia.Threading; namespace Material.Ripple { public class RippleEffect : ContentControl { - public static readonly StyledProperty UseTransitionsProperty = - AvaloniaProperty.Register(nameof(UseTransitions)); + private bool _isCancelled; - private Ripple? _last; + private CompositionContainerVisual? _container; + private CompositionCustomVisual? _last; private byte _pointers; - - // ReSharper disable once InconsistentNaming - private Canvas PART_RippleCanvasRoot = null!; - + + static RippleEffect() { + BackgroundProperty.OverrideDefaultValue(Brushes.Transparent); + } + public RippleEffect() { AddHandler(LostFocusEvent, LostFocusHandler); AddHandler(PointerReleasedEvent, PointerReleasedHandler); @@ -26,36 +26,61 @@ public RippleEffect() { AddHandler(PointerCaptureLostEvent, PointerCaptureLostHandler); } - public bool UseTransitions { - get => GetValue(UseTransitionsProperty); - set => SetValue(UseTransitionsProperty, value); + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { + base.OnAttachedToVisualTree(e); + + var thisVisual = ElementComposition.GetElementVisual(this)!; + _container = thisVisual.Compositor.CreateContainerVisual(); + _container.Size = new Vector(Bounds.Width, Bounds.Height); + ElementComposition.SetElementChildVisual(this, _container); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { + base.OnDetachedFromVisualTree(e); + + _container = null; + ElementComposition.SetElementChildVisual(this, null); + } + + protected override void OnSizeChanged(SizeChangedEventArgs e) { + base.OnSizeChanged(e); + + if (_container is { } container) { + var newSize = new Vector(e.NewSize.Width, e.NewSize.Height); + if (newSize != default) { + container.Size = newSize; + foreach (var child in container.Children) { + child.Size = newSize; + } + } + } } private void PointerPressedHandler(object sender, PointerPressedEventArgs e) { var (x, y) = e.GetPosition(this); - if (x < 0 || x > Bounds.Width || y < 0 || y > Bounds.Height) { + if (_container is null || x < 0 || x > Bounds.Width || y < 0 || y > Bounds.Height) { return; } _isCancelled = false; - Dispatcher.UIThread.InvokeAsync(delegate { - if (!IsAllowedRaiseRipple) - return; - - if (_pointers != 0) - return; - - // Only first pointer can arrive a ripple - _pointers++; - var r = CreateRipple(e, RaiseRippleCenter); - _last = r; - - // Attach ripple instance to canvas - PART_RippleCanvasRoot.Children.Add(r); - r.RunFirstStep(); - if (_isCancelled) { - RemoveLastRipple(); - } - }, DispatcherPriority.Render); + + if (!IsAllowedRaiseRipple) + return; + + if (_pointers != 0) + return; + + // Only first pointer can arrive a ripple + _pointers++; + var r = CreateRipple(x, y, RaiseRippleCenter); + _last = r; + + // Attach ripple instance to canvas + _container.Children.Add(r); + r.SendHandlerMessage(RippleHandler.FirstStepMessage); + + if (_isCancelled) { + RemoveLastRipple(); + } } private void LostFocusHandler(object sender, RoutedEventArgs e) { @@ -85,44 +110,43 @@ private void RemoveLastRipple() { _last = null; } - private void OnReleaseHandler(Ripple r) { + private void OnReleaseHandler(CompositionCustomVisual r) { // Fade out ripple - r.RunSecondStep(); - - void RemoveRippleTask(Task arg1, object arg2) { - Dispatcher.UIThread.InvokeAsync(delegate { PART_RippleCanvasRoot.Children.Remove(r); }, DispatcherPriority.Render); - } + r.SendHandlerMessage(RippleHandler.SecondStepMessage); // Remove ripple from canvas to finalize ripple instance - Task.Delay(Ripple.Duration).ContinueWith(RemoveRippleTask, null); - } - - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - base.OnApplyTemplate(e); - - // Find canvas host - PART_RippleCanvasRoot = e.NameScope.Find(nameof(PART_RippleCanvasRoot))!; + var container = _container; + DispatcherTimer.RunOnce(() => { + container?.Children.Remove(r); + }, Ripple.Duration, DispatcherPriority.Render); } - private Ripple CreateRipple(PointerPressedEventArgs e, bool center) { + private CompositionCustomVisual CreateRipple(double x, double y, bool center) { var w = Bounds.Width; var h = Bounds.Height; var t = UseTransitions; - var r = new Ripple(w, h, t) { - Fill = RippleFill - }; - - if (center) r.Margin = new Thickness(w / 2, h / 2, 0, 0); - else r.SetupInitialValues(e, this); - - return r; + if (center) { + x = w / 2; + y = h / 2; + } + + var handler = new RippleHandler( + RippleFill.ToImmutable(), + Ripple.Easing, + Ripple.Duration, + RippleOpacity, + x, y, w, h, t); + + var visual = ElementComposition.GetElementVisual(this)!.Compositor.CreateCustomVisual(handler); + visual.Size = new Vector(Bounds.Width, Bounds.Height); + return visual; } #region Styled properties public static readonly StyledProperty RippleFillProperty = - AvaloniaProperty.Register(nameof(RippleFill), inherits: true); + AvaloniaProperty.Register(nameof(RippleFill), inherits: true, defaultValue: Brushes.White); public IBrush RippleFill { get => GetValue(RippleFillProperty); @@ -130,7 +154,7 @@ public IBrush RippleFill { } public static readonly StyledProperty RippleOpacityProperty = - AvaloniaProperty.Register(nameof(RippleOpacity), inherits: true); + AvaloniaProperty.Register(nameof(RippleOpacity), inherits: true, defaultValue: 0.6); public double RippleOpacity { get => GetValue(RippleOpacityProperty); @@ -146,13 +170,21 @@ public bool RaiseRippleCenter { } public static readonly StyledProperty IsAllowedRaiseRippleProperty = - AvaloniaProperty.Register(nameof(IsAllowedRaiseRipple)); + AvaloniaProperty.Register(nameof(IsAllowedRaiseRipple), defaultValue: true); public bool IsAllowedRaiseRipple { get => GetValue(IsAllowedRaiseRippleProperty); set => SetValue(IsAllowedRaiseRippleProperty, value); } + public static readonly StyledProperty UseTransitionsProperty = + AvaloniaProperty.Register(nameof(UseTransitions), defaultValue: true); + + public bool UseTransitions { + get => GetValue(UseTransitionsProperty); + set => SetValue(UseTransitionsProperty, value); + } + #endregion Styled properties } } \ No newline at end of file diff --git a/Material.Ripple/RippleEffect.xaml b/Material.Ripple/RippleEffect.xaml deleted file mode 100644 index ffd445fe..00000000 --- a/Material.Ripple/RippleEffect.xaml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Material.Ripple/RippleHandler.cs b/Material.Ripple/RippleHandler.cs new file mode 100644 index 00000000..51c0815d --- /dev/null +++ b/Material.Ripple/RippleHandler.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia; +using Avalonia.Animation.Easings; +using Avalonia.Media; +using Avalonia.Rendering.Composition; + +namespace Material.Ripple; + +internal class RippleHandler : CompositionCustomVisualHandler { + private TimeSpan _animationElapsed; + private TimeSpan? _lastServerTime; + private TimeSpan? _secondStepStart; + + private readonly IImmutableBrush _brush; + private readonly Point _center; + private readonly Easing _easing; + private readonly TimeSpan _duration; + private readonly double _opacity; + private readonly bool _transitions; + + public static readonly object FirstStepMessage = new(), SecondStepMessage = new(); + + private readonly double _maxRadius; + + public RippleHandler( + IImmutableBrush brush, + Easing easing, + TimeSpan duration, + double opacity, + double positionX, double positionY, + double outerWidth, double outerHeight, bool transitions) { + + _brush = brush; + _easing = easing; + _duration = duration; + _opacity = opacity; + _transitions = transitions; + _center = new Point(positionX, positionY); + + _maxRadius = Math.Sqrt(Math.Pow(outerWidth, 2) + Math.Pow(outerHeight, 2)) / 2; + } + + public override void OnRender(ImmediateDrawingContext drawingContext) { + if (_lastServerTime.HasValue) _animationElapsed += (CompositionNow - _lastServerTime.Value); + _lastServerTime = CompositionNow; + + var currentRadius = _maxRadius; + var currentOpacity = _opacity; + + if (_transitions) { + var expandingStep = _easing.Ease((double)_animationElapsed.Ticks / _duration.Ticks); + currentRadius = _maxRadius * expandingStep; + + if (_secondStepStart is { } secondStepStart) { + var opacityStep = _easing.Ease((double)(_animationElapsed - secondStepStart).Ticks / + (_duration - secondStepStart).Ticks); + currentOpacity = _opacity - _opacity * opacityStep; + } + } + + using (drawingContext.PushOpacity(currentOpacity, default)) { + drawingContext.DrawEllipse(_brush, null, _center, currentRadius, currentRadius); + } + } + + public override void OnMessage(object message) { + if (message == FirstStepMessage) { + _lastServerTime = null; + _secondStepStart = null; + RegisterForNextAnimationFrameUpdate(); + } + else if (message == SecondStepMessage) { + _secondStepStart = _animationElapsed; + } + } + + public override void OnAnimationFrameUpdate() { + if (_animationElapsed >= _duration) return; + Invalidate(); + RegisterForNextAnimationFrameUpdate(); + } +} \ No newline at end of file diff --git a/Material.Ripple/Theme.axaml b/Material.Ripple/Theme.axaml index 7aa22998..751cd74c 100644 --- a/Material.Ripple/Theme.axaml +++ b/Material.Ripple/Theme.axaml @@ -2,32 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cc="clr-namespace:Material.Ripple"> - - - - - - - - - - - - - + TargetType="cc:RippleEffect" + BasedOn="{StaticResource {x:Type ContentControl}}">