Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
4 changes: 4 additions & 0 deletions Material.Avalonia.Demo/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Material.Avalonia.Demo.Assists;

namespace Material.Avalonia.Demo;

Expand All @@ -10,6 +11,9 @@ public override void Initialize() {
}

public override void OnFrameworkInitializationCompleted() {
// initiate AutomationAssist to attach events automatically
AutomationAssist.Placeholder();

if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) {
desktop.MainWindow = new MainWindow();
}
Expand Down
86 changes: 86 additions & 0 deletions Material.Avalonia.Demo/Assists/AutomationAssist.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Linq;
using System.Reactive;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Material.Ripple;
using Material.Styles.Assists;

namespace Material.Avalonia.Demo.Assists;

public static class AutomationAssist {
#region Initiator (used for observe control changes)

// This placeholder is used for anti-optimising to keep it persisted, also triggering static constructor
internal static void Placeholder() {
Console.WriteLine("Automation assist is initiate...");
}

static AutomationAssist() {
Button.ClickEvent.Raised
.Subscribe(new AnonymousObserver<(object, RoutedEventArgs)>(OnButtonClickedPrivate));
}

#endregion

// Used for memorise which hyperlink button clicked (or just say, visited)
// this property can be attached to any buttons for use
// we have confirmed that this property doesn't need to integrate to the theming library
// but it can be used as an example
#region AttachedProperty : IsClicked

public static readonly AvaloniaProperty<bool?> IsClickedProperty =
AvaloniaProperty.RegisterAttached<Button, bool?>("IsClicked", typeof(ButtonAssist));

public static void SetIsClicked(AvaloniaObject element, bool? value) =>
element.SetValue(IsClickedProperty, value);

public static bool? GetIsClicked(AvaloniaObject element) =>
element.GetValue<bool?>(IsClickedProperty);


private static void OnButtonClickedPrivate((object, RoutedEventArgs) args) {
if (args.Item1 is not Button button)
return;

UpdateIsClickedPropertyPrivate(button);
RaiseRipplePrivate(button);
}

internal static void RaiseRipplePrivate(Control c) {
// Try to find first RippleEffect control with name PART_Ripple
var visual = c
.GetVisualDescendants()
.FirstOrDefault(a => a is RippleEffect && a.Name == "PART_Ripple");

// if such control not exist or mouse is over on it
if (visual is not RippleEffect effect || effect.IsPointerOver)
return;

effect.RaiseRipple();
}

private static void UpdateIsClickedPropertyPrivate(Button button) {
var value = GetIsClicked(button);

// null means not required for handling
if (!value.HasValue && button is not HyperlinkButton)
return;

value = false;

// if IsClickedProperty is false, put it to true
if (!value.Value)
SetIsClicked(button, true);

if (button is not HyperlinkButton hyperlink)
return;

// change Hyperlink button is visited state
hyperlink.IsVisited = true;
}

#endregion
}
58 changes: 44 additions & 14 deletions Material.Ripple/RippleEffect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@

namespace Material.Ripple {
public class RippleEffect : ContentControl {

private bool _isCancelled;

private CompositionContainerVisual? _container;
private CompositionCustomVisual? _last;
private byte _pointers;

static RippleEffect() {
BackgroundProperty.OverrideDefaultValue<RippleEffect>(Brushes.Transparent);
}

public RippleEffect() {
AddHandler(LostFocusEvent, LostFocusHandler);
AddHandler(PointerReleasedEvent, PointerReleasedHandler);
Expand Down Expand Up @@ -57,30 +56,41 @@ protected override void OnSizeChanged(SizeChangedEventArgs e) {
}

private void PointerPressedHandler(object? sender, PointerPressedEventArgs e) {
var c = _container;

if (c is null)
return;

var (x, y) = e.GetPosition(this);
if (_container is null || x < 0 || x > Bounds.Width || y < 0 || y > Bounds.Height) {

if (x < 0 || x > Bounds.Width || y < 0 || y > Bounds.Height)
return;
}

_isCancelled = false;

if (!IsAllowedRaiseRipple)
return;

CreateRippleInstancePrivate(c, x, y);

if (_isCancelled) {
RemoveLastRipple();
}
}

private void CreateRippleInstancePrivate(CompositionContainerVisual container,
double x, double y) {
// Only first pointer can arrive a ripple
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);
container.Children.Add(r);
r.SendHandlerMessage(RippleHandler.FirstStepMessage);

if (_isCancelled) {
RemoveLastRipple();
}
}

private void LostFocusHandler(object? sender, RoutedEventArgs e) {
Expand Down Expand Up @@ -110,6 +120,25 @@ private void RemoveLastRipple() {
_last = null;
}

public void RaiseRipple(double nX = 0.5, double nY = 0.5) {
var c = _container;

if (c is null || nX < 0 || nX > 1 || nY < 0 || nY > 1)
return;

lock (this) {
if (!IsAllowedRaiseRipple)
return;

var x = nX * Bounds.Width;
var y = nY * Bounds.Height;

CreateRippleInstancePrivate(c, x, y);

RemoveLastRipple();
}
}

private void OnReleaseHandler(CompositionCustomVisual r) {
// Fade out ripple
r.SendHandlerMessage(RippleHandler.SecondStepMessage);
Expand All @@ -130,7 +159,7 @@ private CompositionCustomVisual CreateRipple(double x, double y, bool center) {
x = w / 2;
y = h / 2;
}

var handler = new RippleHandler(
RippleFill.ToImmutable(),
Ripple.Easing,
Expand All @@ -147,7 +176,8 @@ private CompositionCustomVisual CreateRipple(double x, double y, bool center) {
#region Styled properties

public static readonly StyledProperty<IBrush> RippleFillProperty =
AvaloniaProperty.Register<RippleEffect, IBrush>(nameof(RippleFill), inherits: true, defaultValue: Brushes.White);
AvaloniaProperty.Register<RippleEffect, IBrush>(nameof(RippleFill), inherits: true,
defaultValue: Brushes.White);

public IBrush RippleFill {
get => GetValue(RippleFillProperty);
Expand Down Expand Up @@ -185,7 +215,7 @@ public bool UseTransitions {
get => GetValue(UseTransitionsProperty);
set => SetValue(UseTransitionsProperty, value);
}

#endregion Styled properties
}
}
3 changes: 2 additions & 1 deletion Material.Styles/Controls/FloatingButton.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
MinWidth="{TemplateBinding MinWidth}">
<Border Name="PART_HoverIndicator"
Background="{TemplateBinding assists:ButtonAssist.HoverColor}" />
<ripple:RippleEffect RippleFill="{TemplateBinding assists:ButtonAssist.ClickFeedbackColor}"
<ripple:RippleEffect Name="PART_Ripple"
RippleFill="{TemplateBinding assists:ButtonAssist.ClickFeedbackColor}"
RippleOpacity="{StaticResource ButtonPressedOpacity}">
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Expand Down