diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f1f8d5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,109 @@ +# NOTE: Requires **VS2019 16.3** or later + +# StyleCopRules +# Description: StyleCopRules custom ruleset + +# Code files +[*.{cs,vb}] + + +# Default severity for analyzer diagnostics - Requires **VS2019 16.5** or later +dotnet_analyzer_diagnostic.severity = warning + +dotnet_diagnostic.SA1101.severity = none + +dotnet_diagnostic.SA1200.severity = none + +dotnet_diagnostic.SA1305.severity = warning + +dotnet_diagnostic.SA1309.severity = none + +dotnet_diagnostic.SA1402.severity = none + +dotnet_diagnostic.SA1407.severity = none + +dotnet_diagnostic.SA1412.severity = warning + +dotnet_diagnostic.SA1600.severity = none + +dotnet_diagnostic.SA1601.severity = none + +dotnet_diagnostic.SA1602.severity = none + +dotnet_diagnostic.SA1604.severity = none + +dotnet_diagnostic.SA1605.severity = none + +dotnet_diagnostic.SA1606.severity = none + +dotnet_diagnostic.SA1607.severity = none + +dotnet_diagnostic.SA1608.severity = none + +dotnet_diagnostic.SA1610.severity = none + +dotnet_diagnostic.SA1611.severity = none + +dotnet_diagnostic.SA1612.severity = none + +dotnet_diagnostic.SA1613.severity = none + +dotnet_diagnostic.SA1614.severity = none + +dotnet_diagnostic.SA1615.severity = none + +dotnet_diagnostic.SA1616.severity = none + +dotnet_diagnostic.SA1617.severity = none + +dotnet_diagnostic.SA1618.severity = none + +dotnet_diagnostic.SA1619.severity = none + +dotnet_diagnostic.SA1620.severity = none + +dotnet_diagnostic.SA1621.severity = none + +dotnet_diagnostic.SA1622.severity = none + +dotnet_diagnostic.SA1623.severity = none + +dotnet_diagnostic.SA1624.severity = none + +dotnet_diagnostic.SA1625.severity = none + +dotnet_diagnostic.SA1626.severity = none + +dotnet_diagnostic.SA1627.severity = none + +dotnet_diagnostic.SA1633.severity = none + +dotnet_diagnostic.SA1634.severity = none + +dotnet_diagnostic.SA1635.severity = none + +dotnet_diagnostic.SA1636.severity = none + +dotnet_diagnostic.SA1637.severity = none + +dotnet_diagnostic.SA1638.severity = none + +dotnet_diagnostic.SA1640.severity = none + +dotnet_diagnostic.SA1641.severity = none + +dotnet_diagnostic.SA1642.severity = none + +dotnet_diagnostic.SA1643.severity = none + +dotnet_diagnostic.SA1648.severity = none + +dotnet_diagnostic.SA1649.severity = none + +dotnet_diagnostic.SA1651.severity = none + +dotnet_diagnostic.SA1652.severity = none + +dotnet_diagnostic.SX1101.severity = warning + +dotnet_diagnostic.SX1309.severity = warning diff --git a/Maui.Tabs/Effects/TouchEffect.cs b/Maui.Tabs/Effects/TouchEffect.cs index 8a8d6f1..ba565af 100644 --- a/Maui.Tabs/Effects/TouchEffect.cs +++ b/Maui.Tabs/Effects/TouchEffect.cs @@ -1,63 +1,84 @@ -using Microsoft.Maui.Controls; - -namespace Sharpnado.Tabs.Effects { - public static class TouchEffect { - - public static readonly BindableProperty ColorProperty = - BindableProperty.CreateAttached( - "Color", - typeof(Color), - typeof(TouchEffect), - KnownColor.Accent, - propertyChanged: PropertyChanged - ); - - public static void SetColor(BindableObject view, Color value) { - view.SetValue(ColorProperty, value); - } +namespace Sharpnado.Tabs.Effects; - public static Color GetColor(BindableObject view) { - return (Color)view.GetValue(ColorProperty); - } +public static class TouchEffect +{ + public static readonly BindableProperty ColorProperty = BindableProperty.CreateAttached( + "Color", + typeof(Color), + typeof(TouchEffect), + KnownColor.Accent, + propertyChanged: PropertyChanged); + + public static void SetColor(BindableObject view, Color value) + { + view.SetValue(ColorProperty, value); + } + + public static Color GetColor(BindableObject view) + { + return (Color)view.GetValue(ColorProperty); + } - public static void UnregisterEffect(BindableObject bindable) + public static void UnregisterEffect(BindableObject bindable) + { + if (!(bindable is View view)) { - if (!(bindable is View view)) - return; + return; + } - var eff = view.Effects.FirstOrDefault(e => e is TouchRoutingEffect); + var eff = view.Effects.FirstOrDefault(e => e is TouchRoutingEffect); - if (eff == null) return; - view.Effects.Remove(eff); + if (eff == null) + { + return; } - static void PropertyChanged(BindableObject bindable, object oldValue, object newValue) { - if (!(bindable is View view)) - return; + view.Effects.Remove(eff); + } - var eff = view.Effects.FirstOrDefault(e => e is TouchRoutingEffect); - if (GetColor(bindable) != Colors.Transparent) { - view.InputTransparent = false; + private static void PropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + if (!(bindable is View view)) + { + return; + } + + var eff = view.Effects.FirstOrDefault(e => e is TouchRoutingEffect); + if (GetColor(bindable) != Colors.Transparent) + { + view.InputTransparent = false; - if (eff != null) return; - view.Effects.Add(new TouchRoutingEffect()); - if (EffectsConfig.AutoChildrenInputTransparent && bindable is Layout && - !EffectsConfig.GetChildrenInputTransparent(view)) { - EffectsConfig.SetChildrenInputTransparent(view, true); - } + if (eff != null) + { + return; } - else { - if (eff == null) return; - view.Effects.Remove(eff); - if (EffectsConfig.AutoChildrenInputTransparent && bindable is Layout && - EffectsConfig.GetChildrenInputTransparent(view)) { - EffectsConfig.SetChildrenInputTransparent(view, false); - } + + view.Effects.Add(new TouchRoutingEffect()); + if (EffectsConfig.AutoChildrenInputTransparent + && bindable is Layout + && !EffectsConfig.GetChildrenInputTransparent(view)) + { + EffectsConfig.SetChildrenInputTransparent(view, true); } } - } + else + { + if (eff == null) + { + return; + } - public class TouchRoutingEffect : RoutingEffect - { + view.Effects.Remove(eff); + if (EffectsConfig.AutoChildrenInputTransparent + && bindable is Layout + && EffectsConfig.GetChildrenInputTransparent(view)) + { + EffectsConfig.SetChildrenInputTransparent(view, false); + } + } } } + +public class TouchRoutingEffect : RoutingEffect +{ +} \ No newline at end of file diff --git a/Maui.Tabs/Maui.Tabs.csproj b/Maui.Tabs/Maui.Tabs.csproj index d034962..214c0de 100644 --- a/Maui.Tabs/Maui.Tabs.csproj +++ b/Maui.Tabs/Maui.Tabs.csproj @@ -22,9 +22,9 @@ $(AssemblyName) ($(TargetFramework)) - 3.1.0 - 3.1.0 - 3.1.0 + 3.1.1 + 3.1.1 + 3.1.1 true en diff --git a/Maui.Tabs/Maui.Tabs.sln.DotSettings b/Maui.Tabs/Maui.Tabs.sln.DotSettings new file mode 100644 index 0000000..af0f654 --- /dev/null +++ b/Maui.Tabs/Maui.Tabs.sln.DotSettings @@ -0,0 +1,112 @@ + + True + VISIBLE_FILES + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + False + Required + Required + Required + Required + False + False + False + False + False + False + False + NEXT_LINE + NEXT_LINE + NEXT_LINE + False + False + False + NEVER + NEVER + False + NEVER + False + NEVER + True + False + True + True + True + CHOP_IF_LONG + CHOP_IF_LONG + True + True + CHOP_IF_LONG + CHOP_ALWAYS + CHOP_IF_LONG + CHOP_ALWAYS + CHOP_IF_LONG + True + iOS + OS + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="iOS" Suffix="" Style="AaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/Maui.Tabs/Platforms/Android/CommandsPlatform.cs b/Maui.Tabs/Platforms/Android/CommandsPlatform.cs index 111e569..98c6cdc 100644 --- a/Maui.Tabs/Platforms/Android/CommandsPlatform.cs +++ b/Maui.Tabs/Platforms/Android/CommandsPlatform.cs @@ -8,83 +8,115 @@ using Android.Views; -using View = Android.Views.View; -using Microsoft.Maui.Controls.Compatibility.Platform.Android; - using Microsoft.Maui.Controls.Platform; using Sharpnado.Tabs.Effects.Droid.GestureCollectors; +using Object = Java.Lang.Object; +using View = Android.Views.View; using Rect = Android.Graphics.Rect; -namespace Sharpnado.Tabs.Effects.Droid { - public class CommandsPlatform : PlatformEffect { - public View View => Control ?? Container; - public bool IsDisposed => Container is null - || (Container is Java.Lang.Object javaContainer) && javaContainer.Handle == IntPtr.Zero; +namespace Sharpnado.Tabs.Effects.Droid; - DateTime _tapTime; - readonly Rect _rect = new Rect(); - readonly int[] _location = new int[2]; +public class CommandsPlatform : PlatformEffect +{ + private const string Tag = "CommandEffectAndroid"; - public static void Init() { - } + private readonly int[] _location = new int[2]; - protected override void OnAttached() { - View.Clickable = true; - View.LongClickable = true; - View.SoundEffectsEnabled = true; - TouchCollector.Add(View, OnTouch, ActionType.Tap); - } + private readonly Rect _rect = new(); + + private DateTime _tapTime; - void OnTouch(View.TouchEventArgs args) { - switch (args.Event.Action) { - case MotionEventActions.Down: - _tapTime = DateTime.Now; - break; - - case MotionEventActions.Up: - if (IsViewInBounds((int)args.Event.RawX, (int)args.Event.RawY)) { - var range = (DateTime.Now - _tapTime).TotalMilliseconds; - if (range > 800) - LongClickHandler(); - else - ClickHandler(); + public View View => Control ?? Container; + + public bool IsDisposed => Container is null + || (Container is Object javaContainer && javaContainer.Handle == IntPtr.Zero); + + public static void Init() + { + } + + protected override void OnAttached() + { + InternalLogger.Debug(Tag, () => "OnAttached"); + + View.Clickable = true; + View.LongClickable = true; + View.SoundEffectsEnabled = true; + TouchCollector.Add(View, OnTouch, ActionType.Tap); + } + + private void OnTouch(View.TouchEventArgs args) + { + switch (args.Event.Action) + { + case MotionEventActions.Down: + _tapTime = DateTime.Now; + break; + + case MotionEventActions.Up: + if (IsViewInBounds((int)args.Event.RawX, (int)args.Event.RawY)) + { + double range = (DateTime.Now - _tapTime).TotalMilliseconds; + if (range > 800) + { + LongClickHandler(); } - break; - } - } + else + { + ClickHandler(); + } + } - bool IsViewInBounds(int x, int y) { - View.GetDrawingRect(_rect); - View.GetLocationOnScreen(_location); - _rect.Offset(_location[0], _location[1]); - return _rect.Contains(x, y); + break; } + } + + private bool IsViewInBounds(int x, int y) + { + View.GetDrawingRect(_rect); + View.GetLocationOnScreen(_location); + _rect.Offset(_location[0], _location[1]); + return _rect.Contains(x, y); + } - void ClickHandler() { - var cmd = Commands.GetTap(Element); - var param = Commands.GetTapParameter(Element); - if (cmd?.CanExecute(param) ?? false) - cmd.Execute(param); + private void ClickHandler() + { + var cmd = Commands.GetTap(Element); + object param = Commands.GetTapParameter(Element); + if (cmd?.CanExecute(param) ?? false) + { + cmd.Execute(param); } + } - void LongClickHandler() { - var cmd = Commands.GetLongTap(Element); + private void LongClickHandler() + { + var cmd = Commands.GetLongTap(Element); - if (cmd == null) { - ClickHandler(); - return; - } + if (cmd == null) + { + ClickHandler(); + return; + } - var param = Commands.GetLongTapParameter(Element); - if (cmd.CanExecute(param)) - cmd.Execute(param); + object param = Commands.GetLongTapParameter(Element); + if (cmd.CanExecute(param)) + { + cmd.Execute(param); } + } - protected override void OnDetached() { - if (IsDisposed) return; - TouchCollector.Delete(View, OnTouch, ActionType.Tap); + protected override void OnDetached() + { + InternalLogger.Debug(Tag, () => "OnDetached"); + + if (IsDisposed) + { + return; } + + TouchCollector.Delete(View, OnTouch, ActionType.Tap); } -} +} \ No newline at end of file diff --git a/Maui.Tabs/Platforms/Android/TintableImageEffect.cs b/Maui.Tabs/Platforms/Android/TintableImageEffect.cs index ebcdf83..bf6f852 100644 --- a/Maui.Tabs/Platforms/Android/TintableImageEffect.cs +++ b/Maui.Tabs/Platforms/Android/TintableImageEffect.cs @@ -1,9 +1,8 @@ using Android.Widget; -using Microsoft.Maui.Controls.Compatibility.Platform.Android; using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Platform; -using Sharpnado.Tabs.Droid; using Sharpnado.Tabs.Effects; namespace Sharpnado.Tabs.Droid @@ -33,7 +32,7 @@ private void UpdateColor() { var effect = (TintableImageEffect)Element.Effects.FirstOrDefault(x => x is TintableImageEffect); - var color = effect?.TintColor?.ToAndroid(); + var color = effect?.TintColor?.ToPlatform(); if (Control is ImageView imageView && imageView.Handle != IntPtr.Zero && color.HasValue) { diff --git a/Maui.Tabs/Platforms/Android/TouchCollector.cs b/Maui.Tabs/Platforms/Android/TouchCollector.cs index 4cfbb44..ea2b723 100644 --- a/Maui.Tabs/Platforms/Android/TouchCollector.cs +++ b/Maui.Tabs/Platforms/Android/TouchCollector.cs @@ -6,78 +6,117 @@ // This will exclude this file from stylecop analysis // +using Android.OS; using Android.Views; using View = Android.Views.View; -namespace Sharpnado.Tabs.Effects.Droid.GestureCollectors { - enum ActionType - { - Ripple = 0, - Tap = 1, - } +namespace Sharpnado.Tabs.Effects.Droid.GestureCollectors; + +internal enum ActionType +{ + Ripple = 0, - record ActionSource(ActionType ActionType, Action Action); + Tap = 1 +} - internal static class TouchCollector { - static Dictionary> Collection { get; } = - new (); +internal record ActionSource(ActionType ActionType, Action Action); - static View _activeView; +internal static class TouchCollector +{ + private const string Tag = "CommandEffectAndroid"; - public static void Add(View view, Action action, ActionType actionType) { - if (Collection.ContainsKey(view)) { - Collection[view].Add(new ActionSource(actionType, action)); - } - else { - view.Touch += ActionActivator; - Collection.Add(view, [ new(actionType, action) ] ); - } + private static View _activeView; + + private static Dictionary> Collection { get; } = new(); + + public static void Add(View view, Action action, ActionType actionType) + { + if (!Collection.ContainsKey(view)) + { + view.Touch += ActionActivator; + Collection.Add(view, []); } - public static void Delete(View view, Action action, ActionType actionType) { - if (!Collection.ContainsKey(view)) return; + Collection[view] + .Add(new ActionSource(actionType, action)); - var actions = Collection[view]; - actions.Remove(new ActionSource(actionType, action)); + InternalLogger.Debug(Tag, () => $"Add {actionType}: {Collection[view].Count} actions"); + } - if (actions.Count != 0) return; - view.Touch -= ActionActivator; - Collection.Remove(view); + public static void Delete(View view, Action action, ActionType actionType) + { + if (!Collection.ContainsKey(view)) + { + return; } - static async void ActionActivator(object sender, View.TouchEventArgs e) { - var view = (View)sender; - if (!Collection.ContainsKey(view) || (_activeView != null && _activeView != view)) return; - - var actions = Collection[view].ToArray(); - bool rippleRan = false; - foreach (var valueAction in actions.Where(source => source.ActionType == ActionType.Ripple)) { - valueAction?.Action.Invoke(e); - rippleRan = true; - } - - switch (e.Event.Action) { - case MotionEventActions.Down: - _activeView = view; - view.PlaySoundEffect(SoundEffects.Click); - break; - - case MotionEventActions.Up: - case MotionEventActions.Cancel: - _activeView = null; - e.Handled = true; - break; - } - - if (rippleRan) - { - await Task.Delay(200); - } + var actions = Collection[view]; + actions.Remove(new ActionSource(actionType, action)); + + if (actions.Count > 0) + { + InternalLogger.Debug(Tag, () => $"Delete {actionType}: {Collection[view].Count} actions"); + return; + } - foreach (var valueAction in actions.Where(source => source.ActionType == ActionType.Tap)) { - valueAction?.Action.Invoke(e); - } + view.Touch -= ActionActivator; + Collection.Remove(view); + + InternalLogger.Debug(Tag, () => $"Delete {actionType}: view removed!"); + } + + private static void ActionActivator(object sender, View.TouchEventArgs e) + { + var view = (View)sender; + if (!Collection.ContainsKey(view) || (_activeView != null && _activeView != view)) + { + return; } + + var actions = Collection[view] + .ToArray(); + + var rippleActions = actions.Where(source => source.ActionType == ActionType.Ripple).ToList(); + bool hasRipple = rippleActions.Any(); + + using var handler = new Handler(Looper.MainLooper); + + handler.PostAtFrontOfQueue( + () => + { + switch (e.Event.Action) + { + case MotionEventActions.Down: + _activeView = view; + view.PlaySoundEffect(SoundEffects.Click); + break; + + case MotionEventActions.Up: + case MotionEventActions.Cancel: + _activeView = null; + e.Handled = true; + break; + } + }); + + handler.Post( + () => + { + foreach (var valueAction in rippleActions) + { + valueAction?.Action.Invoke(e); + } + }); + + handler.PostDelayed( + () => + { + foreach (var valueAction in actions.Where(source => source.ActionType == ActionType.Tap)) + { + valueAction?.Action.Invoke(e); + } + }, + hasRipple ? 100 : 0); } } \ No newline at end of file diff --git a/Maui.Tabs/Platforms/Android/TouchEffectPlatform.cs b/Maui.Tabs/Platforms/Android/TouchEffectPlatform.cs index 33a5249..d6ba6df 100644 --- a/Maui.Tabs/Platforms/Android/TouchEffectPlatform.cs +++ b/Maui.Tabs/Platforms/Android/TouchEffectPlatform.cs @@ -1,236 +1,348 @@ using System.ComponentModel; + using Android.Animation; +using Android.Content; using Android.Content.Res; using Android.Graphics.Drawables; using Android.OS; +using Android.Runtime; using Android.Views; -using Android.Widget; -using Microsoft.Maui.Controls.Compatibility.Platform.Android; -using Microsoft.Maui.Controls.Platform; -using Sharpnado.Tabs.Effects; -using Sharpnado.Tabs.Effects.Droid; -using Sharpnado.Tabs.Effects.Droid.GestureCollectors; +using JetBrains.Annotations; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Platform; +using Sharpnado.Tabs.Effects.Droid.GestureCollectors; using Color = Android.Graphics.Color; using ListView = Android.Widget.ListView; using ScrollView = Android.Widget.ScrollView; using View = Android.Views.View; -namespace Sharpnado.Tabs.Effects.Droid { - public class TouchEffectPlatform : PlatformEffect { - public bool EnableRipple => Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop; - public bool IsDisposed => Container == null || Container.Handle == IntPtr.Zero; - public View View => Control ?? Container; +namespace Sharpnado.Tabs.Effects.Droid; - Color _color; - byte _alpha; - RippleDrawable _ripple; - FrameLayout _viewOverlay; - ObjectAnimator _animator; +public class TouchEffectPlatform : PlatformEffect +{ + public class RippleOverlay : View + { + private const string Tag = "RippleOverlay"; - public static void Init() { + protected RippleOverlay(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) + { } - protected override void OnAttached() { - if (Control is ListView || Control is ScrollView) { - return; - } + public RippleOverlay([NotNull] Context context) + : base(context) + { + } + + public override bool DispatchTouchEvent(MotionEvent e) + { + InternalLogger.Debug(Tag, () => $"DispatchTouchEvent {e.Action}"); - if (Container is not ViewGroup group) + if (e.Action == MotionEventActions.Move) { - throw new InvalidOperationException("Touch color effect requires to be attached to a container like a ContentView or a layout (Grid, StackLayout, etc...)"); + return false; } - View.Clickable = true; - View.LongClickable = true; - _viewOverlay = new FrameLayout(Container.Context) { - LayoutParameters = new ViewGroup.LayoutParams(-1, -1), - Clickable = false, - Focusable = false, - }; - Container.LayoutChange += ViewOnLayoutChange; - - if (EnableRipple) - _viewOverlay.Background = CreateRipple(_color); + return base.DispatchTouchEvent(e); + } - SetEffectColor(); - TouchCollector.Add(View, OnTouch, ActionType.Ripple); + public override bool OnTouchEvent(MotionEvent e) + { + InternalLogger.Debug(Tag, () => $"OnTouchEvent {e.Action}"); - group.AddView(_viewOverlay); - _viewOverlay.BringToFront(); + return base.OnTouchEvent(e); } + } - protected override void OnDetached() { - if (IsDisposed) return; + private const string Tag = "TouchEffectAndroid"; - if (Container is not ViewGroup group) - { - return; - } + private byte _alpha; - group.RemoveView(_viewOverlay); - _viewOverlay.Pressed = false; - _viewOverlay.Foreground = null; - _viewOverlay.Dispose(); - Container.LayoutChange -= ViewOnLayoutChange; + private ObjectAnimator _animator; - if (EnableRipple) - _ripple?.Dispose(); + private Color _color; - TouchCollector.Delete(View, OnTouch, ActionType.Ripple); - } + private RippleDrawable _ripple; - protected override void OnElementPropertyChanged(PropertyChangedEventArgs e) { - base.OnElementPropertyChanged(e); + private View _viewOverlay; - if (e.PropertyName == TouchEffect.ColorProperty.PropertyName) { - SetEffectColor(); - } + public static bool EnableRipple => Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop; + + public bool IsDisposed => Container == null || Container.Handle == IntPtr.Zero; + + public View View => Control ?? Container; + + public static void Init() + { + } + + protected override void OnAttached() + { + InternalLogger.Debug(Tag, () => "OnAttached"); + + if (Control is ListView || Control is ScrollView) + { + return; } - void SetEffectColor() { - var color = TouchEffect.GetColor(Element); - if (color == Colors.Transparent) { - return; - } + if (Container is not ViewGroup group) + { + throw new InvalidOperationException( + "Touch color effect requires to be attached to a container like a ContentView or a layout (Grid, StackLayout, etc...)"); + } - _color = color.ToAndroid(); - _alpha = _color.A == 255 ? (byte)80 : _color.A; + View.Clickable = true; + View.LongClickable = true; + View.SoundEffectsEnabled = true; - if (EnableRipple) { - _ripple.SetColor(GetPressedColorSelector(_color)); - } + _viewOverlay = new RippleOverlay(Container.Context) + { + LayoutParameters = new ViewGroup.LayoutParams(-1, -1), + Clickable = false, + Focusable = false, + }; + + Container.LayoutChange += ViewOnLayoutChange; + + if (EnableRipple) + { + _viewOverlay.Background = CreateRipple(_color); } - void OnTouch(View.TouchEventArgs args) { - switch (args.Event.Action) { - case MotionEventActions.Down: - if (EnableRipple) - ForceStartRipple(args.Event.GetX(), args.Event.GetY()); - else - BringLayer(); + SetEffectColor(); + TouchCollector.Add(View, OnTouch, ActionType.Ripple); - break; + group.AddView(_viewOverlay); + _viewOverlay.BringToFront(); + } - case MotionEventActions.Up: - case MotionEventActions.Cancel: - if (IsDisposed) return; + protected override void OnDetached() + { + InternalLogger.Debug(Tag, () => "OnDetached"); - if (EnableRipple) - ForceEndRipple(); - else - TapAnimation(250, _alpha, 0); + if (IsDisposed) + { + return; + } - break; - } + if (Container is not ViewGroup group) + { + return; } - void ViewOnLayoutChange(object sender, View.LayoutChangeEventArgs layoutChangeEventArgs) { - var group = (ViewGroup)sender; - if (group == null || IsDisposed) return; - _viewOverlay.Right = group.Width; - _viewOverlay.Bottom = group.Height; + group.RemoveView(_viewOverlay); + _viewOverlay.Pressed = false; + _viewOverlay.Foreground = null; + _viewOverlay.Dispose(); + Container.LayoutChange -= ViewOnLayoutChange; + + if (EnableRipple) + { + _ripple?.Dispose(); } - #region Ripple + TouchCollector.Delete(View, OnTouch, ActionType.Ripple); + } - RippleDrawable CreateRipple(Color color) { - if (Element is Layout) { - var mask = new ColorDrawable(Color.White); - return _ripple = new RippleDrawable(GetPressedColorSelector(color), null, mask); - } + protected override void OnElementPropertyChanged(PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(e); - var back = View.Background; - if (back == null) { - var mask = new ColorDrawable(Color.White); - return _ripple = new RippleDrawable(GetPressedColorSelector(color), null, mask); - } + if (e.PropertyName == TouchEffect.ColorProperty.PropertyName) + { + SetEffectColor(); + } + } - if (back is RippleDrawable) { - _ripple = (RippleDrawable)back.GetConstantState().NewDrawable(); - _ripple.SetColor(GetPressedColorSelector(color)); + private void SetEffectColor() + { + var color = TouchEffect.GetColor(Element); + if (color == Colors.Transparent) + { + return; + } - return _ripple; - } + _color = color.ToPlatform(); + _alpha = _color.A == 255 ? (byte)80 : _color.A; - return _ripple = new RippleDrawable(GetPressedColorSelector(color), back, null); + if (EnableRipple) + { + _ripple.SetColor(GetPressedColorSelector(_color)); } + } + + private void OnTouch(View.TouchEventArgs args) + { + InternalLogger.Debug(Tag, () => $"OnTouch:{args.Event.Action}"); + + switch (args.Event.Action) + { + case MotionEventActions.Down: + if (EnableRipple) + { + InternalLogger.Debug(Tag, () => "ripple start"); + ForceStartRipple(args.Event.GetX(), args.Event.GetY()); + } + else + { + BringLayer(); + } + + break; + + case MotionEventActions.Up: + case MotionEventActions.Cancel: + case MotionEventActions.Move: + if (IsDisposed) + { + return; + } + + if (EnableRipple) + { + InternalLogger.Debug(Tag, () => "ripple end"); + ForceEndRipple(); + } + else + { + TapAnimation(250, _alpha, 0); + } + + break; + } + } - static ColorStateList GetPressedColorSelector(int pressedColor) { - return new ColorStateList( - new[] { new int[] { } }, - new[] { pressedColor, }); + private void ViewOnLayoutChange(object sender, View.LayoutChangeEventArgs layoutChangeEventArgs) + { + var group = (ViewGroup)sender; + if (group == null || IsDisposed) + { + return; } - void ForceStartRipple(float x, float y) { - if (IsDisposed || !(_viewOverlay.Background is RippleDrawable bc)) return; + _viewOverlay.Right = group.Width; + _viewOverlay.Bottom = group.Height; + } + + private RippleDrawable CreateRipple(Color color) + { + if (Element is Layout) + { + var mask = new ColorDrawable(Color.White); + return _ripple = new RippleDrawable(GetPressedColorSelector(color), null, mask); + } - _viewOverlay.BringToFront(); - bc.SetHotspot(x, y); - _viewOverlay.Pressed = true; + var back = View.Background; + if (back == null) + { + var mask = new ColorDrawable(Color.White); + return _ripple = new RippleDrawable(GetPressedColorSelector(color), null, mask); } - void ForceEndRipple() { - if (IsDisposed) return; + if (back is RippleDrawable) + { + _ripple = (RippleDrawable)back.GetConstantState() + .NewDrawable(); + _ripple.SetColor(GetPressedColorSelector(color)); - _viewOverlay.Pressed = false; + return _ripple; } - #endregion + return _ripple = new RippleDrawable(GetPressedColorSelector(color), back, null); + } - #region Overlay + private static ColorStateList GetPressedColorSelector(int pressedColor) + { + return new ColorStateList([Array.Empty()], [pressedColor]); + } - void BringLayer() { - if (IsDisposed) - return; + private void ForceStartRipple(float x, float y) + { + if (IsDisposed || _viewOverlay.Background is not RippleDrawable bc) + { + return; + } - ClearAnimation(); + _viewOverlay.BringToFront(); + bc.SetHotspot(x, y); + _viewOverlay.Pressed = true; + } - _viewOverlay.BringToFront(); - var color = _color; - color.A = _alpha; - _viewOverlay.SetBackgroundColor(color); + private void ForceEndRipple() + { + if (IsDisposed) + { + return; } - void TapAnimation(long duration, byte startAlpha, byte endAlpha) { - if (IsDisposed) - return; + _viewOverlay.Pressed = false; + } + + private void BringLayer() + { + if (IsDisposed) + { + return; + } - _viewOverlay.BringToFront(); + ClearAnimation(); - var start = _color; - var end = _color; - start.A = startAlpha; - end.A = endAlpha; + _viewOverlay.BringToFront(); + var color = _color; + color.A = _alpha; + _viewOverlay.SetBackgroundColor(color); + } - ClearAnimation(); - _animator = ObjectAnimator.OfObject(_viewOverlay, - "BackgroundColor", - new ArgbEvaluator(), - start.ToArgb(), - end.ToArgb()); - _animator.SetDuration(duration); - _animator.RepeatCount = 0; - _animator.RepeatMode = ValueAnimatorRepeatMode.Restart; - _animator.Start(); - _animator.AnimationEnd += AnimationOnAnimationEnd; + private void TapAnimation(long duration, byte startAlpha, byte endAlpha) + { + if (IsDisposed) + { + return; } - void AnimationOnAnimationEnd(object sender, EventArgs eventArgs) { - if (IsDisposed) return; + _viewOverlay.BringToFront(); + + var start = _color; + var end = _color; + start.A = startAlpha; + end.A = endAlpha; + + ClearAnimation(); + _animator = ObjectAnimator.OfObject( + _viewOverlay, + "BackgroundColor", + new ArgbEvaluator(), + start.ToArgb(), + end.ToArgb()); + _animator.SetDuration(duration); + _animator.RepeatCount = 0; + _animator.RepeatMode = ValueAnimatorRepeatMode.Restart; + _animator.Start(); + _animator.AnimationEnd += AnimationOnAnimationEnd; + } - ClearAnimation(); + private void AnimationOnAnimationEnd(object sender, EventArgs eventArgs) + { + if (IsDisposed) + { + return; } - void ClearAnimation() { - if (_animator == null) return; - _animator.AnimationEnd -= AnimationOnAnimationEnd; - _animator.Cancel(); - _animator.Dispose(); - _animator = null; + ClearAnimation(); + } + + private void ClearAnimation() + { + if (_animator == null) + { + return; } - #endregion + _animator.AnimationEnd -= AnimationOnAnimationEnd; + _animator.Cancel(); + _animator.Dispose(); + _animator = null; } -} +} \ No newline at end of file diff --git a/Maui.Tabs/Platforms/iOS/CommandsPlatform.cs b/Maui.Tabs/Platforms/iOS/CommandsPlatform.cs index a554502..1f95ef8 100644 --- a/Maui.Tabs/Platforms/iOS/CommandsPlatform.cs +++ b/Maui.Tabs/Platforms/iOS/CommandsPlatform.cs @@ -3,8 +3,6 @@ using Microsoft.Maui.Controls.Platform; -using Sharpnado.Tabs.Effects; -using Sharpnado.Tabs.Effects.iOS; using Sharpnado.Tabs.Effects.iOS.GestureCollectors; using Sharpnado.Tabs.Effects.iOS.GestureRecognizers; @@ -28,7 +26,7 @@ protected override void OnAttached() { UpdateLongTap(); UpdateLongTapParameter(); - TouchGestureCollector.Add(View, OnTouch); + TouchGestureCollector.Add(View, OnTouch, ActionType.Tap); } protected override void OnDetached() { diff --git a/Maui.Tabs/Platforms/iOS/TouchEffectPlatform.cs b/Maui.Tabs/Platforms/iOS/TouchEffectPlatform.cs index e41cf2d..5ac3b68 100644 --- a/Maui.Tabs/Platforms/iOS/TouchEffectPlatform.cs +++ b/Maui.Tabs/Platforms/iOS/TouchEffectPlatform.cs @@ -1,107 +1,130 @@ using System.ComponentModel; + using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Platform; + using ObjCRuntime; + using Sharpnado.Tabs.Effects.iOS.GestureCollectors; using Sharpnado.Tabs.Effects.iOS.GestureRecognizers; + using UIKit; -namespace Sharpnado.Tabs.Effects.iOS { - public class TouchEffectPlatform : PlatformEffect - { - public bool IsDisposed => Container == null || Container.Handle == NativeHandle.Zero; - public UIView View => Control ?? Container; +namespace Sharpnado.Tabs.Effects.iOS; - UIView _layer; - float _alpha; +public class TouchEffectPlatform : PlatformEffect +{ + private const string Tag = "TouchEffectiOS"; - protected override void OnAttached() { - View.UserInteractionEnabled = true; - _layer = new UIView { - UserInteractionEnabled = false, - Opaque = false, - Alpha = 0, - TranslatesAutoresizingMaskIntoConstraints = false - }; + private float _alpha; - UpdateEffectColor(); - TouchGestureCollector.Add(View, OnTouch); - - View.AddSubview(_layer); - View.BringSubviewToFront(_layer); - _layer.TopAnchor.ConstraintEqualTo(View.TopAnchor).Active = true; - _layer.LeftAnchor.ConstraintEqualTo(View.LeftAnchor).Active = true; - _layer.BottomAnchor.ConstraintEqualTo(View.BottomAnchor).Active = true; - _layer.RightAnchor.ConstraintEqualTo(View.RightAnchor).Active = true; - } + private UIView _layer; - protected override void OnDetached() { - TouchGestureCollector.Delete(View, OnTouch); - _layer?.RemoveFromSuperview(); - _layer?.Dispose(); - } + public bool IsDisposed => Container == null || Container.Handle == NativeHandle.Zero; - void OnTouch(TouchGestureRecognizer.TouchArgs e) { - switch (e.State) { - case TouchGestureRecognizer.TouchState.Started: - InternalLogger.Debug($"OnTouch Started"); - BringLayer(); - break; - - case TouchGestureRecognizer.TouchState.Ended: - InternalLogger.Debug($"OnTouch Ended"); - EndAnimation(); - break; - - case TouchGestureRecognizer.TouchState.Cancelled: - InternalLogger.Debug($"OnTouch Cancelled"); - if (!IsDisposed && _layer != null) { - _layer.Layer.RemoveAllAnimations(); - _layer.Alpha = 0; - } - - break; - } - } + public UIView View => Control ?? Container; - protected override void OnElementPropertyChanged(PropertyChangedEventArgs e) { - base.OnElementPropertyChanged(e); + protected override void OnAttached() + { + View.UserInteractionEnabled = true; + _layer = new UIView + { + UserInteractionEnabled = false, + Opaque = false, + Alpha = 0, + TranslatesAutoresizingMaskIntoConstraints = false, + }; + + UpdateEffectColor(); + TouchGestureCollector.Add(View, OnTouch, ActionType.Color); + + View.AddSubview(_layer); + View.BringSubviewToFront(_layer); + _layer.TopAnchor.ConstraintEqualTo(View.TopAnchor) + .Active = true; + _layer.LeftAnchor.ConstraintEqualTo(View.LeftAnchor) + .Active = true; + _layer.BottomAnchor.ConstraintEqualTo(View.BottomAnchor) + .Active = true; + _layer.RightAnchor.ConstraintEqualTo(View.RightAnchor) + .Active = true; + } - if (e.PropertyName == TouchEffect.ColorProperty.PropertyName) { - UpdateEffectColor(); - } - } + protected override void OnDetached() + { + TouchGestureCollector.Delete(View, OnTouch); + _layer?.RemoveFromSuperview(); + _layer?.Dispose(); + } - void UpdateEffectColor() { - var color = TouchEffect.GetColor(Element); - if (color == Colors.Transparent) { - return; - } + private void OnTouch(TouchGestureRecognizer.TouchArgs e) + { + switch (e.State) + { + case TouchGestureRecognizer.TouchState.Started: + InternalLogger.Debug(Tag, () => "OnTouch Started"); + BringLayer(); + break; + + case TouchGestureRecognizer.TouchState.Ended: + InternalLogger.Debug(Tag, () => "OnTouch Ended"); + EndAnimation(); + break; + + case TouchGestureRecognizer.TouchState.Cancelled: + InternalLogger.Debug(Tag, () => "OnTouch Cancelled"); + if (!IsDisposed && _layer != null) + { + _layer.Layer.RemoveAllAnimations(); + _layer.Alpha = 0; + } - InternalLogger.Debug($"UpdateEffectColor"); - _alpha = color.Alpha < 1.0 ? 1 : (float)0.3; - _layer.BackgroundColor = color.ToPlatform(); + break; } + } - void BringLayer() { - InternalLogger.Debug($"BringLayer"); - _layer.Layer.RemoveAllAnimations(); - _layer.Alpha = _alpha; - View.BringSubviewToFront(_layer); + protected override void OnElementPropertyChanged(PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(e); + + if (e.PropertyName == TouchEffect.ColorProperty.PropertyName) + { + UpdateEffectColor(); } + } - void EndAnimation() { - if (!IsDisposed && _layer != null) { - InternalLogger.Debug($"EndAnimation"); - _layer.Layer.RemoveAllAnimations(); - UIView.Animate(0.225, - () => { - _layer.Alpha = 0; - }); - } + private void UpdateEffectColor() + { + var color = TouchEffect.GetColor(Element); + if (color == Colors.Transparent) + { + return; } - public static void Init() { + InternalLogger.Debug(Tag, () => "UpdateEffectColor"); + _alpha = color.Alpha < 1.0 ? 1 : (float)0.3; + _layer.BackgroundColor = color.ToPlatform(); + } + + private void BringLayer() + { + InternalLogger.Debug(Tag, () => "BringLayer"); + _layer.Layer.RemoveAllAnimations(); + _layer.Alpha = _alpha; + View.BringSubviewToFront(_layer); + } + + private void EndAnimation() + { + if (!IsDisposed && _layer != null) + { + InternalLogger.Debug(Tag, () => "EndAnimation"); + _layer.Layer.RemoveAllAnimations(); + UIView.Animate(0.225, () => { _layer.Alpha = 0; }); } } -} + + public static void Init() + { + } +} \ No newline at end of file diff --git a/Maui.Tabs/Platforms/iOS/TouchGestureCollector.cs b/Maui.Tabs/Platforms/iOS/TouchGestureCollector.cs index e9afda7..a5026d3 100644 --- a/Maui.Tabs/Platforms/iOS/TouchGestureCollector.cs +++ b/Maui.Tabs/Platforms/iOS/TouchGestureCollector.cs @@ -2,54 +2,108 @@ using UIKit; -namespace Sharpnado.Tabs.Effects.iOS.GestureCollectors { - internal static class TouchGestureCollector { - static Dictionary Collection { get; } = - new Dictionary(); - - public static void Add(UIView view, Action action) { - if (Collection.ContainsKey(view)) { - Collection[view].Actions.Add(action); - } - else { - var gest = new TouchGestureRecognizer { - CancelsTouchesInView = false, - Delegate = new TouchGestureRecognizerDelegate(view) - }; - gest.OnTouch += ActionActivator; - Collection.Add(view, - new GestureActionsContainer { - Recognizer = gest, - Actions = new List> { action } - }); - view.AddGestureRecognizer(gest); - } +namespace Sharpnado.Tabs.Effects.iOS.GestureCollectors; + +internal enum ActionType +{ + Color = 0, + Tap = 1, +} + +internal static class TouchGestureCollector +{ + private static Dictionary Collection { get; } = []; + + public static void Add(UIView view, Action action, ActionType actionType) + { + if (!Collection.TryGetValue(view, out var value)) + { + var gest = new TouchGestureRecognizer + { + CancelsTouchesInView = false, + Delegate = new TouchGestureRecognizerDelegate(view), + }; + + gest.OnTouch += ActionActivator; + value = new GestureActionsContainer + { + Recognizer = gest, + }; + + Collection.Add(view, value); + view.AddGestureRecognizer(gest); + } + + switch (actionType) + { + case ActionType.Color: + value.ColorActions.Add(action); + break; + case ActionType.Tap: + value.TapActions.Add(action); + break; + default: + throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null); } + } - public static void Delete(UIView view, Action action) { - if (!Collection.ContainsKey(view)) return; + public static void Delete(UIView view, Action action) + { + if (!Collection.ContainsKey(view)) + { + return; + } - var ci = Collection[view]; - ci.Actions.Remove(action); + var ci = Collection[view]; + ci.RemoveAction(action); - if (ci.Actions.Count != 0) return; - view.RemoveGestureRecognizer(ci.Recognizer); - Collection.Remove(view); + if (ci.ActionCount != 0) + { + return; } - static void ActionActivator(object sender, TouchGestureRecognizer.TouchArgs e) { - var gest = (TouchGestureRecognizer)sender; - if (!Collection.ContainsKey(gest.View)) return; + view.RemoveGestureRecognizer(ci.Recognizer); + Collection.Remove(view); + } - var actions = Collection[gest.View].Actions.ToArray(); - foreach (var valueAction in actions) { - valueAction?.Invoke(e); - } + private static void ActionActivator(object sender, TouchGestureRecognizer.TouchArgs e) + { + var gest = (TouchGestureRecognizer)sender; + if (!Collection.ContainsKey(gest.View)) + { + return; + } + + var actions = Collection[gest.View] + .Actions.ToArray(); + foreach (var valueAction in actions) + { + valueAction?.Invoke(e); } + } + + private class GestureActionsContainer + { + public TouchGestureRecognizer Recognizer { get; init; } + + public IEnumerable> Actions => ColorActions.Concat(TapActions); - class GestureActionsContainer { - public TouchGestureRecognizer Recognizer { get; set; } - public List> Actions { get; set; } + public List> TapActions { get; } = []; + + public List> ColorActions { get; } = []; + + public int ActionCount => TapActions.Count + ColorActions.Count; + + public void RemoveAction(Action action) + { + if (TapActions.Contains(action)) + { + TapActions.Remove(action); + } + else if (ColorActions.Contains(action)) + { + ColorActions.Remove(action); + } } } } \ No newline at end of file diff --git a/Maui.Tabs/Platforms/iOS/TouchGestureRecognizer.cs b/Maui.Tabs/Platforms/iOS/TouchGestureRecognizer.cs index c2817e6..517d6d4 100644 --- a/Maui.Tabs/Platforms/iOS/TouchGestureRecognizer.cs +++ b/Maui.Tabs/Platforms/iOS/TouchGestureRecognizer.cs @@ -1,125 +1,165 @@ -using Foundation; +using CoreFoundation; + +using Foundation; + using UIKit; -using CoreFoundation; -namespace Sharpnado.Tabs.Effects.iOS.GestureRecognizers { - public class TouchGestureRecognizer : UIGestureRecognizer { - public class TouchArgs : EventArgs { - public TouchState State { get; } - public bool Inside { get; } +namespace Sharpnado.Tabs.Effects.iOS.GestureRecognizers; - public TouchArgs(TouchState state, bool inside) { - State = state; - Inside = inside; - } - } +public class TouchGestureRecognizer : UIGestureRecognizer +{ + public enum TouchState + { + Started, - public enum TouchState { - Started, - Ended, - Cancelled - } + Ended, - bool _disposed; - bool _startCalled; + Cancelled + } - public static bool IsActive { get; private set; } + private bool _disposed; - public bool Processing => State == UIGestureRecognizerState.Began || State == UIGestureRecognizerState.Changed; - public event EventHandler OnTouch; + private bool _startCalled; - public override async void TouchesBegan(NSSet touches, UIEvent evt) { - base.TouchesBegan(touches, evt); - if (Processing) - return; + public static bool IsActive { get; private set; } - State = UIGestureRecognizerState.Began; - IsActive = true; - _startCalled = false; + public bool Processing => State == UIGestureRecognizerState.Began || State == UIGestureRecognizerState.Changed; - await Task.Delay(125); - DispatchQueue.MainQueue.DispatchAsync(() => { - if (!Processing || _disposed) return; - OnTouch?.Invoke(this, new TouchArgs(TouchState.Started, true)); - _startCalled = true; - }); + public event EventHandler OnTouch; + + public override async void TouchesBegan(NSSet touches, UIEvent evt) + { + base.TouchesBegan(touches, evt); + if (Processing) + { + return; } - public override void TouchesMoved(NSSet touches, UIEvent evt) { - base.TouchesMoved(touches, evt); + State = UIGestureRecognizerState.Began; + IsActive = true; + _startCalled = false; - var inside = View.PointInside(LocationInView(View), evt); + await Task.Delay(125); + DispatchQueue.MainQueue.DispatchAsync( + () => + { + if (!Processing || _disposed) + { + return; + } - if (!inside) { - if (_startCalled) - OnTouch?.Invoke(this, new TouchArgs(TouchState.Ended, false)); - State = UIGestureRecognizerState.Ended; - IsActive = false; - return; - } + OnTouch?.Invoke(this, new TouchArgs(TouchState.Started, true)); + _startCalled = true; + }); + } - State = UIGestureRecognizerState.Changed; - } + public override void TouchesMoved(NSSet touches, UIEvent evt) + { + base.TouchesMoved(touches, evt); - public override void TouchesEnded(NSSet touches, UIEvent evt) { - base.TouchesEnded(touches, evt); + bool inside = View.PointInside(LocationInView(View), evt); - if (!_startCalled) - OnTouch?.Invoke(this, new TouchArgs(TouchState.Started, true)); + if (!inside) + { + if (_startCalled) + { + OnTouch?.Invoke(this, new TouchArgs(TouchState.Ended, false)); + } - OnTouch?.Invoke(this, new TouchArgs(TouchState.Ended, View.PointInside(LocationInView(View), null))); State = UIGestureRecognizerState.Ended; IsActive = false; + return; } - public override void TouchesCancelled(NSSet touches, UIEvent evt) { - base.TouchesCancelled(touches, evt); - OnTouch?.Invoke(this, new TouchArgs(TouchState.Cancelled, false)); - State = UIGestureRecognizerState.Cancelled; - IsActive = false; - } + State = UIGestureRecognizerState.Changed; + } - internal void TryEndOrFail() { - if (_startCalled) { - OnTouch?.Invoke(this, new TouchArgs(TouchState.Ended, false)); - State = UIGestureRecognizerState.Ended; - } + public override void TouchesEnded(NSSet touches, UIEvent evt) + { + base.TouchesEnded(touches, evt); - State = UIGestureRecognizerState.Failed; - IsActive = false; + if (!_startCalled) + { + OnTouch?.Invoke(this, new TouchArgs(TouchState.Started, true)); } - protected override void Dispose(bool disposing) { - _disposed = true; - IsActive = false; + OnTouch?.Invoke(this, new TouchArgs(TouchState.Ended, View.PointInside(LocationInView(View), null))); + State = UIGestureRecognizerState.Ended; + IsActive = false; + } - base.Dispose(disposing); + public override void TouchesCancelled(NSSet touches, UIEvent evt) + { + base.TouchesCancelled(touches, evt); + OnTouch?.Invoke(this, new TouchArgs(TouchState.Cancelled, false)); + State = UIGestureRecognizerState.Cancelled; + IsActive = false; + } + + internal void TryEndOrFail() + { + if (_startCalled) + { + OnTouch?.Invoke(this, new TouchArgs(TouchState.Ended, false)); + State = UIGestureRecognizerState.Ended; } + + State = UIGestureRecognizerState.Failed; + IsActive = false; } - public class TouchGestureRecognizerDelegate : UIGestureRecognizerDelegate { - readonly UIView _view; + protected override void Dispose(bool disposing) + { + _disposed = true; + IsActive = false; - public TouchGestureRecognizerDelegate(UIView view) { - _view = view; + base.Dispose(disposing); + } + + public class TouchArgs : EventArgs + { + public TouchArgs(TouchState state, bool inside) + { + State = state; + Inside = inside; } - public override bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, - UIGestureRecognizer otherGestureRecognizer) { - if (gestureRecognizer is TouchGestureRecognizer rec && otherGestureRecognizer is UIPanGestureRecognizer && - otherGestureRecognizer.State == UIGestureRecognizerState.Began) { - rec.TryEndOrFail(); - } + public TouchState State { get; } + + public bool Inside { get; } + } +} + +public class TouchGestureRecognizerDelegate : UIGestureRecognizerDelegate +{ + private readonly UIView _view; - return true; + public TouchGestureRecognizerDelegate(UIView view) + { + _view = view; + } + + public override bool ShouldRecognizeSimultaneously( + UIGestureRecognizer gestureRecognizer, + UIGestureRecognizer otherGestureRecognizer) + { + if (gestureRecognizer is TouchGestureRecognizer rec + && otherGestureRecognizer is UIPanGestureRecognizer + && otherGestureRecognizer.State == UIGestureRecognizerState.Began) + { + rec.TryEndOrFail(); } - public override bool ShouldReceiveTouch(UIGestureRecognizer recognizer, UITouch touch) { - if (recognizer is TouchGestureRecognizer && TouchGestureRecognizer.IsActive) { - return false; - } + return true; + } - return touch.View.IsDescendantOfView(_view); + public override bool ShouldReceiveTouch(UIGestureRecognizer recognizer, UITouch touch) + { + if (recognizer is TouchGestureRecognizer && TouchGestureRecognizer.IsActive) + { + return false; } + + return touch.View.IsDescendantOfView(_view); } } \ No newline at end of file diff --git a/MauiSample/MainPage.xaml b/MauiSample/MainPage.xaml index cfd9e30..afa3d9c 100644 --- a/MauiSample/MainPage.xaml +++ b/MauiSample/MainPage.xaml @@ -78,7 +78,7 @@ SegmentedOutlineColor="{StaticResource Gray950}" SelectedIndex="{Binding Source={x:Reference Switcher}, Path=SelectedIndex, Mode=TwoWay}" TabType="Fixed" - UseMauiTapGesture="True"> + UseMauiTapGesture="False"> + TabType="Scrollable">