mirror of
https://github.com/bitwarden/android.git
synced 2024-12-27 11:28:28 +03:00
Merge branch 'feature/maui-migration' of https://github.com/bitwarden/mobile into feature/maui-migration
This commit is contained in:
commit
5712639492
26 changed files with 3335 additions and 22 deletions
|
@ -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<T>(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<bool> ColorTo(this VisualElement element, Color color, uint length = 250u, Easing? easing = null)
|
||||
{
|
||||
_ = element ?? throw new ArgumentNullException(nameof(element));
|
||||
|
||||
var animationCompletionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
789
src/Core/Behaviors/PlatformBehaviors/MCTTouch/GestureManager.cs
Normal file
789
src/Core/Behaviors/PlatformBehaviors/MCTTouch/GestureManager.cs
Normal file
|
@ -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<TouchBehavior, TouchState, HoverState, int, Easing?, CancellationToken, Task>? 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<TouchBehavior, TouchState, HoverState, int, Easing?, CancellationToken, Task>? 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<object>();
|
||||
|
||||
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<bool> 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<bool> 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<bool>();
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.HoverStateChanged"/> event.
|
||||
/// </summary>
|
||||
///
|
||||
public enum HoverState
|
||||
{
|
||||
/// <summary>
|
||||
/// The pointer is not over the element.
|
||||
/// </summary>
|
||||
Normal,
|
||||
/// <summary>
|
||||
/// The pointer is over the element.
|
||||
/// </summary>
|
||||
Hovered
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.HoverStateChanged"/> event.
|
||||
/// </summary>
|
||||
public class HoverStateChangedEventArgs : EventArgs
|
||||
{
|
||||
internal HoverStateChangedEventArgs(HoverState state)
|
||||
=> State = state;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new <see cref="HoverState"/> of the element.
|
||||
/// </summary>
|
||||
public HoverState State { get; }
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.HoverStatusChanged"/> event.
|
||||
/// </summary>
|
||||
public enum HoverStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The pointer has entered the element.
|
||||
/// </summary>
|
||||
Entered,
|
||||
/// <summary>
|
||||
/// The pointer has exited the element.
|
||||
/// </summary>
|
||||
Exited
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.HoverStatusChanged"/> event.
|
||||
/// </summary>
|
||||
public class HoverStatusChangedEventArgs : EventArgs
|
||||
{
|
||||
internal HoverStatusChangedEventArgs(HoverStatus status)
|
||||
=> Status = status;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new <see cref="HoverStatus"/> of the element.
|
||||
/// </summary>
|
||||
public HoverStatus Status { get; }
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.LongPressCompleted"/> event.
|
||||
/// </summary>
|
||||
public class LongPressCompletedEventArgs : EventArgs
|
||||
{
|
||||
internal LongPressCompletedEventArgs(object? parameter)
|
||||
=> Parameter = parameter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter of the <see cref="TouchBehavior.LongPressCompleted"/> event.
|
||||
/// </summary>
|
||||
public object? Parameter { get; }
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.Completed"/> event.
|
||||
/// </summary>
|
||||
public class TouchCompletedEventArgs : EventArgs
|
||||
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TouchCompletedEventArgs"/> class.
|
||||
/// </summary>
|
||||
internal TouchCompletedEventArgs(object? parameter)
|
||||
=> Parameter = parameter;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter associated with the touch event.
|
||||
/// </summary>
|
||||
public object? Parameter { get; }
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.Completed"/> event.
|
||||
/// </summary>
|
||||
public enum TouchInteractionStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The touch interaction has started.
|
||||
/// </summary>
|
||||
Started,
|
||||
|
||||
/// <summary>
|
||||
/// The touch interaction has completed.
|
||||
/// </summary>
|
||||
Completed
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.InteractionStatusChanged"/> event.
|
||||
/// </summary>
|
||||
public class TouchInteractionStatusChangedEventArgs : EventArgs
|
||||
{
|
||||
internal TouchInteractionStatusChangedEventArgs(TouchInteractionStatus touchInteractionStatus)
|
||||
=> TouchInteractionStatus = touchInteractionStatus;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current touch interaction status.
|
||||
/// </summary>
|
||||
public TouchInteractionStatus TouchInteractionStatus { get; }
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.StatusChanged"/> event.
|
||||
/// </summary>
|
||||
public enum TouchState
|
||||
{
|
||||
/// <summary>
|
||||
/// The pointer is not over the element.
|
||||
/// </summary>
|
||||
Normal,
|
||||
|
||||
/// <summary>
|
||||
/// The pointer is over the element.
|
||||
/// </summary>
|
||||
Pressed
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.StateChanged"/> event.
|
||||
/// </summary>
|
||||
public class TouchStateChangedEventArgs : EventArgs
|
||||
{
|
||||
internal TouchStateChangedEventArgs(TouchState state)
|
||||
=> State = state;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current state of the touch event.
|
||||
/// </summary>
|
||||
public TouchState State { get; }
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.StatusChanged"/> event.
|
||||
/// </summary>
|
||||
public enum TouchStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// The touch interaction has started.
|
||||
/// </summary>
|
||||
Started,
|
||||
|
||||
/// <summary>
|
||||
/// The touch interaction has completed.
|
||||
/// </summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>
|
||||
/// The touch interaction has been canceled.
|
||||
/// </summary>
|
||||
Canceled
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
namespace CommunityToolkit.Maui.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for the <see cref="TouchBehavior.StatusChanged"/> event.
|
||||
/// </summary>
|
||||
public class TouchStatusChangedEventArgs : EventArgs
|
||||
{
|
||||
internal TouchStatusChangedEventArgs(TouchStatus status)
|
||||
=> Status = status;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current touch status.
|
||||
/// </summary>
|
||||
public TouchStatus Status { get; }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Dispose the object.
|
||||
/// </summary>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// free managed resources
|
||||
gestureManager.Dispose();
|
||||
}
|
||||
|
||||
isDisposed = true;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the behavior to the platform view.
|
||||
/// </summary>
|
||||
/// <param name="bindable">Maui Visual Element</param>
|
||||
/// <param name="platformView">Native View</param>
|
||||
protected override void OnAttachedTo(VisualElement bindable, AView platformView)
|
||||
{
|
||||
Element = bindable;
|
||||
view = platformView;
|
||||
//viewGroup = Microsoft.Maui.Platform.ViewExtensions.GetParentOfType<ViewGroup>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches the behavior from the platform view.
|
||||
/// </summary>
|
||||
/// <param name="bindable">Maui Visual Element</param>
|
||||
/// <param name="platformView">Native View</param>
|
||||
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<int>()},
|
||||
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
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the behavior to the platform view.
|
||||
/// </summary>
|
||||
/// <param name="bindable">Maui Visual Element</param>
|
||||
/// <param name="platformView">Native View</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches the behavior from the platform view.
|
||||
/// </summary>
|
||||
/// <param name="bindable">Maui Visual Element</param>
|
||||
/// <param name="platformView">Native View</param>
|
||||
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<UIViewAnimatingPosition>();
|
||||
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
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ViewCell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:xct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
x:Class="Bit.App.Controls.AccountViewCell"
|
||||
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
xmlns:core="clr-namespace:Bit.Core"
|
||||
xmlns:behaviors="clr-namespace:CommunityToolkit.Maui.Behaviors"
|
||||
x:Name="_accountView"
|
||||
x:DataType="controls:AccountViewCellViewModel">
|
||||
<!--TODO: [MAUI-Migration] add long press ( https://github.com/CommunityToolkit/Maui/issues/86 )
|
||||
xct:TouchEffect.LongPressCommand="{Binding LongPressAccountCommand, Source={x:Reference _accountView}}"
|
||||
xct:TouchEffect.LongPressCommandParameter="{Binding .}"-->
|
||||
<Grid RowSpacing="0"
|
||||
ColumnSpacing="0">
|
||||
<Grid.Behaviors>
|
||||
<!--TODO: [MAUI-Migration] Currently using a "copied" implementation from the github issue in the link until they add this to the Community Toolkit ( https://github.com/CommunityToolkit/Maui/issues/86 ) -->
|
||||
<behaviors:TouchBehavior NativeAnimation="True"
|
||||
LongPressCommand="{Binding LongPressAccountCommand, Source={x:Reference _accountView}}"
|
||||
LongPressCommandParameter="{Binding .BindingContext, Source={x:Reference _accountView}}" />
|
||||
</Grid.Behaviors>
|
||||
<Grid.GestureRecognizers>
|
||||
<TapGestureRecognizer Command="{Binding SelectAccountCommand, Source={x:Reference _accountView}}" CommandParameter="{Binding .}" />
|
||||
</Grid.GestureRecognizers>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -248,7 +248,7 @@
|
|||
AutomationId="SendHideTextByDefaultToggle" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
<!--TODO: [MAUI-Migration] xct:TouchEffect.Command="{Binding ToggleOptionsCommand}" for below ( https://github.com/CommunityToolkit/Maui/issues/86 ) -->
|
||||
<!--TODO: [MAUI-Migration] xct:TouchEffect.Command="{Binding ToggleOptionsCommand}" for below ( https://github.com/CommunityToolkit/Maui/issues/86 ) -->
|
||||
<StackLayout
|
||||
Orientation="Horizontal"
|
||||
Spacing="0"
|
||||
|
|
|
@ -59,12 +59,11 @@
|
|||
<Setter Property="FontAttributes"
|
||||
Value="Bold" />
|
||||
</Style>
|
||||
<!--[MAUI-Migration][Color-default]-->
|
||||
<Style TargetType="Label"
|
||||
Class="text-html"
|
||||
ApplyToDerivedTypes="True">
|
||||
<Setter Property="TextColor"
|
||||
Value="#ffffff" />
|
||||
Value="{x:Null}" />
|
||||
<Setter Property="TextType"
|
||||
Value="Html" />
|
||||
</Style>
|
||||
|
|
|
@ -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 = $"<span style=\"color:#{ThemeManager.GetResourceColor("TextColor").ToHex().Substring(3)}\">";
|
||||
var numberColor = $"<span style=\"color:#{ThemeManager.GetResourceColor("PasswordNumberColor").ToHex().Substring(3)}\">";
|
||||
var specialColor = $"<span style=\"color:#{ThemeManager.GetResourceColor("PasswordSpecialColor").ToHex().Substring(3)}\">";
|
||||
var normalColor = $"<span style=\"color:{ThemeManager.GetResourceColor("TextColor").ToHex()}\">";
|
||||
var numberColor = $"<span style=\"color:{ThemeManager.GetResourceColor("PasswordNumberColor").ToHex()}\">";
|
||||
var specialColor = $"<span style=\"color:{ThemeManager.GetResourceColor("PasswordSpecialColor").ToHex()}\">";
|
||||
var result = string.Empty;
|
||||
|
||||
// iOS won't hide the zero-width space char without these div attrs, but Android won't respect
|
||||
|
|
Loading…
Reference in a new issue