From 0e75f3f5c8d6f30de2b18d880a70065e5f364017 Mon Sep 17 00:00:00 2001 From: Dinis Vieira Date: Fri, 10 Nov 2023 23:30:54 +0000 Subject: [PATCH 1/3] PM-3349 Fix for HTML Label on Android. Color hex doesn't need to be cropped anymore. --- src/Core/Utilities/GeneratedValueFormatter.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Core/Utilities/GeneratedValueFormatter.cs b/src/Core/Utilities/GeneratedValueFormatter.cs index 20f2d12aa..e36557d4d 100644 --- a/src/Core/Utilities/GeneratedValueFormatter.cs +++ b/src/Core/Utilities/GeneratedValueFormatter.cs @@ -27,11 +27,9 @@ namespace Bit.App.Utilities return string.Empty; } - // First two digits of returned hex code contains the alpha, - // which is not supported in HTML color, so we need to cut those out. - var normalColor = $""; - var numberColor = $""; - var specialColor = $""; + var normalColor = $""; + var numberColor = $""; + var specialColor = $""; var result = string.Empty; // iOS won't hide the zero-width space char without these div attrs, but Android won't respect From a31f15559ff2871a1028701ca2b496f0766aad9c Mon Sep 17 00:00:00 2001 From: Dinis Vieira Date: Sat, 11 Nov 2023 15:05:51 +0000 Subject: [PATCH 2/3] PM-3350 Fix for colored html text on iOS --- src/Core/Controls/MonoLabel.cs | 4 ---- src/Core/Resources/Styles/Base.xaml | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Core/Controls/MonoLabel.cs b/src/Core/Controls/MonoLabel.cs index 35a26fd02..665269d03 100644 --- a/src/Core/Controls/MonoLabel.cs +++ b/src/Core/Controls/MonoLabel.cs @@ -7,10 +7,6 @@ if (DeviceInfo.Platform == DevicePlatform.iOS) { FontFamily = "Menlo-Regular"; - - //[MAUI-Migration] Temporary Workaround for the Text to appear in iOS. - // A proper solution needs to be found to be able to have html text with different colors or alternatively use Label FormattedString Spans - TextColor = Colors.Black; } else if (DeviceInfo.Platform == DevicePlatform.Android) { diff --git a/src/Core/Resources/Styles/Base.xaml b/src/Core/Resources/Styles/Base.xaml index 81727de02..ca825485a 100644 --- a/src/Core/Resources/Styles/Base.xaml +++ b/src/Core/Resources/Styles/Base.xaml @@ -59,12 +59,11 @@ - From 27306fe3533411e5c10b6633d61c56d71255ed16 Mon Sep 17 00:00:00 2001 From: Dinis Vieira Date: Mon, 13 Nov 2023 09:59:54 +0000 Subject: [PATCH 3/3] PM-3349 PM-3350 Added the partial MAUI Community Toolkit implementation for TouchEffect. This is a temporary solution until they finalize this and add it to their nuget package. This allows implementing the LongPressCommand in AccountSwitchingOverlay and also have the "Ripple effect" animation when touching an item in Android --- .../Extensions/VisualElementExtensions.cs | 72 + .../MCTTouch/GestureManager.cs | 789 +++++++++++ .../MCTTouch/Primitives/HoverState.shared.cs | 18 + .../HoverStateChangedEventArgs.shared.cs | 15 + .../MCTTouch/Primitives/HoverStatus.shared.cs | 16 + .../HoverStatusChangedEventArgs.shared.cs | 15 + .../LongPressCompletedEventArgs.shared.cs | 15 + .../TouchCompletedEventArgs.shared.cs | 19 + .../TouchInteractionStatus.shared.cs | 17 + ...nteractionStatusChangedEventArgs.shared.cs | 15 + .../MCTTouch/Primitives/TouchState.shared.cs | 17 + .../TouchStateChangedEventArgs.shared.cs | 15 + .../MCTTouch/Primitives/TouchStatus.shared.cs | 22 + .../TouchStatusChangedEventArgs.shared.cs | 15 + .../MCTTouch/TouchBehavior.Methods.shared.cs | 162 +++ .../MCTTouch/TouchBehavior.android.cs | 555 ++++++++ .../MCTTouch/TouchBehavior.macios.cs | 308 +++++ .../MCTTouch/TouchBehavior.shared.cs | 1208 +++++++++++++++++ .../AccountSwitchingOverlayView.xaml.cs | 2 +- .../AccountViewCell/AccountViewCell.xaml | 13 +- src/Core/Controls/ExtendedGrid.cs | 16 +- src/Core/Controls/ExtendedStackLayout.cs | 16 +- src/Core/Pages/Send/SendAddEditPage.xaml | 2 +- 23 files changed, 3331 insertions(+), 11 deletions(-) create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Extensions/VisualElementExtensions.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/GestureManager.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverState.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStateChangedEventArgs.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStatus.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStatusChangedEventArgs.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/LongPressCompletedEventArgs.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchCompletedEventArgs.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchInteractionStatus.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchInteractionStatusChangedEventArgs.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchState.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStateChangedEventArgs.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStatus.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStatusChangedEventArgs.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.Methods.shared.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.android.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.macios.cs create mode 100644 src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.shared.cs diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Extensions/VisualElementExtensions.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Extensions/VisualElementExtensions.cs new file mode 100644 index 000000000..2532aa844 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Extensions/VisualElementExtensions.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ViewExtensions = Microsoft.Maui.Controls.ViewExtensions; + +namespace CommunityToolkit.Maui.Extensions; +static class VisualElementExtensions +{ + internal static bool TryFindParentElementWithParentOfType(this VisualElement? element, out VisualElement? result, out T? parent) where T : VisualElement + { + result = null; + parent = null; + + while (element?.Parent is not null) + { + if (element.Parent is not T parentElement) + { + element = element.Parent as VisualElement; + continue; + } + + result = element; + parent = parentElement; + + return true; + } + + return false; + } + + public static Task ColorTo(this VisualElement element, Color color, uint length = 250u, Easing? easing = null) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + + var animationCompletionSource = new TaskCompletionSource(); + + if (element.BackgroundColor is null) + { + return Task.FromResult(false); + } + + new Animation + { + { 0, 1, new Animation(v => element.BackgroundColor = new Color((float)v, element.BackgroundColor.Green, element.BackgroundColor.Blue, element.BackgroundColor.Alpha), element.BackgroundColor.Red, color.Red) }, + { 0, 1, new Animation(v => element.BackgroundColor = new Color(element.BackgroundColor.Red, (float)v, element.BackgroundColor.Blue, element.BackgroundColor.Alpha), element.BackgroundColor.Green, color.Green) }, + { 0, 1, new Animation(v => element.BackgroundColor = new Color(element.BackgroundColor.Red, element.BackgroundColor.Green, (float)v, element.BackgroundColor.Alpha), element.BackgroundColor.Blue, color.Blue) }, + { 0, 1, new Animation(v => element.BackgroundColor = new Color(element.BackgroundColor.Red, element.BackgroundColor.Green, element.BackgroundColor.Blue, (float)v), element.BackgroundColor.Alpha, color.Alpha) }, + }.Commit(element, nameof(ColorTo), 16, length, easing, (d, b) => animationCompletionSource.SetResult(true)); + + return animationCompletionSource.Task; + } + + public static void AbortAnimations(this VisualElement element, params string[] otherAnimationNames) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + + ViewExtensions.CancelAnimations(element); + element.AbortAnimation(nameof(ColorTo)); + + if (otherAnimationNames is null) + { + return; + } + + foreach (var name in otherAnimationNames) + { + element.AbortAnimation(name); + } + } +} diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/GestureManager.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/GestureManager.cs new file mode 100644 index 000000000..1d02e85d7 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/GestureManager.cs @@ -0,0 +1,789 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CommunityToolkit.Maui.Extensions; +using static System.Math; + +namespace CommunityToolkit.Maui.Behaviors; + +sealed class GestureManager : IDisposable +{ + const int animationProgressDelay = 10; + + Color? defaultBackgroundColor; + + CancellationTokenSource? longPressTokenSource; + + CancellationTokenSource? animationTokenSource; + + Func? animationTaskFactory; + + double? durationMultiplier; + + double animationProgress; + + TouchState animationState; + + internal void HandleTouch(TouchBehavior sender, TouchStatus status) + { + if (sender.IsDisabled) + { + return; + } + + var canExecuteAction = sender.CanExecute; + if (status != TouchStatus.Started || canExecuteAction) + { + if (!canExecuteAction) + { + status = TouchStatus.Canceled; + } + + var state = status == TouchStatus.Started + ? TouchState.Pressed + : TouchState.Normal; + + if (status == TouchStatus.Started) + { + animationProgress = 0; + animationState = state; + } + + var isToggled = sender.IsToggled; + if (isToggled.HasValue) + { + if (status != TouchStatus.Started) + { + durationMultiplier = (animationState == TouchState.Pressed && !isToggled.Value) || + (animationState == TouchState.Normal && isToggled.Value) + ? 1 - animationProgress + : animationProgress; + + UpdateStatusAndState(sender, status, state); + + if (status == TouchStatus.Canceled) + { + sender.ForceUpdateState(false); + return; + } + + OnTapped(sender); + sender.IsToggled = !isToggled; + return; + } + + state = isToggled.Value + ? TouchState.Normal + : TouchState.Pressed; + } + + UpdateStatusAndState(sender, status, state); + } + + if (status == TouchStatus.Completed) + { + OnTapped(sender); + } + } + + internal void HandleUserInteraction(TouchBehavior sender, TouchInteractionStatus interactionStatus) + { + if (sender.InteractionStatus != interactionStatus) + { + sender.InteractionStatus = interactionStatus; + sender.RaiseInteractionStatusChanged(); + } + } + + internal void HandleHover(TouchBehavior sender, HoverStatus status) + { + if (!sender.Element?.IsEnabled ?? true) + { + return; + } + + var hoverState = status == HoverStatus.Entered + ? HoverState.Hovered + : HoverState.Normal; + + if (sender.HoverState != hoverState) + { + sender.HoverState = hoverState; + sender.RaiseHoverStateChanged(); + } + + if (sender.HoverStatus != status) + { + sender.HoverStatus = status; + sender.RaiseHoverStatusChanged(); + } + } + + internal async Task ChangeStateAsync(TouchBehavior sender, bool animated) + { + var status = sender.Status; + var state = sender.State; + var hoverState = sender.HoverState; + + AbortAnimations(sender); + animationTokenSource = new CancellationTokenSource(); + var token = animationTokenSource.Token; + + var isToggled = sender.IsToggled; + + if (sender.Element is not null) + { + UpdateVisualState(sender.Element, state, hoverState); + } + + if (!animated) + { + if (isToggled.HasValue) + { + state = isToggled.Value + ? TouchState.Pressed + : TouchState.Normal; + } + + var durationMultiplier = this.durationMultiplier; + this.durationMultiplier = null; + + await RunAnimationTask(sender, state, hoverState, animationTokenSource.Token, durationMultiplier.GetValueOrDefault()).ConfigureAwait(false); + return; + } + + var pulseCount = sender.PulseCount; + + if (pulseCount == 0 || (state == TouchState.Normal && !isToggled.HasValue)) + { + if (isToggled.HasValue) + { + Console.WriteLine($"Touch state: {status}"); + var r = (status == TouchStatus.Started && isToggled.Value) || + (status != TouchStatus.Started && !isToggled.Value); + state = r + ? TouchState.Normal + : TouchState.Pressed; + } + + await RunAnimationTask(sender, state, hoverState, animationTokenSource.Token).ConfigureAwait(false); + return; + } + do + { + var rippleState = isToggled.HasValue && isToggled.Value + ? TouchState.Normal + : TouchState.Pressed; + + await RunAnimationTask(sender, rippleState, hoverState, animationTokenSource.Token); + if (token.IsCancellationRequested) + { + return; + } + + rippleState = isToggled.HasValue && isToggled.Value + ? TouchState.Pressed + : TouchState.Normal; + + await RunAnimationTask(sender, rippleState, hoverState, animationTokenSource.Token); + if (token.IsCancellationRequested) + { + return; + } + } + while (--pulseCount != 0); + } + + internal void HandleLongPress(TouchBehavior sender) + { + if (sender.State == TouchState.Normal) + { + longPressTokenSource?.Cancel(); + longPressTokenSource?.Dispose(); + longPressTokenSource = null; + return; + } + + if (sender.LongPressCommand is null || sender.InteractionStatus == TouchInteractionStatus.Completed) + { + return; + } + + longPressTokenSource = new CancellationTokenSource(); + Task.Delay(sender.LongPressDuration, longPressTokenSource.Token).ContinueWith(t => + { + if (t.IsFaulted && t.Exception != null) + { + throw t.Exception; + } + + if (t.IsCanceled) + { + return; + } + + var longPressAction = new Action(() => + { + sender.HandleUserInteraction(TouchInteractionStatus.Completed); + sender.RaiseLongPressCompleted(); + }); + + if (sender.Dispatcher.IsDispatchRequired) + { + sender.Dispatcher.Dispatch(longPressAction); + } + else + { + longPressAction.Invoke(); + } + }); + } + + internal void SetCustomAnimationTask(Func? animationTaskFactory) + => this.animationTaskFactory = animationTaskFactory; + + internal void Reset() + { + SetCustomAnimationTask(null); + defaultBackgroundColor = default; + } + + internal void OnTapped(TouchBehavior sender) + { + if (!sender.CanExecute || (sender.LongPressCommand != null && sender.InteractionStatus == TouchInteractionStatus.Completed)) + { + return; + } + + if (DeviceInfo.Platform == DevicePlatform.Android) + { + HandleCollectionViewSelection(sender); + } + + if (sender.Element is IButtonController button) + { + button.SendClicked(); + } + + sender.RaiseCompleted(); + } + + void HandleCollectionViewSelection(TouchBehavior sender) + { + CollectionView? parent = null; + VisualElement? result = null; + if (!sender.Element?.TryFindParentElementWithParentOfType(out result, out parent) ?? true) + { + return; + } + + var collectionView = parent ?? throw new NullReferenceException(); + + var item = result?.BindingContext ?? result ?? throw new NullReferenceException(); + + switch (collectionView.SelectionMode) + { + case SelectionMode.Single: + collectionView.SelectedItem = item; + break; + case SelectionMode.Multiple: + var selectedItems = collectionView.SelectedItems?.ToList() ?? new List(); + + if (selectedItems.Contains(item)) + { + selectedItems.Remove(item); + } + else + { + selectedItems.Add(item); + } + + collectionView.UpdateSelectedItems(selectedItems); + break; + } + } + + internal void AbortAnimations(TouchBehavior sender) + { + animationTokenSource?.Cancel(); + animationTokenSource?.Dispose(); + animationTokenSource = null; + var element = sender.Element; + if (element == null) + { + return; + } + + element.AbortAnimations(); + } + + void UpdateStatusAndState(TouchBehavior sender, TouchStatus status, TouchState state) + { + sender.Status = status; + sender.RaiseStatusChanged(); + + if (sender.State != state || status != TouchStatus.Canceled) + { + sender.State = state; + sender.RaiseStateChanged(); + } + } + + void UpdateVisualState(VisualElement visualElement, TouchState touchState, HoverState hoverState) + { + var state = touchState == TouchState.Pressed + ? TouchBehavior.PressedVisualState + : hoverState == HoverState.Hovered + ? TouchBehavior.HoveredVisualState + : TouchBehavior.UnpressedVisualState; + + VisualStateManager.GoToState(visualElement, state); + } + + async Task SetBackgroundImageAsync(TouchBehavior sender, TouchState touchState, HoverState hoverState, int duration, CancellationToken token) + { + var normalBackgroundImageSource = sender.NormalBackgroundImageSource; + var pressedBackgroundImageSource = sender.PressedBackgroundImageSource; + var hoveredBackgroundImageSource = sender.HoveredBackgroundImageSource; + + if (normalBackgroundImageSource is null && + pressedBackgroundImageSource is null && + hoveredBackgroundImageSource is null) + { + return; + } + + var aspect = sender.BackgroundImageAspect; + var source = normalBackgroundImageSource; + if (touchState == TouchState.Pressed) + { + if (sender.IsSet(TouchBehavior.PressedBackgroundImageAspectProperty)) + { + aspect = sender.PressedBackgroundImageAspect; + } + + source = pressedBackgroundImageSource; + } + else if (hoverState == HoverState.Hovered) + { + if (sender.IsSet(TouchBehavior.HoveredBackgroundImageAspectProperty)) + { + aspect = sender.HoveredBackgroundImageAspect; + } + + if (sender.IsSet(TouchBehavior.HoveredBackgroundImageSourceProperty)) + { + source = hoveredBackgroundImageSource; + } + } + else + { + if (sender.IsSet(TouchBehavior.NormalBackgroundImageAspectProperty)) + { + aspect = sender.NormalBackgroundImageAspect; + } + } + + try + { + if (sender.ShouldSetImageOnAnimationEnd && duration > 0) + { + await Task.Delay(duration, token); + } + } + catch (TaskCanceledException) + { + return; + } + + if (sender.Element is Image image) + { + using (image.Batch()) + { + image.Aspect = aspect; + image.Source = source; + } + } + } + + Task SetBackgroundColor(TouchBehavior sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) + { + var normalBackgroundColor = sender.NormalBackgroundColor; + var pressedBackgroundColor = sender.PressedBackgroundColor; + var hoveredBackgroundColor = sender.HoveredBackgroundColor; + + if (sender.Element == null + || (normalBackgroundColor is null + && pressedBackgroundColor is null + && hoveredBackgroundColor is null)) + { + return Task.FromResult(false); + } + + var element = sender.Element; + if (defaultBackgroundColor == default) + { + defaultBackgroundColor = element.BackgroundColor; + } + + var color = GetBackgroundColor(normalBackgroundColor); + + if (touchState == TouchState.Pressed) + { + color = GetBackgroundColor(pressedBackgroundColor); + } + else if (hoverState == HoverState.Hovered && sender.IsSet(TouchBehavior.HoveredBackgroundColorProperty)) + { + color = GetBackgroundColor(hoveredBackgroundColor); + } + + if (duration <= 0) + { + element.AbortAnimations(); + element.BackgroundColor = color; + return Task.FromResult(true); + } + + return element.ColorTo(color ?? Colors.Transparent, (uint)duration, easing); + } + + Task SetOpacity(TouchBehavior sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) + { + var normalOpacity = sender.NormalOpacity; + var pressedOpacity = sender.PressedOpacity; + var hoveredOpacity = sender.HoveredOpacity; + + if (Abs(normalOpacity - 1) <= double.Epsilon && + Abs(pressedOpacity - 1) <= double.Epsilon && + Abs(hoveredOpacity - 1) <= double.Epsilon) + { + return Task.FromResult(false); + } + + var opacity = normalOpacity; + + if (touchState == TouchState.Pressed) + { + opacity = pressedOpacity; + } + else if (hoverState == HoverState.Hovered && sender.IsSet(TouchBehavior.HoveredOpacityProperty)) + { + opacity = hoveredOpacity; + } + + var element = sender.Element; + if (duration <= 0 && element is not null) + { + element.AbortAnimations(); + element.Opacity = opacity; + return Task.FromResult(true); + } + + return element is null ? + Task.FromResult(false) : + element.FadeTo(opacity, (uint)Abs(duration), easing); + } + + Task SetScale(TouchBehavior sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) + { + var normalScale = sender.NormalScale; + var pressedScale = sender.PressedScale; + var hoveredScale = sender.HoveredScale; + + if (Abs(normalScale - 1) <= double.Epsilon && + Abs(pressedScale - 1) <= double.Epsilon && + Abs(hoveredScale - 1) <= double.Epsilon) + { + return Task.FromResult(false); + } + + var scale = normalScale; + + if (touchState == TouchState.Pressed) + { + scale = pressedScale; + } + else if (hoverState == HoverState.Hovered && sender.IsSet(TouchBehavior.HoveredScaleProperty)) + { + scale = hoveredScale; + } + + var element = sender.Element; + if (element is null) + { + return Task.FromResult(false); + } + + if (duration <= 0) + { + element.AbortAnimations(nameof(SetScale)); + element.Scale = scale; + return Task.FromResult(true); + } + + var animationCompletionSource = new TaskCompletionSource(); + element.Animate(nameof(SetScale), v => + { + if (double.IsNaN(v)) + { + return; + } + + element.Scale = v; + }, element.Scale, scale, 16, (uint)Abs(duration), easing, (v, b) => animationCompletionSource.SetResult(b)); + return animationCompletionSource.Task; + } + + Task SetTranslation(TouchBehavior sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) + { + var normalTranslationX = sender.NormalTranslationX; + var pressedTranslationX = sender.PressedTranslationX; + var hoveredTranslationX = sender.HoveredTranslationX; + + var normalTranslationY = sender.NormalTranslationY; + var pressedTranslationY = sender.PressedTranslationY; + var hoveredTranslationY = sender.HoveredTranslationY; + + if (Abs(normalTranslationX) <= double.Epsilon + && Abs(pressedTranslationX) <= double.Epsilon + && Abs(hoveredTranslationX) <= double.Epsilon + && Abs(normalTranslationY) <= double.Epsilon + && Abs(pressedTranslationY) <= double.Epsilon + && Abs(hoveredTranslationY) <= double.Epsilon) + { + return Task.FromResult(false); + } + + var translationX = normalTranslationX; + var translationY = normalTranslationY; + + if (touchState == TouchState.Pressed) + { + translationX = pressedTranslationX; + translationY = pressedTranslationY; + } + else if (hoverState == HoverState.Hovered) + { + if (sender.IsSet(TouchBehavior.HoveredTranslationXProperty)) + { + translationX = hoveredTranslationX; + } + + if (sender.IsSet(TouchBehavior.HoveredTranslationYProperty)) + { + translationY = hoveredTranslationY; + } + } + + var element = sender.Element; + if (duration <= 0 && element != null) + { + element.AbortAnimations(); + element.TranslationX = translationX; + element.TranslationY = translationY; + return Task.FromResult(true); + } + + return element?.TranslateTo(translationX, translationY, (uint)Abs(duration), easing) ?? Task.FromResult(false); + } + + Task SetRotation(TouchBehavior sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) + { + var normalRotation = sender.NormalRotation; + var pressedRotation = sender.PressedRotation; + var hoveredRotation = sender.HoveredRotation; + + if (Abs(normalRotation) <= double.Epsilon + && Abs(pressedRotation) <= double.Epsilon + && Abs(hoveredRotation) <= double.Epsilon) + { + return Task.FromResult(false); + } + + var rotation = normalRotation; + + if (touchState == TouchState.Pressed) + { + rotation = pressedRotation; + } + else if (hoverState == HoverState.Hovered && sender.IsSet(TouchBehavior.HoveredRotationProperty)) + { + rotation = hoveredRotation; + } + + var element = sender.Element; + if (duration <= 0 && element != null) + { + element.AbortAnimations(); + element.Rotation = rotation; + return Task.FromResult(true); + } + + return element?.RotateTo(rotation, (uint)Abs(duration), easing) ?? Task.FromResult(false); + } + + Task SetRotationX(TouchBehavior sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) + { + var normalRotationX = sender.NormalRotationX; + var pressedRotationX = sender.PressedRotationX; + var hoveredRotationX = sender.HoveredRotationX; + + if (Abs(normalRotationX) <= double.Epsilon && + Abs(pressedRotationX) <= double.Epsilon && + Abs(hoveredRotationX) <= double.Epsilon) + { + return Task.FromResult(false); + } + + var rotationX = normalRotationX; + + if (touchState == TouchState.Pressed) + { + rotationX = pressedRotationX; + } + else if (hoverState == HoverState.Hovered && sender.IsSet(TouchBehavior.HoveredRotationXProperty)) + { + rotationX = hoveredRotationX; + } + + var element = sender.Element; + if (duration <= 0 && element != null) + { + element.AbortAnimations(); + element.RotationX = rotationX; + return Task.FromResult(true); + } + + return element?.RotateXTo(rotationX, (uint)Abs(duration), easing) ?? Task.FromResult(false); + } + + Task SetRotationY(TouchBehavior sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) + { + var normalRotationY = sender.NormalRotationY; + var pressedRotationY = sender.PressedRotationY; + var hoveredRotationY = sender.HoveredRotationY; + + if (Abs(normalRotationY) <= double.Epsilon && + Abs(pressedRotationY) <= double.Epsilon && + Abs(hoveredRotationY) <= double.Epsilon) + { + return Task.FromResult(false); + } + + var rotationY = normalRotationY; + + if (touchState == TouchState.Pressed) + { + rotationY = pressedRotationY; + } + else if (hoverState == HoverState.Hovered && sender.IsSet(TouchBehavior.HoveredRotationYProperty)) + { + rotationY = hoveredRotationY; + } + + var element = sender.Element; + if (duration <= 0 && element != null) + { + element.AbortAnimations(); + element.RotationY = rotationY; + return Task.FromResult(true); + } + + return element?.RotateYTo(rotationY, (uint)Abs(duration), easing) ?? Task.FromResult(false); + } + + Color? GetBackgroundColor(Color? color) + => color is not null + ? color + : defaultBackgroundColor; + + Task RunAnimationTask(TouchBehavior sender, TouchState touchState, HoverState hoverState, CancellationToken token, double? durationMultiplier = null) + { + if (sender.Element == null) + { + return Task.FromResult(false); + } + + var duration = sender.AnimationDuration; + var easing = sender.AnimationEasing; + + if (touchState == TouchState.Pressed) + { + if (sender.IsSet(TouchBehavior.PressedAnimationDurationProperty)) + { + duration = sender.PressedAnimationDuration; + } + + if (sender.IsSet(TouchBehavior.PressedAnimationEasingProperty)) + { + easing = sender.PressedAnimationEasing; + } + } + else if (hoverState == HoverState.Hovered) + { + if (sender.IsSet(TouchBehavior.HoveredAnimationDurationProperty)) + { + duration = sender.HoveredAnimationDuration; + } + + if (sender.IsSet(TouchBehavior.HoveredAnimationEasingProperty)) + { + easing = sender.HoveredAnimationEasing; + } + } + else + { + if (sender.IsSet(TouchBehavior.NormalAnimationDurationProperty)) + { + duration = sender.NormalAnimationDuration; + } + + if (sender.IsSet(TouchBehavior.NormalAnimationEasingProperty)) + { + easing = sender.NormalAnimationEasing; + } + } + + if (durationMultiplier.HasValue) + { + duration = (int)durationMultiplier.Value * duration; + } + + duration = Max(duration, 0); + + return Task.WhenAll( + animationTaskFactory?.Invoke(sender, touchState, hoverState, duration, easing, token) ?? Task.FromResult(true), + SetBackgroundImageAsync(sender, touchState, hoverState, duration, token), + SetBackgroundColor(sender, touchState, hoverState, duration, easing), + SetOpacity(sender, touchState, hoverState, duration, easing), + SetScale(sender, touchState, hoverState, duration, easing), + SetTranslation(sender, touchState, hoverState, duration, easing), + SetRotation(sender, touchState, hoverState, duration, easing), + SetRotationX(sender, touchState, hoverState, duration, easing), + SetRotationY(sender, touchState, hoverState, duration, easing), + Task.Run(async () => + { + animationProgress = 0; + animationState = touchState; + + for (var progress = animationProgressDelay; progress < duration; progress += animationProgressDelay) + { + await Task.Delay(animationProgressDelay).ConfigureAwait(false); + if (token.IsCancellationRequested) + { + return; + } + + animationProgress = (double)progress / duration; + } + animationProgress = 1; + })); + } + + public void Dispose() + { + throw new NotImplementedException(); + } +} diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverState.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverState.shared.cs new file mode 100644 index 000000000..66cc64a87 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverState.shared.cs @@ -0,0 +1,18 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +/// +public enum HoverState +{ + /// + /// The pointer is not over the element. + /// + Normal, + /// + /// The pointer is over the element. + /// + Hovered +} + diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStateChangedEventArgs.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStateChangedEventArgs.shared.cs new file mode 100644 index 000000000..9cd4f94f7 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStateChangedEventArgs.shared.cs @@ -0,0 +1,15 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public class HoverStateChangedEventArgs : EventArgs +{ + internal HoverStateChangedEventArgs(HoverState state) + => State = state; + + /// + /// Gets the new of the element. + /// + public HoverState State { get; } +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStatus.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStatus.shared.cs new file mode 100644 index 000000000..4d6ee4a3d --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStatus.shared.cs @@ -0,0 +1,16 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public enum HoverStatus +{ + /// + /// The pointer has entered the element. + /// + Entered, + /// + /// The pointer has exited the element. + /// + Exited +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStatusChangedEventArgs.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStatusChangedEventArgs.shared.cs new file mode 100644 index 000000000..8ea7c02be --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/HoverStatusChangedEventArgs.shared.cs @@ -0,0 +1,15 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public class HoverStatusChangedEventArgs : EventArgs +{ + internal HoverStatusChangedEventArgs(HoverStatus status) + => Status = status; + + /// + /// Gets the new of the element. + /// + public HoverStatus Status { get; } +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/LongPressCompletedEventArgs.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/LongPressCompletedEventArgs.shared.cs new file mode 100644 index 000000000..9d65c4734 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/LongPressCompletedEventArgs.shared.cs @@ -0,0 +1,15 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public class LongPressCompletedEventArgs : EventArgs +{ + internal LongPressCompletedEventArgs(object? parameter) + => Parameter = parameter; + + /// + /// Gets the parameter of the event. + /// + public object? Parameter { get; } +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchCompletedEventArgs.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchCompletedEventArgs.shared.cs new file mode 100644 index 000000000..eb3533bf6 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchCompletedEventArgs.shared.cs @@ -0,0 +1,19 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public class TouchCompletedEventArgs : EventArgs + +{ + /// + /// Initializes a new instance of the class. + /// + internal TouchCompletedEventArgs(object? parameter) + => Parameter = parameter; + + /// + /// Gets the parameter associated with the touch event. + /// + public object? Parameter { get; } +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchInteractionStatus.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchInteractionStatus.shared.cs new file mode 100644 index 000000000..bbf9e1eca --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchInteractionStatus.shared.cs @@ -0,0 +1,17 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public enum TouchInteractionStatus +{ + /// + /// The touch interaction has started. + /// + Started, + + /// + /// The touch interaction has completed. + /// + Completed +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchInteractionStatusChangedEventArgs.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchInteractionStatusChangedEventArgs.shared.cs new file mode 100644 index 000000000..952762c49 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchInteractionStatusChangedEventArgs.shared.cs @@ -0,0 +1,15 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public class TouchInteractionStatusChangedEventArgs : EventArgs +{ + internal TouchInteractionStatusChangedEventArgs(TouchInteractionStatus touchInteractionStatus) + => TouchInteractionStatus = touchInteractionStatus; + + /// + /// Gets the current touch interaction status. + /// + public TouchInteractionStatus TouchInteractionStatus { get; } +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchState.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchState.shared.cs new file mode 100644 index 000000000..e3a25ab22 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchState.shared.cs @@ -0,0 +1,17 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public enum TouchState +{ + /// + /// The pointer is not over the element. + /// + Normal, + + /// + /// The pointer is over the element. + /// + Pressed +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStateChangedEventArgs.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStateChangedEventArgs.shared.cs new file mode 100644 index 000000000..28366ef03 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStateChangedEventArgs.shared.cs @@ -0,0 +1,15 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public class TouchStateChangedEventArgs : EventArgs +{ + internal TouchStateChangedEventArgs(TouchState state) + => State = state; + + /// + /// Gets the current state of the touch event. + /// + public TouchState State { get; } +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStatus.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStatus.shared.cs new file mode 100644 index 000000000..3d1d637c7 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStatus.shared.cs @@ -0,0 +1,22 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public enum TouchStatus +{ + /// + /// The touch interaction has started. + /// + Started, + + /// + /// The touch interaction has completed. + /// + Completed, + + /// + /// The touch interaction has been canceled. + /// + Canceled +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStatusChangedEventArgs.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStatusChangedEventArgs.shared.cs new file mode 100644 index 000000000..19f2bc031 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/Primitives/TouchStatusChangedEventArgs.shared.cs @@ -0,0 +1,15 @@ +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// Provides data for the event. +/// +public class TouchStatusChangedEventArgs : EventArgs +{ + internal TouchStatusChangedEventArgs(TouchStatus status) + => Status = status; + + /// + /// Gets the current touch status. + /// + public TouchStatus Status { get; } +} \ No newline at end of file diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.Methods.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.Methods.shared.cs new file mode 100644 index 000000000..2215c0cc5 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.Methods.shared.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace CommunityToolkit.Maui.Behaviors; +public partial class TouchBehavior : IDisposable +{ + readonly NullReferenceException nre = new(nameof(Element)); + internal void RaiseInteractionStatusChanged() + => weakEventManager.HandleEvent(Element ?? throw nre, new TouchInteractionStatusChangedEventArgs(InteractionStatus), nameof(InteractionStatusChanged)); + + internal void RaiseStatusChanged() + => weakEventManager.HandleEvent(Element ?? throw nre, new TouchStatusChangedEventArgs(Status), nameof(StatusChanged)); + + internal void RaiseHoverStateChanged() + { + weakEventManager.HandleEvent(Element ?? throw nre, new HoverStateChangedEventArgs(HoverState), nameof(HoverStateChanged)); + } + + internal void RaiseHoverStatusChanged() + => weakEventManager.HandleEvent(Element ?? throw nre, new HoverStatusChangedEventArgs(HoverStatus), nameof(HoverStatusChanged)); + + internal void RaiseCompleted() + { + var element = Element; + if (element is null) + { + return; + } + + var parameter = CommandParameter; + Command?.Execute(parameter); + weakEventManager.HandleEvent(element, new TouchCompletedEventArgs(parameter), nameof(Completed)); + } + + internal void RaiseLongPressCompleted() + { + var element = Element; + if (element is null) + { + return; + } + + var parameter = LongPressCommandParameter ?? CommandParameter; + LongPressCommand?.Execute(parameter); + weakEventManager.HandleEvent(element, new LongPressCompletedEventArgs(parameter), nameof(LongPressCompleted)); + } + + internal void ForceUpdateState(bool animated = true) + { + if (element is null) + { + return; + } + + gestureManager.ChangeStateAsync(this, animated).ContinueWith(t => + { + if (t.Exception is null) + { + return; + } + + Console.WriteLine($"Failed to force update state, with the {t.Exception} exception and the {t.Exception.Message} message."); + }, TaskContinuationOptions.OnlyOnFaulted); + } + + internal void HandleTouch(TouchStatus status) + => gestureManager.HandleTouch(this, status); + + internal void HandleUserInteraction(TouchInteractionStatus interactionStatus) + => gestureManager.HandleUserInteraction(this, interactionStatus); + + internal void HandleHover(HoverStatus status) + => gestureManager.HandleHover(this, status); + + internal void RaiseStateChanged() + { + ForceUpdateState(); + HandleLongPress(); + weakEventManager.HandleEvent(Element ?? throw nre, new TouchStateChangedEventArgs(State), nameof(StateChanged)); + } + + internal void HandleLongPress() + { + if (Element is null) + { + return; + } + + gestureManager.HandleLongPress(this); + } + + void SetChildrenInputTransparent(bool value) + { + if (Element is not Layout layout) + { + return; + } + + layout.ChildAdded -= OnLayoutChildAdded; + + if (!value) + { + return; + } + + layout.InputTransparent = false; + foreach (var view in layout.Children) + { + OnLayoutChildAdded(layout, new ElementEventArgs((View)view)); + } + + layout.ChildAdded += OnLayoutChildAdded; + } + + void OnLayoutChildAdded(object? sender, ElementEventArgs e) + { + if (e.Element is not View view) + { + return; + } + + if (!ShouldMakeChildrenInputTransparent) + { + view.InputTransparent = false; + return; + } + + view.InputTransparent = IsAvailable; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + bool isDisposed; + + /// + /// Dispose the object. + /// + protected virtual void Dispose(bool disposing) + { + if (isDisposed) + { + return; + } + + if (disposing) + { + // free managed resources + gestureManager.Dispose(); + } + + isDisposed = true; + } +} diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.android.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.android.cs new file mode 100644 index 000000000..99b7285f8 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.android.cs @@ -0,0 +1,555 @@ +#if ANDROID +using Android.Content; +using Android.Content.Res; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Views; +using Android.Views.Accessibility; +using Android.Widget; +using Microsoft.Maui.Controls.Compatibility.Platform.Android; +using static System.OperatingSystem; +using AView = Android.Views.View; +using Color = Android.Graphics.Color; +using MColor = Microsoft.Maui.Graphics.Color; +using MView = Microsoft.Maui.Controls.View; +using PlatformView = Android.Views.View; +using ParentView = Android.Views.IViewParent; + +namespace CommunityToolkit.Maui.Behaviors; + +public partial class TouchBehavior +{ + private static readonly MColor defaultNativeAnimationColor = MColor.FromRgba(128, 128, 128, 64); + private bool isHoverSupported; + private RippleDrawable? ripple; + private AView? rippleView; + private float startX; + private float startY; + private MColor? rippleColor; + private int rippleRadius = -1; + private AView? view = null; + private ViewGroup? viewGroup; + + private AccessibilityManager? accessibilityManager; + private AccessibilityListener? accessibilityListener; + + private bool IsAccessibilityMode => accessibilityManager is not null + && accessibilityManager.IsEnabled + && accessibilityManager.IsTouchExplorationEnabled; + + private readonly bool isAtLeastM = IsAndroidVersionAtLeast((int) BuildVersionCodes.M); + + internal bool IsCanceled { get; set; } + + private bool IsForegroundRippleWithTapGestureRecognizer => + ripple is not null && + this.view is not null && + /* + ripple.IsAlive() && + this.view.IsAlive() && + */ + (isAtLeastM ? this.view.Foreground : this.view.Background) == ripple && + element is MView view && + view.GestureRecognizers.Any(gesture => gesture is TapGestureRecognizer); + + /// + /// Attaches the behavior to the platform view. + /// + /// Maui Visual Element + /// Native View + protected override void OnAttachedTo(VisualElement bindable, AView platformView) + { + Element = bindable; + view = platformView; + //viewGroup = Microsoft.Maui.Platform.ViewExtensions.GetParentOfType(platformView); + if (IsDisabled) + { + return; + } + + platformView.Touch += OnTouch; + UpdateClickHandler(); + accessibilityManager = platformView.Context?.GetSystemService(Context.AccessibilityService) as AccessibilityManager; + + if (accessibilityManager is not null) + { + accessibilityListener = new AccessibilityListener(this); + accessibilityManager.AddAccessibilityStateChangeListener(accessibilityListener); + accessibilityManager.AddTouchExplorationStateChangeListener(accessibilityListener); + } + + if (!IsAndroidVersionAtLeast((int) BuildVersionCodes.Lollipop) || !NativeAnimation) + { + return; + } + + platformView.Clickable = true; + platformView.LongClickable = true; + + CreateRipple(); + ApplyRipple(); + + platformView.LayoutChange += OnLayoutChange; + } + + /// + /// Detaches the behavior from the platform view. + /// + /// Maui Visual Element + /// Native View + protected override void OnDetachedFrom(VisualElement bindable, AView platformView) + { + element = bindable; + view = platformView; + + if (element is null) + { + return; + } + + try + { + if (accessibilityManager is not null && accessibilityListener is not null) + { + accessibilityManager.RemoveAccessibilityStateChangeListener(accessibilityListener); + accessibilityManager.RemoveTouchExplorationStateChangeListener(accessibilityListener); + accessibilityListener.Dispose(); + accessibilityManager = null; + accessibilityListener = null; + } + + RemoveRipple(); + + if (view is not null) + { + view.LayoutChange -= OnLayoutChange; + view.Touch -= OnTouch; + view.Click -= OnClick; + } + + if (rippleView is not null) + { + rippleView.Pressed = false; + viewGroup?.RemoveView(rippleView); + rippleView.Dispose(); + rippleView = null; + } + } + catch (ObjectDisposedException) + { + // Suppress exception + } + + isHoverSupported = false; + } + + private void OnLayoutChange(object? sender, AView.LayoutChangeEventArgs e) + { + if (sender is not AView view || rippleView is null) + { + return; + } + + rippleView.Right = view.Width; + rippleView.Bottom = view.Height; + } + + private void CreateRipple() + { + RemoveRipple(); + + var drawable = isAtLeastM && viewGroup is null + ? view?.Foreground + : view?.Background; + + var isBorderLess = NativeAnimationBorderless; + var isEmptyDrawable = Element is Layout || drawable is null; + var color = NativeAnimationColor; + + if (drawable is RippleDrawable rippleDrawable && rippleDrawable.GetConstantState() is Drawable.ConstantState constantState) + { + ripple = (RippleDrawable) constantState.NewDrawable(); + } + else + { + var content = isEmptyDrawable || isBorderLess ? null : drawable; + var mask = isEmptyDrawable && !isBorderLess ? new ColorDrawable(Color.White) : null; + + ripple = new RippleDrawable(GetColorStateList(color), content, mask); + } + + UpdateRipple(color); + } + + private void RemoveRipple() + { + if (ripple is null) + { + return; + } + + if (view is not null) + { + if (isAtLeastM && view.Foreground == ripple) + { + view.Foreground = null; + } + else if (view.Background == ripple) + { + view.Background = null; + } + } + + if (rippleView is not null) + { + rippleView.Foreground = null; + rippleView.Background = null; + } + + ripple.Dispose(); + ripple = null; + } + + private void UpdateRipple(MColor color) + { + if (IsDisabled || (color == rippleColor && NativeAnimationRadius == rippleRadius)) + { + return; + } + + rippleColor = color; + rippleRadius = NativeAnimationRadius; + ripple?.SetColor(GetColorStateList(color)); + if (isAtLeastM && ripple is not null) + { + ripple.Radius = (int) (view?.Context?.Resources?.DisplayMetrics?.Density * NativeAnimationRadius ?? throw new NullReferenceException()); + } + } + + private ColorStateList GetColorStateList(MColor? color) + { + var animationColor = color; + animationColor ??= defaultNativeAnimationColor; + + return new ColorStateList( + new[] {Array.Empty()}, + new[] {(int) animationColor.ToAndroid()}); + } + + private void UpdateClickHandler() + { + if (view is null /* || !view.IsAlive()*/) + { + return; + } + + view.Click -= OnClick; + if (IsAccessibilityMode || (IsAvailable && (element?.IsEnabled ?? false))) + { + view.Click += OnClick; + return; + } + } + + private void ApplyRipple() + { + if (ripple is null) + { + return; + } + + var isBorderless = NativeAnimationBorderless; + + if (viewGroup is null && view is not null) + { + if (IsAndroidVersionAtLeast((int) BuildVersionCodes.M)) + { + view.Foreground = ripple; + } + else + { + view.Background = ripple; + } + + return; + } + + if (rippleView is null) + { + rippleView = new FrameLayout(viewGroup?.Context ?? view?.Context ?? throw new NullReferenceException()) + { + LayoutParameters = new ViewGroup.LayoutParams(-1, -1), + Clickable = false, + Focusable = false, + Enabled = false + }; + + viewGroup?.AddView(rippleView); + rippleView.BringToFront(); + } + + viewGroup?.SetClipChildren(!isBorderless); + + if (isBorderless) + { + rippleView.Background = null; + rippleView.Foreground = ripple; + } + else + { + rippleView.Foreground = null; + rippleView.Background = ripple; + } + } + + private void OnClick(object? sender, EventArgs args) + { + if (IsDisabled) + { + return; + } + + if (!IsAccessibilityMode) + { + return; + } + + IsCanceled = false; + HandleEnd(TouchStatus.Completed); + } + + private void HandleEnd(TouchStatus status) + { + if (IsCanceled) + { + return; + } + + IsCanceled = true; + if (DisallowTouchThreshold > 0) + { + viewGroup?.Parent?.RequestDisallowInterceptTouchEvent(false); + } + + HandleTouch(status); + + HandleUserInteraction(TouchInteractionStatus.Completed); + + EndRipple(); + } + + private void EndRipple() + { + if (IsDisabled) + { + return; + } + + if (rippleView != null) + { + if (rippleView.Pressed) + { + rippleView.Pressed = false; + rippleView.Enabled = false; + } + } + else if (IsForegroundRippleWithTapGestureRecognizer) + { + if (view?.Pressed ?? false) + { + view.Pressed = false; + } + } + } + + + private void OnTouch(object? sender, AView.TouchEventArgs e) + { + e.Handled = false; + + if (IsDisabled) + { + return; + } + + if (IsAccessibilityMode) + { + return; + } + + switch (e.Event?.ActionMasked) + { + case MotionEventActions.Down: + OnTouchDown(e); + break; + case MotionEventActions.Up: + OnTouchUp(); + break; + case MotionEventActions.Cancel: + OnTouchCancel(); + break; + case MotionEventActions.Move: + OnTouchMove(sender, e); + break; + case MotionEventActions.HoverEnter: + OnHoverEnter(); + break; + case MotionEventActions.HoverExit: + OnHoverExit(); + break; + } + } + + private void OnTouchDown(AView.TouchEventArgs e) + { + _ = e.Event ?? throw new NullReferenceException(); + + IsCanceled = false; + + startX = e.Event.GetX(); + startY = e.Event.GetY(); + + HandleUserInteraction(TouchInteractionStatus.Started); + HandleTouch(TouchStatus.Started); + + StartRipple(e.Event.GetX(), e.Event.GetY()); + + if (DisallowTouchThreshold > 0) + { + viewGroup?.Parent?.RequestDisallowInterceptTouchEvent(true); + } + } + + private void OnTouchUp() + { + HandleEnd(Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled); + } + + private void OnTouchCancel() + { + HandleEnd(TouchStatus.Canceled); + } + + private void OnTouchMove(object? sender, AView.TouchEventArgs e) + { + if (IsCanceled || e.Event == null) + { + return; + } + + var diffX = Math.Abs(e.Event.GetX() - startX) / this.view?.Context?.Resources?.DisplayMetrics?.Density ?? throw new NullReferenceException(); + var diffY = Math.Abs(e.Event.GetY() - startY) / this.view?.Context?.Resources?.DisplayMetrics?.Density ?? throw new NullReferenceException(); + var maxDiff = Math.Max(diffX, diffY); + + var disallowTouchThreshold = DisallowTouchThreshold; + if (disallowTouchThreshold > 0 && maxDiff > disallowTouchThreshold) + { + HandleEnd(TouchStatus.Canceled); + return; + } + + if (sender is not AView view) + { + return; + } + + var screenPointerCoords = new Point(view.Left + e.Event.GetX(), view.Top + e.Event.GetY()); + var viewRect = new Rect(view.Left, view.Top, view.Right - view.Left, view.Bottom - view.Top); + var status = viewRect.Contains(screenPointerCoords) ? TouchStatus.Started : TouchStatus.Canceled; + + if (isHoverSupported && ((status == TouchStatus.Canceled && HoverStatus == HoverStatus.Entered) + || (status == TouchStatus.Started && HoverStatus == HoverStatus.Exited))) + { + HandleHover(status == TouchStatus.Started ? HoverStatus.Entered : HoverStatus.Exited); + } + + if (Status != status) + { + HandleTouch(status); + + if (status == TouchStatus.Started) + { + StartRipple(e.Event.GetX(), e.Event.GetY()); + } + + if (status == TouchStatus.Canceled) + { + EndRipple(); + } + } + } + + private void OnHoverEnter() + { + isHoverSupported = true; + HandleHover(HoverStatus.Entered); + } + + private void OnHoverExit() + { + isHoverSupported = true; + HandleHover(HoverStatus.Exited); + } + + private void StartRipple(float x, float y) + { + if (IsDisabled || !NativeAnimation) + { + return; + } + + if (CanExecute) + { + UpdateRipple(NativeAnimationColor); + if (rippleView is not null) + { + rippleView.Enabled = true; + rippleView.BringToFront(); + ripple?.SetHotspot(x, y); + rippleView.Pressed = true; + } + else if (IsForegroundRippleWithTapGestureRecognizer && view is not null) + { + ripple?.SetHotspot(x, y); + view.Pressed = true; + } + } + else if (rippleView is null) + { + UpdateRipple(Colors.Transparent); + } + } + + private sealed class AccessibilityListener : Java.Lang.Object, + AccessibilityManager.IAccessibilityStateChangeListener, + AccessibilityManager.ITouchExplorationStateChangeListener + { + private TouchBehavior? platformTouchEffect; + + internal AccessibilityListener(TouchBehavior platformTouchEffect) + { + this.platformTouchEffect = platformTouchEffect; + } + + public void OnAccessibilityStateChanged(bool enabled) + { + platformTouchEffect?.UpdateClickHandler(); + } + + public void OnTouchExplorationStateChanged(bool enabled) + { + platformTouchEffect?.UpdateClickHandler(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + platformTouchEffect = null; + } + + base.Dispose(disposing); + } + } +} +#endif diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.macios.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.macios.cs new file mode 100644 index 000000000..f96615af4 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.macios.cs @@ -0,0 +1,308 @@ +#if IOS +using AsyncAwaitBestPractices; +using CoreGraphics; +using Foundation; +using Microsoft.Maui.Controls.Compatibility.Platform.iOS; +using Microsoft.Maui.Platform; +using UIKit; + +namespace CommunityToolkit.Maui.Behaviors; + +public partial class TouchBehavior +{ + private UIGestureRecognizer? touchGesture; + private UIGestureRecognizer? hoverGesture; + + /// + /// Attaches the behavior to the platform view. + /// + /// Maui Visual Element + /// Native View + protected override void OnAttachedTo(VisualElement bindable, UIView platformView) + { + Element = bindable; + + touchGesture = new TouchUITapGestureRecognizer(this); + + if (((platformView as IVisualNativeElementRenderer)?.Control ?? platformView) is UIButton button) + { + button.AllTouchEvents += PreventButtonHighlight; + ((TouchUITapGestureRecognizer) touchGesture).IsButton = true; + } + + platformView.AddGestureRecognizer(touchGesture); + + if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0)) + { + hoverGesture = new UIHoverGestureRecognizer(OnHover); + platformView.AddGestureRecognizer(hoverGesture); + } + + platformView.UserInteractionEnabled = true; + } + + /// + /// Detaches the behavior from the platform view. + /// + /// Maui Visual Element + /// Native View + protected override void OnDetachedFrom(VisualElement bindable, UIView platformView) + { + if (((platformView as IVisualNativeElementRenderer)?.Control ?? platformView) is UIButton button) + { + button.AllTouchEvents -= PreventButtonHighlight; + } + + if (touchGesture != null) + { + platformView?.RemoveGestureRecognizer(touchGesture); + touchGesture?.Dispose(); + touchGesture = null; + } + + if (hoverGesture != null) + { + platformView?.RemoveGestureRecognizer(hoverGesture); + hoverGesture?.Dispose(); + hoverGesture = null; + } + + Element = null; + } + + private void OnHover() + { + if (IsDisabled) + { + return; + } + + switch (hoverGesture?.State) + { + case UIGestureRecognizerState.Began: + case UIGestureRecognizerState.Changed: + HandleHover(HoverStatus.Entered); + break; + case UIGestureRecognizerState.Ended: + HandleHover(HoverStatus.Exited); + break; + } + } + + private void PreventButtonHighlight(object? sender, EventArgs args) + { + if (sender is not UIButton button) + { + throw new ArgumentException($"{nameof(sender)} must be Type {nameof(UIButton)}", nameof(sender)); + } + + button.Highlighted = false; + } +} + +internal sealed class TouchUITapGestureRecognizer : UIGestureRecognizer +{ + private TouchBehavior behavior; + private float? defaultRadius; + private float? defaultShadowRadius; + private float? defaultShadowOpacity; + private CGPoint? startPoint; + + public TouchUITapGestureRecognizer(TouchBehavior behavior) + { + this.behavior = behavior; + CancelsTouchesInView = false; + Delegate = new TouchUITapGestureRecognizerDelegate(); + } + + public bool IsCanceled { get; set; } = true; + + public bool IsButton { get; set; } + + public override void TouchesBegan(NSSet touches, UIEvent evt) + { + if (behavior?.IsDisabled ?? true) + { + return; + } + + IsCanceled = false; + startPoint = GetTouchPoint(touches); + + HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started).SafeFireAndForget(); + + base.TouchesBegan(touches, evt); + } + + public override void TouchesEnded(NSSet touches, UIEvent evt) + { + if (behavior?.IsDisabled ?? true) + { + return; + } + + HandleTouch(behavior?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); + + IsCanceled = true; + + base.TouchesEnded(touches, evt); + } + + public override void TouchesCancelled(NSSet touches, UIEvent evt) + { + if (behavior?.IsDisabled ?? true) + { + return; + } + + HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); + + IsCanceled = true; + + base.TouchesCancelled(touches, evt); + } + + public override void TouchesMoved(NSSet touches, UIEvent evt) + { + if (behavior?.IsDisabled ?? true) + { + return; + } + + var disallowTouchThreshold = behavior.DisallowTouchThreshold; + var point = GetTouchPoint(touches); + if (point != null && startPoint != null && disallowTouchThreshold > 0) + { + var diffX = Math.Abs(point.Value.X - startPoint.Value.X); + var diffY = Math.Abs(point.Value.Y - startPoint.Value.Y); + var maxDiff = Math.Max(diffX, diffY); + if (maxDiff > disallowTouchThreshold) + { + HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); + IsCanceled = true; + base.TouchesMoved(touches, evt); + return; + } + } + + var status = point != null && View?.Bounds.Contains(point.Value) is true + ? TouchStatus.Started + : TouchStatus.Canceled; + + if (behavior?.Status != status) + { + HandleTouch(status).SafeFireAndForget(); + } + + if (status == TouchStatus.Canceled) + { + IsCanceled = true; + } + + base.TouchesMoved(touches, evt); + } + + public async Task HandleTouch(TouchStatus status, TouchInteractionStatus? interactionStatus = null) + { + if (IsCanceled || behavior == null) + { + return; + } + + if (behavior?.IsDisabled ?? true) + { + return; + } + + var canExecuteAction = behavior.CanExecute; + + if (interactionStatus == TouchInteractionStatus.Started) + { + behavior?.HandleUserInteraction(TouchInteractionStatus.Started); + interactionStatus = null; + } + + behavior?.HandleTouch(status); + if (interactionStatus.HasValue) + { + behavior?.HandleUserInteraction(interactionStatus.Value); + } + + if (behavior == null || behavior.Element is null || (!behavior.NativeAnimation && !IsButton) || (!canExecuteAction && status == TouchStatus.Started)) + { + return; + } + + var color = behavior.NativeAnimationColor; + var radius = behavior.NativeAnimationRadius; + var shadowRadius = behavior.NativeAnimationShadowRadius; + var isStarted = status == TouchStatus.Started; + defaultRadius = (float?) (defaultRadius ?? View.Layer.CornerRadius); + defaultShadowRadius = (float?) (defaultShadowRadius ?? View.Layer.ShadowRadius); + defaultShadowOpacity ??= View.Layer.ShadowOpacity; + + var tcs = new TaskCompletionSource(); + UIViewPropertyAnimator.CreateRunningPropertyAnimator(.2, 0, UIViewAnimationOptions.AllowUserInteraction, + () => + { + if (color == default(Color)) + { + View.Layer.Opacity = isStarted ? 0.5f : (float) behavior.Element.Opacity; + } + else + { + View.Layer.BackgroundColor = (isStarted ? color : behavior.Element.BackgroundColor).ToCGColor(); + } + + View.Layer.CornerRadius = isStarted ? radius : defaultRadius.GetValueOrDefault(); + + if (shadowRadius >= 0) + { + View.Layer.ShadowRadius = isStarted ? shadowRadius : defaultShadowRadius.GetValueOrDefault(); + View.Layer.ShadowOpacity = isStarted ? 0.7f : defaultShadowOpacity.GetValueOrDefault(); + } + }, endPos => tcs.SetResult(endPos)); + await tcs.Task; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Delegate.Dispose(); + } + + base.Dispose(disposing); + } + + private CGPoint? GetTouchPoint(NSSet touches) + { + return (touches?.AnyObject as UITouch)?.LocationInView(View); + } + + private class TouchUITapGestureRecognizerDelegate : UIGestureRecognizerDelegate + { + public override bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer) + { + if (gestureRecognizer is TouchUITapGestureRecognizer touchGesture && otherGestureRecognizer is UIPanGestureRecognizer && + otherGestureRecognizer.State == UIGestureRecognizerState.Began) + { + touchGesture.HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); + touchGesture.IsCanceled = true; + } + + return true; + } + + public override bool ShouldReceiveTouch(UIGestureRecognizer recognizer, UITouch touch) + { + if (recognizer.View.IsDescendantOfView(touch.View)) + { + return true; + } + + return recognizer.View.Subviews.Any(view => view == touch.View); + } + } +} +#endif diff --git a/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.shared.cs b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.shared.cs new file mode 100644 index 000000000..c6e5bf092 --- /dev/null +++ b/src/Core/Behaviors/PlatformBehaviors/MCTTouch/TouchBehavior.shared.cs @@ -0,0 +1,1208 @@ +using System.Windows.Input; + +namespace CommunityToolkit.Maui.Behaviors; + +/// +/// +/// +public partial class TouchBehavior : PlatformBehavior +{ + /// + /// The visual state for when the touch is unpressed. + /// + public const string UnpressedVisualState = "Unpressed"; + + /// + /// The visual state for when the touch is pressed. + /// + public const string PressedVisualState = "Pressed"; + + /// + /// The visual state for when the touch is hovered. + /// + public const string HoveredVisualState = "Hovered"; + + readonly GestureManager gestureManager = new(); + + VisualElement? element; + + internal VisualElement? Element + { + get => element; + set + { + if (element != null) + { + IsUsed = false; + gestureManager.Reset(); + SetChildrenInputTransparent(false); + } + gestureManager.AbortAnimations(this); + element = value; + if (value != null) + { + SetChildrenInputTransparent(ShouldMakeChildrenInputTransparent); + IsUsed = true; + ForceUpdateState(); + } + } + } + + readonly WeakEventManager weakEventManager = new(); + + internal bool IsDisabled { get; set; } + + internal bool IsUsed { get; set; } + + internal bool IsAutoGenerated { get; set; } + + + /// + /// Occurs when the touch status changes. + /// + public event EventHandler StatusChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Occurs when the touch state changes. + /// + public event EventHandler StateChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Occurs when the touch interaction status changes. + /// + public event EventHandler InteractionStatusChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Occurs when the hover status changes. + /// + public event EventHandler HoverStatusChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Occurs when the hover state changes. + /// + public event EventHandler HoverStateChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Occurs when a touch gesture is completed. + /// + public event EventHandler Completed + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Occurs when a long press gesture is completed. + /// + public event EventHandler LongPressCompleted + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty IsAvailableProperty = BindableProperty.Create( + nameof(IsAvailable), + typeof(bool), + typeof(TouchBehavior), + true); + + /// + /// Gets or sets a value indicating whether the touch is available. + /// + public bool IsAvailable + { + get => (bool)GetValue(IsAvailableProperty); + set => SetValue(IsAvailableProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty ShouldMakeChildrenInputTransparentProperty = BindableProperty.Create( + nameof(ShouldMakeChildrenInputTransparent), + typeof(bool), + typeof(TouchBehavior), + true); + + /// + /// Gets or sets a value indicating whether the children of the element should be made input transparent. + /// + public bool ShouldMakeChildrenInputTransparent + { + get => (bool)GetValue(ShouldMakeChildrenInputTransparentProperty); + set => SetValue(ShouldMakeChildrenInputTransparentProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty CommandProperty = BindableProperty.Create( + nameof(Command), + typeof(ICommand), + typeof(TouchBehavior), + default(ICommand)); + + /// + /// Gets or sets the command to invoke when the user has completed a touch gesture. + /// + public ICommand Command + { + get => (ICommand)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty LongPressCommandProperty = BindableProperty.Create( + nameof(LongPressCommand), + typeof(ICommand), + typeof(TouchBehavior), + default(ICommand)); + + /// + /// Gets or sets the command to invoke when the user has completed a long press. + /// + public ICommand LongPressCommand + { + get => (ICommand)GetValue(LongPressCommandProperty); + set => SetValue(LongPressCommandProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create( + nameof(CommandParameter), + typeof(object), + typeof(TouchBehavior), + default); + + /// + /// Gets or sets the parameter to pass to the property. + /// + public object CommandParameter + { + get => GetValue(CommandParameterProperty); + set => SetValue(CommandParameterProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty LongPressCommandParameterProperty = BindableProperty.Create( + nameof(LongPressCommandParameter), + typeof(object), + typeof(TouchBehavior), + default); + + /// + /// Gets or sets the parameter to pass to the property. + /// + public object LongPressCommandParameter + { + get => GetValue(LongPressCommandParameterProperty); + set => SetValue(LongPressCommandParameterProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty LongPressDurationProperty = BindableProperty.Create( + nameof(LongPressDuration), + typeof(int), + typeof(TouchBehavior), + 500); + + /// + /// Gets or sets the duration of the long press in milliseconds. + /// + public int LongPressDuration + { + get => (int)GetValue(LongPressDurationProperty); + set => SetValue(LongPressDurationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty StatusProperty = BindableProperty.Create( + nameof(Status), + typeof(TouchStatus), + typeof(TouchBehavior), + TouchStatus.Completed, + BindingMode.OneWayToSource); + + /// + /// Gets the current status of the touch. + /// + public TouchStatus Status + { + get => (TouchStatus)GetValue(StatusProperty); + set => SetValue(StatusProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty StateProperty = BindableProperty.Create( + nameof(State), + typeof(TouchState), + typeof(TouchBehavior), + TouchState.Normal, + BindingMode.OneWayToSource); + + /// + /// Gets the current state of the touch. + /// + public TouchState State + { + get => (TouchState)GetValue(StateProperty); + set => SetValue(StateProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty InteractionStatusProperty = BindableProperty.Create( + nameof(InteractionStatus), + typeof(TouchInteractionStatus), + typeof(TouchBehavior), + TouchInteractionStatus.Completed, + BindingMode.OneWayToSource); + + /// + /// Gets the current interaction status of the touch. + /// + public TouchInteractionStatus InteractionStatus + { + get => (TouchInteractionStatus)GetValue(InteractionStatusProperty); + set => SetValue(InteractionStatusProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoverStatusProperty = BindableProperty.Create( + nameof(Behaviors.HoverStatus), + typeof(HoverStatus), + typeof(TouchBehavior), + HoverStatus.Exited, + BindingMode.OneWayToSource); + + /// + /// Gets the current hover status of the touch. + /// + public HoverStatus HoverStatus + { + get => (HoverStatus)GetValue(HoverStatusProperty); + set => SetValue(HoverStatusProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoverStateProperty = BindableProperty.Create( + nameof(Behaviors.HoverState), + typeof(HoverState), + typeof(TouchBehavior), + HoverState.Normal, + BindingMode.OneWayToSource); + + /// + /// Gets the current hover state of the touch. + /// + public HoverState HoverState + { + get => (HoverState)GetValue(HoverStateProperty); + set => SetValue(HoverStateProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalBackgroundColorProperty = BindableProperty.Create( + nameof(NormalBackgroundColor), + typeof(Color), + typeof(TouchBehavior), + null); + + /// + /// Gets or sets the background color of the element when the touch is in the normal state. + /// + public Color NormalBackgroundColor + { + get => (Color)GetValue(NormalBackgroundColorProperty); + set => SetValue(NormalBackgroundColorProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredBackgroundColorProperty = BindableProperty.Create( + nameof(HoveredBackgroundColor), + typeof(Color), + typeof(TouchBehavior), + null); + + /// + /// Gets or sets the background color of the element when the touch is in the hovered state. + /// + public Color HoveredBackgroundColor + { + get => (Color)GetValue(HoveredBackgroundColorProperty); + set => SetValue(HoveredBackgroundColorProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedBackgroundColorProperty = BindableProperty.Create( + nameof(PressedBackgroundColor), + typeof(Color), + typeof(TouchBehavior), + null); + + /// + /// Gets or sets the background color of the element when the touch is in the pressed state. + /// + public Color PressedBackgroundColor + { + get => (Color)GetValue(PressedBackgroundColorProperty); + set => SetValue(PressedBackgroundColorProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalOpacityProperty = BindableProperty.Create( + nameof(NormalOpacity), + typeof(double), + typeof(TouchBehavior), + 1.0); + + + /// + /// Gets or sets the opacity of the element when the touch is in the normal state. + /// + public double NormalOpacity + { + get => (double)GetValue(NormalOpacityProperty); + set => SetValue(NormalOpacityProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredOpacityProperty = BindableProperty.Create( + nameof(HoveredOpacity), + typeof(double), + typeof(TouchBehavior), + 1.0); + + /// + /// Gets or sets the opacity of the element when the touch is in the hovered state. + /// + public double HoveredOpacity + { + get => (double)GetValue(HoveredOpacityProperty); + set => SetValue(HoveredOpacityProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedOpacityProperty = BindableProperty.Create( + nameof(PressedOpacity), + typeof(double), + typeof(TouchBehavior), + 1.0); + + /// + /// Gets or sets the opacity of the element when the touch is in the pressed state. + /// + public double PressedOpacity + { + get => (double)GetValue(PressedOpacityProperty); + set => SetValue(PressedOpacityProperty, value); + } + + internal bool CanExecute => IsAvailable + && (element?.IsEnabled ?? false) + && (Command?.CanExecute(CommandParameter) ?? true); + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalScaleProperty = BindableProperty.Create( + nameof(NormalScale), + typeof(double), + typeof(TouchBehavior), + 1.0); + + /// + /// Gets or sets the scale of the element when the touch is in the normal state. + /// + public double NormalScale + { + get => (double)GetValue(NormalScaleProperty); + set => SetValue(NormalScaleProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredScaleProperty = BindableProperty.Create( + nameof(HoveredScale), + typeof(double), + typeof(TouchBehavior), + 1.0); + + /// + /// Gets or sets the scale of the element when the touch is in the hovered state. + /// + public double HoveredScale + { + get => (double)GetValue(HoveredScaleProperty); + set => SetValue(HoveredScaleProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedScaleProperty = BindableProperty.Create( + nameof(PressedScale), + typeof(double), + typeof(TouchBehavior), + 1.0); + + /// + /// Gets or sets the scale of the element when the touch is in the pressed state. + /// + public double PressedScale + { + get => (double)GetValue(PressedScaleProperty); + set => SetValue(PressedScaleProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalTranslationXProperty = BindableProperty.Create( + nameof(NormalTranslationX), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the translation X of the element when the touch is in the normal state. + /// + public double NormalTranslationX + { + get => (double)GetValue(NormalTranslationXProperty); + set => SetValue(NormalTranslationXProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredTranslationXProperty = BindableProperty.Create( + nameof(HoveredTranslationX), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the translation X of the element when the touch is in the hovered state. + /// + public double HoveredTranslationX + { + get => (double)GetValue(HoveredTranslationXProperty); set => SetValue(HoveredTranslationXProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedTranslationXProperty = BindableProperty.Create( + nameof(PressedTranslationX), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the translation X of the element when the touch is in the pressed state. + /// + public double PressedTranslationX + { + get => (double)GetValue(PressedTranslationXProperty); + set => SetValue(PressedTranslationXProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalTranslationYProperty = BindableProperty.Create( + nameof(NormalTranslationY), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the translation Y of the element when the touch is in the normal state. + /// + public double NormalTranslationY + { + get => (double)GetValue(NormalTranslationYProperty); set => SetValue(NormalTranslationYProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredTranslationYProperty = BindableProperty.Create( + nameof(HoveredTranslationY), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the translation Y of the element when the touch is in the hovered state. + /// + public double HoveredTranslationY + { + get => (double)GetValue(HoveredTranslationYProperty); set => SetValue(HoveredTranslationYProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedTranslationYProperty = BindableProperty.Create( + nameof(PressedTranslationY), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the translation Y of the element when the touch is in the pressed state. + /// + public double PressedTranslationY + { + get => (double)GetValue(PressedTranslationYProperty); + set => SetValue(PressedTranslationYProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalRotationProperty = BindableProperty.Create( + nameof(NormalRotation), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation of the element when the touch is in the normal state. + /// + public double NormalRotation + { + get => (double)GetValue(NormalRotationProperty); + set => SetValue(NormalRotationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredRotationProperty = BindableProperty.Create( + nameof(HoveredRotation), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation of the element when the touch is in the hovered state. + /// + public double HoveredRotation + { + get => (double)GetValue(HoveredRotationProperty); + set => SetValue(HoveredRotationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedRotationProperty = BindableProperty.Create( + nameof(PressedRotation), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation of the element when the touch is in the pressed state. + /// + public double PressedRotation + { + get => (double)GetValue(PressedRotationProperty); set => SetValue(PressedRotationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalRotationXProperty = BindableProperty.Create( + nameof(NormalRotationX), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation X of the element when the touch is in the normal state. + /// + public double NormalRotationX + { + get => (double)GetValue(NormalRotationXProperty); + set => SetValue(NormalRotationXProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredRotationXProperty = BindableProperty.Create( + nameof(HoveredRotationX), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation X of the element when the touch is in the hovered state. + /// + public double HoveredRotationX + { + get => (double)GetValue(HoveredRotationXProperty); + set => SetValue(HoveredRotationXProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedRotationXProperty = BindableProperty.Create( + nameof(PressedRotationX), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation X of the element when the touch is in the pressed state. + /// + public double PressedRotationX + { + get => (double)GetValue(PressedRotationXProperty); set => SetValue(PressedRotationXProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalRotationYProperty = BindableProperty.Create( + nameof(NormalRotationY), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation Y of the element when the touch is in the normal state. + /// + public double NormalRotationY + { + get => (double)GetValue(NormalRotationYProperty); + set => SetValue(NormalRotationYProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredRotationYProperty = BindableProperty.Create( + nameof(HoveredRotationY), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation Y of the element when the touch is in the hovered state. + /// + public double HoveredRotationY + { + get => (double)GetValue(HoveredRotationYProperty); + set => SetValue(HoveredRotationYProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedRotationYProperty = BindableProperty.Create( + nameof(PressedRotationY), + typeof(double), + typeof(TouchBehavior), + 0.0); + + /// + /// Gets or sets the rotation Y of the element when the touch is in the pressed state. + /// + public double PressedRotationY + { + get => (double)GetValue(PressedRotationYProperty); + set => SetValue(PressedRotationYProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty AnimationDurationProperty = BindableProperty.Create( + nameof(AnimationDuration), + typeof(int), + typeof(TouchBehavior), + default(int)); + + /// + /// Gets or sets the duration of the animation. + /// + public int AnimationDuration + { + get => (int)GetValue(AnimationDurationProperty); + set => SetValue(AnimationDurationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty AnimationEasingProperty = BindableProperty.Create( + nameof(AnimationEasing), + typeof(Easing), + typeof(TouchBehavior), + null); + + /// + /// Gets or sets the easing of the animation. + /// + public Easing AnimationEasing + { + get => (Easing)GetValue(AnimationEasingProperty); + set => SetValue(AnimationEasingProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedAnimationDurationProperty = BindableProperty.Create( + nameof(PressedAnimationDuration), + typeof(int), + typeof(TouchBehavior), + default(int)); + + /// + /// Gets or sets the duration of the pressed animation. + /// + public int PressedAnimationDuration + { + get => (int)GetValue(PressedAnimationDurationProperty); + set => SetValue(PressedAnimationDurationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedAnimationEasingProperty = BindableProperty.Create( + nameof(PressedAnimationEasing), + typeof(Easing), + typeof(TouchBehavior), + null); + + /// + /// Gets or sets the easing of the pressed animation. + /// + public Easing PressedAnimationEasing + { + get => (Easing)GetValue(PressedAnimationEasingProperty); + set => SetValue(PressedAnimationEasingProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalAnimationDurationProperty = BindableProperty.Create( + nameof(NormalAnimationDuration), + typeof(int), + typeof(TouchBehavior), + default(int)); + + /// + /// Gets or sets the duration of the normal animation. + /// + public int NormalAnimationDuration + { + get => (int)GetValue(NormalAnimationDurationProperty); + set => SetValue(NormalAnimationDurationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalAnimationEasingProperty = BindableProperty.Create( + nameof(NormalAnimationEasing), + typeof(Easing), + typeof(TouchBehavior), + null); + + /// + /// Gets or sets the easing of the normal animation. + /// + public Easing NormalAnimationEasing + { + get => (Easing)GetValue(NormalAnimationEasingProperty); + set => SetValue(NormalAnimationEasingProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredAnimationDurationProperty = BindableProperty.Create( + nameof(HoveredAnimationDuration), + typeof(int), + typeof(TouchBehavior), + default(int)); + + /// + /// Gets or sets the duration of the hovered animation. + /// + public int HoveredAnimationDuration + { + get => (int)GetValue(HoveredAnimationDurationProperty); + set => SetValue(HoveredAnimationDurationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredAnimationEasingProperty = BindableProperty.Create( + nameof(HoveredAnimationEasing), + typeof(Easing), + typeof(TouchBehavior), + null); + + /// + /// Gets or sets the easing of the hovered animation. + /// + public Easing HoveredAnimationEasing + { + get => (Easing)GetValue(HoveredAnimationEasingProperty); + set => SetValue(HoveredAnimationEasingProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PulseCountProperty = BindableProperty.Create( + nameof(PulseCount), + typeof(int), + typeof(TouchBehavior), + default(int)); + + /// + /// Gets or sets the number of times the element should pulse. + /// + public int PulseCount + { + get => (int)GetValue(PulseCountProperty); + set => SetValue(PulseCountProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty IsToggledProperty = BindableProperty.Create( + nameof(IsToggled), + typeof(bool?), + typeof(TouchBehavior), + default(bool?), + BindingMode.TwoWay); + + /// + /// Gets or sets a value indicating whether the element is toggled. + /// + public bool? IsToggled + { + get => (bool?)GetValue(IsToggledProperty); + set => SetValue(IsToggledProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty DisallowTouchThresholdProperty = BindableProperty.Create( + nameof(DisallowTouchThreshold), + typeof(int), + typeof(TouchBehavior), + default(int)); + + /// + /// Gets or sets the threshold for disallowing touch. + /// + public int DisallowTouchThreshold + { + get => (int)GetValue(DisallowTouchThresholdProperty); + set => SetValue(DisallowTouchThresholdProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NativeAnimationProperty = BindableProperty.Create( + nameof(NativeAnimation), + typeof(bool), + typeof(TouchBehavior), + false); + + /// + /// Gets or sets a value indicating whether the animation should be native. + /// + public bool NativeAnimation + { + get => (bool)GetValue(NativeAnimationProperty); + set => SetValue(NativeAnimationProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NativeAnimationColorProperty = BindableProperty.Create( + nameof(NativeAnimationColor), + typeof(Color), + typeof(TouchBehavior), + null); + + /// + /// Gets or sets the color of the native animation. + /// + public Color NativeAnimationColor + { + get => (Color)GetValue(NativeAnimationColorProperty); + set => SetValue(NativeAnimationColorProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NativeAnimationRadiusProperty = BindableProperty.Create( + nameof(NativeAnimationRadius), + typeof(int), + typeof(TouchBehavior), + -1); + + /// + /// Gets or sets the radius of the native animation. + /// + public int NativeAnimationRadius + { + get => (int)GetValue(NativeAnimationRadiusProperty); + set => SetValue(NativeAnimationRadiusProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NativeAnimationShadowRadiusProperty = BindableProperty.Create( + nameof(NativeAnimationShadowRadius), + typeof(int), + typeof(TouchBehavior), + -1); + + /// + /// Gets or sets the shadow radius of the native animation. + /// + public int NativeAnimationShadowRadius + { + get => (int)GetValue(NativeAnimationShadowRadiusProperty); + set => SetValue(NativeAnimationShadowRadiusProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NativeAnimationBorderlessProperty = BindableProperty.Create( + nameof(NativeAnimationBorderless), + typeof(bool), + typeof(TouchBehavior), + false); + + /// + /// Gets or sets a value indicating whether the native animation should be borderless. + /// + public bool NativeAnimationBorderless + { + get => (bool)GetValue(NativeAnimationBorderlessProperty); + set => SetValue(NativeAnimationBorderlessProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalBackgroundImageSourceProperty = BindableProperty.Create( + nameof(NormalBackgroundImageSource), + typeof(ImageSource), + typeof(TouchBehavior), + default(ImageSource)); + + /// + /// Gets or sets the normal background image source. + /// + public ImageSource NormalBackgroundImageSource + { + get => (ImageSource)GetValue(NormalBackgroundImageSourceProperty); + set => SetValue(NormalBackgroundImageSourceProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredBackgroundImageSourceProperty = BindableProperty.Create( + nameof(HoveredBackgroundImageSource), + typeof(ImageSource), + typeof(TouchBehavior), + default(ImageSource)); + + /// + /// Gets or sets the hovered background image source. + /// + public ImageSource HoveredBackgroundImageSource + { + get => (ImageSource)GetValue(HoveredBackgroundImageSourceProperty); + set => SetValue(HoveredBackgroundImageSourceProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedBackgroundImageSourceProperty = BindableProperty.Create( + nameof(PressedBackgroundImageSource), + typeof(ImageSource), + typeof(TouchBehavior), + default(ImageSource)); + + /// + /// Gets or sets the pressed background image source. + /// + public ImageSource PressedBackgroundImageSource + { + get => (ImageSource)GetValue(PressedBackgroundImageSourceProperty); + set => SetValue(PressedBackgroundImageSourceProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty BackgroundImageAspectProperty = BindableProperty.Create( + nameof(BackgroundImageAspect), + typeof(Aspect), + typeof(TouchBehavior), + default(Aspect)); + + /// + /// Gets or sets the background image aspect. + /// + public Aspect BackgroundImageAspect + { + get => (Aspect)GetValue(BackgroundImageAspectProperty); + set => SetValue(BackgroundImageAspectProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty NormalBackgroundImageAspectProperty = BindableProperty.Create( + nameof(NormalBackgroundImageAspect), + typeof(Aspect), + typeof(TouchBehavior), + default(Aspect)); + + /// + /// Gets or sets the normal background image aspect. + /// + public Aspect NormalBackgroundImageAspect + { + get => (Aspect)GetValue(NormalBackgroundImageAspectProperty); + set => SetValue(NormalBackgroundImageAspectProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty HoveredBackgroundImageAspectProperty = BindableProperty.Create( + nameof(HoveredBackgroundImageAspect), + typeof(Aspect), + typeof(TouchBehavior), + default(Aspect)); + + /// + /// Gets or sets the hovered background image aspect. + /// + public Aspect HoveredBackgroundImageAspect + { + get => (Aspect)GetValue(HoveredBackgroundImageAspectProperty); + set => SetValue(HoveredBackgroundImageAspectProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty PressedBackgroundImageAspectProperty = BindableProperty.Create( + nameof(PressedBackgroundImageAspect), + typeof(Aspect), + typeof(TouchBehavior), + default(Aspect)); + + /// + /// Gets or sets the pressed background image aspect. + /// + public Aspect PressedBackgroundImageAspect + { + get => (Aspect)GetValue(PressedBackgroundImageAspectProperty); + set => SetValue(PressedBackgroundImageAspectProperty, value); + } + + /// + /// Bindable property for + /// + public static readonly BindableProperty ShouldSetImageOnAnimationEndProperty = BindableProperty.Create( + nameof(ShouldSetImageOnAnimationEnd), + typeof(bool), + typeof(TouchBehavior), + default(bool)); + + /// + /// Gets or sets a value indicating whether the image should be set on animation end. + /// + public bool ShouldSetImageOnAnimationEnd + { + get => (bool)GetValue(ShouldSetImageOnAnimationEndProperty); + set => SetValue(ShouldSetImageOnAnimationEndProperty, value); + } +} diff --git a/src/Core/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs b/src/Core/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs index 90e121d68..5a71e90b7 100644 --- a/src/Core/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs +++ b/src/Core/Controls/AccountSwitchingOverlay/AccountSwitchingOverlayView.xaml.cs @@ -172,7 +172,7 @@ namespace Bit.App.Controls private async Task LongPressAccountAsync(AccountViewCellViewModel item) { - if (!LongPressAccountEnabled || !item.IsAccount) + if (!LongPressAccountEnabled || item == null || !item.IsAccount) { return; } diff --git a/src/Core/Controls/AccountViewCell/AccountViewCell.xaml b/src/Core/Controls/AccountViewCell/AccountViewCell.xaml index 34ce0ab3c..05071fb4b 100644 --- a/src/Core/Controls/AccountViewCell/AccountViewCell.xaml +++ b/src/Core/Controls/AccountViewCell/AccountViewCell.xaml @@ -1,18 +1,21 @@  - + + + + diff --git a/src/Core/Controls/ExtendedGrid.cs b/src/Core/Controls/ExtendedGrid.cs index 922973456..5e2208dec 100644 --- a/src/Core/Controls/ExtendedGrid.cs +++ b/src/Core/Controls/ExtendedGrid.cs @@ -1,9 +1,21 @@ -using Microsoft.Maui.Controls; -using Microsoft.Maui; +using CommunityToolkit.Maui.Behaviors; namespace Bit.App.Controls { public class ExtendedGrid : Grid { + public ExtendedGrid() + { + // Add Android Ripple effect. Eventually we should be able to replace this with the Maui Community Toolkit implementation. (https://github.com/CommunityToolkit/Maui/issues/86) + // [MAUI-Migration] When this TouchBehavior is replaced we can delete the existing TouchBehavior support files (which is all the files and folders inside "Core.Behaviors.PlatformBehaviors.MCTTouch.*") + if (DeviceInfo.Platform == DevicePlatform.Android) + { + var touchBehavior = new TouchBehavior() + { + NativeAnimation = true + }; + Behaviors.Add(touchBehavior); + } + } } } diff --git a/src/Core/Controls/ExtendedStackLayout.cs b/src/Core/Controls/ExtendedStackLayout.cs index 5186fbb0a..5709e0a97 100644 --- a/src/Core/Controls/ExtendedStackLayout.cs +++ b/src/Core/Controls/ExtendedStackLayout.cs @@ -1,9 +1,21 @@ -using Microsoft.Maui.Controls; -using Microsoft.Maui; +using CommunityToolkit.Maui.Behaviors; namespace Bit.App.Controls { public class ExtendedStackLayout : StackLayout { + public ExtendedStackLayout() + { + // Add Android Ripple effect. Eventually we should be able to replace this with the Maui Community Toolkit implementation. (https://github.com/CommunityToolkit/Maui/issues/86) + // [MAUI-Migration] When this TouchBehavior is replaced we can delete the existing TouchBehavior support files (which is all the files and folders inside "Core.Behaviors.PlatformBehaviors.MCTTouch.*") + if (DeviceInfo.Platform == DevicePlatform.Android) + { + var touchBehavior = new TouchBehavior() + { + NativeAnimation = true + }; + Behaviors.Add(touchBehavior); + } + } } } diff --git a/src/Core/Pages/Send/SendAddEditPage.xaml b/src/Core/Pages/Send/SendAddEditPage.xaml index 5d235e287..5580b1d5f 100644 --- a/src/Core/Pages/Send/SendAddEditPage.xaml +++ b/src/Core/Pages/Send/SendAddEditPage.xaml @@ -248,7 +248,7 @@ AutomationId="SendHideTextByDefaultToggle" /> - +