Skip to content

Commit

Permalink
Merge pull request #316 from maxkatz6/rippleRework
Browse files Browse the repository at this point in the history
Replace Ellipse-based Ripple with Composition API
  • Loading branch information
SKProCH committed Nov 18, 2023
2 parents d6445cd + e94c4a7 commit fe86323
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 210 deletions.
98 changes: 3 additions & 95 deletions Material.Ripple/Ripple.cs
Original file line number Diff line number Diff line change
@@ -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, 1, 200);
}
}
150 changes: 91 additions & 59 deletions Material.Ripple/RippleEffect.cs
Original file line number Diff line number Diff line change
@@ -1,61 +1,86 @@
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<bool> UseTransitionsProperty =
AvaloniaProperty.Register<RippleEffect, bool>(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<RippleEffect>(Brushes.Transparent);
}

public RippleEffect() {
AddHandler(LostFocusEvent, LostFocusHandler);
AddHandler(PointerReleasedEvent, PointerReleasedHandler);
AddHandler(PointerPressedEvent, PointerPressedHandler);
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) {
Expand Down Expand Up @@ -85,52 +110,51 @@ 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<Canvas>(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<IBrush> RippleFillProperty =
AvaloniaProperty.Register<RippleEffect, IBrush>(nameof(RippleFill), inherits: true);
AvaloniaProperty.Register<RippleEffect, IBrush>(nameof(RippleFill), inherits: true, defaultValue: Brushes.White);

public IBrush RippleFill {
get => GetValue(RippleFillProperty);
set => SetValue(RippleFillProperty, value);
}

public static readonly StyledProperty<double> RippleOpacityProperty =
AvaloniaProperty.Register<RippleEffect, double>(nameof(RippleOpacity), inherits: true);
AvaloniaProperty.Register<RippleEffect, double>(nameof(RippleOpacity), inherits: true, defaultValue: 0.6);

public double RippleOpacity {
get => GetValue(RippleOpacityProperty);
Expand All @@ -146,13 +170,21 @@ public bool RaiseRippleCenter {
}

public static readonly StyledProperty<bool> IsAllowedRaiseRippleProperty =
AvaloniaProperty.Register<RippleEffect, bool>(nameof(IsAllowedRaiseRipple));
AvaloniaProperty.Register<RippleEffect, bool>(nameof(IsAllowedRaiseRipple), defaultValue: true);

public bool IsAllowedRaiseRipple {
get => GetValue(IsAllowedRaiseRippleProperty);
set => SetValue(IsAllowedRaiseRippleProperty, value);
}

public static readonly StyledProperty<bool> UseTransitionsProperty =
AvaloniaProperty.Register<RippleEffect, bool>(nameof(UseTransitions), defaultValue: true);

public bool UseTransitions {
get => GetValue(UseTransitionsProperty);
set => SetValue(UseTransitionsProperty, value);
}

#endregion Styled properties
}
}
30 changes: 0 additions & 30 deletions Material.Ripple/RippleEffect.xaml

This file was deleted.

Loading

0 comments on commit fe86323

Please sign in to comment.