Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Ellipse-based Ripple with Composition API #316

Merged
merged 2 commits into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, 0, 500);
}
}
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