Trying to migrate Xamarin Forms Effect functionality to Maui
I am trying to migrate a portion of the code in the Xamarin Forms samples Touch-Tracking Effect Demos to Maui.
I created a small project for Xamarin Forms that only includes the BoxViewDraggingPage and that works fine. So then I am trying to create the Maui version and have looked at the Maui Controls Sample for a focus effect. However, the touch tracking effect has some additional complexity beyond what that sample shows that I am needing help with.
So in the Xamarin Forms version, there is a TouchEffect.cs under each platform's project. In the Maui version that I created, I followed the example in the Maui sample and have attempted to create a single TouchEffect.cs at the root that uses conditional compilation for each platform. However, I am having trouble having the BoxViewDraggingPage.cs compile because it is referring to the TouchEffect class (that inherits from Routing Effect) yet all of the public properties and events are in the TouchPlatformEffect class. But when I try to put those properties in the TouchEffect class, I get compilation errors of various types within the different platforms that I can't resolve. I can't figure out if I am supposed to be referencing the TouchEffect class in the platform code or the TouchPlatformEffect. In the original Xamarin Forms code, there wasn't two different classes like that, the platform classes were named the same as the shared class they were exporting to. Right now I am referencing the TouchPlatformEffect, but if it needs to be changed to the TouchEffect class, then I need help in how to do that without the compilation errors. Below is the code I have for the relevant classes.
So the BoxViewDraggingPage.cs has not been changed, but the problems I have are the sections of code referring to properties and events for the effect. Here is one of them:
void AddBoxViewToLayout()
{
...
TouchEffect touchEffect = new TouchEffect();
touchEffect.TouchAction += OnTouchEffectAction;
'''
}
And here is the full code for TouchEffect.cs:
using Microsoft.Maui.Platform;
using Microsoft.Maui.Controls.Platform;
#if WINDOWS
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
#elif __ANDROID__
using View = Android.Views.View;
#elif __IOS__
using UIKit;
using CoreGraphics;
using Foundation;
#endif
namespace EffectsMaui
{
public class TouchEffect : RoutingEffect
{
}
public class TouchPlatformEffect : PlatformEffect
{
public TouchPlatformEffect() : base()
{
}
public event TouchActionEventHandler TouchAction;
public bool Capture { set; get; }
public void OnTouchAction(Element element, TouchActionEventArgs args)
{
TouchAction?.Invoke(element, args);
}
#if WINDOWS
FrameworkElement frameworkElement;
TouchPlatformEffect touchPlatformEffect;
Action<Element, TouchActionEventArgs> onTouchAction;
protected override void OnAttached()
{
// Get the Windows FrameworkElement corresponding to the Element that the effect is attached to
frameworkElement = Control == null ? Container : Control;
// Get access to the TouchEffect class in the .NET Standard library
touchPlatformEffect = (TouchPlatformEffect)Element.Effects.
FirstOrDefault(e => e is TouchPlatformEffect);
if (touchPlatformEffect != null && frameworkElement != null)
{
// Save the method to call on touch events
onTouchAction = touchPlatformEffect.OnTouchAction;
// Set event handlers on FrameworkElement
frameworkElement.PointerEntered += OnPointerEntered;
frameworkElement.PointerPressed += OnPointerPressed;
frameworkElement.PointerMoved += OnPointerMoved;
frameworkElement.PointerReleased += OnPointerReleased;
frameworkElement.PointerExited += OnPointerExited;
frameworkElement.PointerCanceled += OnPointerCancelled;
}
}
protected override void OnDetached()
{
if (onTouchAction != null)
{
// Release event handlers on FrameworkElement
frameworkElement.PointerEntered -= OnPointerEntered;
frameworkElement.PointerPressed -= OnPointerPressed;
frameworkElement.PointerMoved -= OnPointerMoved;
frameworkElement.PointerReleased -= OnPointerReleased;
frameworkElement.PointerExited -= OnPointerEntered;
frameworkElement.PointerCanceled -= OnPointerCancelled;
}
}
void OnPointerEntered(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Entered, args);
}
void OnPointerPressed(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Pressed, args);
// Check setting of Capture property
if (touchPlatformEffect.Capture)
{
(sender as FrameworkElement).CapturePointer(args.Pointer);
}
}
void OnPointerMoved(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Moved, args);
}
void OnPointerReleased(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Released, args);
}
void OnPointerExited(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Exited, args);
}
void OnPointerCancelled(object sender, PointerRoutedEventArgs args)
{
CommonHandler(sender, TouchActionType.Cancelled, args);
}
void CommonHandler(object sender, TouchActionType touchActionType, PointerRoutedEventArgs args)
{
Microsoft.UI.Input.PointerPoint pointerPoint = args.GetCurrentPoint(sender as UIElement);
Windows.Foundation.Point windowsPoint = pointerPoint.Position;
onTouchAction(Element, new TouchActionEventArgs(args.Pointer.PointerId,
touchActionType,
new Point(windowsPoint.X, windowsPoint.Y),
args.Pointer.IsInContact));
}
#elif __ANDROID__
View view;
Element formsElement;
TouchPlatformEffect libTouchEffect;
bool capture;
Func<double, double> fromPixels;
int[] twoIntArray = new int[2];
static Dictionary<View, TouchPlatformEffect> viewDictionary =
new Dictionary<View, TouchPlatformEffect>();
static Dictionary<int, TouchPlatformEffect> idToEffectDictionary =
new Dictionary<int, TouchPlatformEffect>();
protected override void OnAttached()
{
// Get the Android View corresponding to the Element that the effect is attached to
view = Control == null ? Container : Control;
// Get access to the TouchEffect class in the .NET Standard library
TouchPlatformEffect touchEffect =
(TouchPlatformEffect)Element.Effects.
FirstOrDefault(e => e is TouchPlatformEffect);
if (touchEffect != null && view != null)
{
viewDictionary.Add(view, this);
formsElement = Element;
libTouchEffect = touchEffect;
// Save fromPixels function
fromPixels = view.Context.FromPixels;
// Set event handler on View
view.Touch += OnTouch;
}
}
protected override void OnDetached()
{
if (viewDictionary.ContainsKey(view))
{
viewDictionary.Remove(view);
view.Touch -= OnTouch;
}
}
void OnTouch(object sender, Android.Views.View.TouchEventArgs args)
{
// Two object common to all the events
View senderView = sender as View;
Android.Views.MotionEvent motionEvent = args.Event;
// Get the pointer index
int pointerIndex = motionEvent.ActionIndex;
// Get the id that identifies a finger over the course of its progress
int id = motionEvent.GetPointerId(pointerIndex);
senderView.GetLocationOnScreen(twoIntArray);
Point screenPointerCoords = new Point(twoIntArray[0] + motionEvent.GetX(pointerIndex),
twoIntArray[1] + motionEvent.GetY(pointerIndex));
// Use ActionMasked here rather than Action to reduce the number of possibilities
switch (args.Event.ActionMasked)
{
case Android.Views.MotionEventActions.Down:
case Android.Views.MotionEventActions.PointerDown:
FireEvent(this, id, TouchActionType.Pressed, screenPointerCoords, true);
idToEffectDictionary.Add(id, this);
capture = libTouchEffect.Capture;
break;
case Android.Views.MotionEventActions.Move:
// Multiple Move events are bundled, so handle them in a loop
for (pointerIndex = 0; pointerIndex < motionEvent.PointerCount; pointerIndex++)
{
id = motionEvent.GetPointerId(pointerIndex);
if (capture)
{
senderView.GetLocationOnScreen(twoIntArray);
screenPointerCoords = new Point(twoIntArray[0] + motionEvent.GetX(pointerIndex),
twoIntArray[1] + motionEvent.GetY(pointerIndex));
FireEvent(this, id, EffectsMaui.TouchActionType.Moved, screenPointerCoords, true);
}
else
{
CheckForBoundaryHop(id, screenPointerCoords);
if (idToEffectDictionary[id] != null)
{
FireEvent(idToEffectDictionary[id], id, EffectsMaui.TouchActionType.Moved, screenPointerCoords, true);
}
}
}
break;
case Android.Views.MotionEventActions.Up:
case Android.Views.MotionEventActions.Pointer1Up:
if (capture)
{
FireEvent(this, id, EffectsMaui.TouchActionType.Released, screenPointerCoords, false);
}
else
{
CheckForBoundaryHop(id, screenPointerCoords);
if (idToEffectDictionary[id] != null)
{
FireEvent(idToEffectDictionary[id], id, EffectsMaui.TouchActionType.Released, screenPointerCoords, false);
}
}
idToEffectDictionary.Remove(id);
break;
case Android.Views.MotionEventActions.Cancel:
if (capture)
{
FireEvent(this, id, EffectsMaui.TouchActionType.Cancelled, screenPointerCoords, false);
}
else
{
if (idToEffectDictionary[id] != null)
{
FireEvent(idToEffectDictionary[id], id, EffectsMaui.TouchActionType.Cancelled, screenPointerCoords, false);
}
}
idToEffectDictionary.Remove(id);
break;
}
}
void CheckForBoundaryHop(int id, Point pointerLocation)
{
TouchPlatformEffect touchPlatformEffectHit = null;
foreach (View view in viewDictionary.Keys)
{
// Get the view rectangle
try
{
view.GetLocationOnScreen(twoIntArray);
}
catch // System.ObjectDisposedException: Cannot access a disposed object.
{
continue;
}
Rect rect = new Rect(twoIntArray[0], twoIntArray[1], view.Width, view.Height);
if (rect.Contains(pointerLocation))
{
touchPlatformEffectHit = viewDictionary[view];
}
}
if (touchPlatformEffectHit != idToEffectDictionary[id])
{
if (idToEffectDictionary[id] != null)
{
FireEvent(idToEffectDictionary[id], id, EffectsMaui.TouchActionType.Exited, pointerLocation, true);
}
if (touchPlatformEffectHit != null)
{
FireEvent(touchPlatformEffectHit, id, EffectsMaui.TouchActionType.Entered, pointerLocation, true);
}
idToEffectDictionary[id] = touchPlatformEffectHit;
}
}
void FireEvent(TouchPlatformEffect touchPlatformEffect, int id, EffectsMaui.TouchActionType actionType, Point pointerLocation, bool isInContact)
{
// Get the method to call for firing events
Action<Element, EffectsMaui.TouchActionEventArgs> onTouchAction = touchPlatformEffect.libTouchEffect.OnTouchAction;
// Get the location of the pointer within the view
touchPlatformEffect.view.GetLocationOnScreen(twoIntArray);
double x = pointerLocation.X - twoIntArray[0];
double y = pointerLocation.Y - twoIntArray[1];
Point point = new Point(fromPixels(x), fromPixels(y));
// Call the method
onTouchAction(touchPlatformEffect.formsElement,
new EffectsMaui.TouchActionEventArgs(id, actionType, point, isInContact));
}
#elif __IOS__
UIView view;
TouchRecognizer touchRecognizer;
protected override void OnAttached()
{
// Get the iOS UIView corresponding to the Element that the effect is attached to
view = Control == null ? Container : Control;
// Uncomment this line if the UIView does not have touch enabled by default
//view.UserInteractionEnabled = true;
// Get access to the TouchEffect class in the .NET Standard library
TouchPlatformEffect effect = (TouchPlatformEffect)Element.Effects.FirstOrDefault(e => e is TouchPlatformEffect);
if (effect != null && view != null)
{
// Create a TouchRecognizer for this UIView
touchRecognizer = new TouchRecognizer(Element, view, effect);
view.AddGestureRecognizer(touchRecognizer);
}
}
protected override void OnDetached()
{
if (touchRecognizer != null)
{
// Clean up the TouchRecognizer object
touchRecognizer.Detach();
// Remove the TouchRecognizer from the UIView
view.RemoveGestureRecognizer(touchRecognizer);
}
}
#endif
}
#if __IOS__
class TouchRecognizer : UIGestureRecognizer
{
Element element; // Forms element for firing events
UIView view; // iOS UIView
EffectsMaui.TouchPlatformEffect touchPlatformEffect;
bool capture;
static Dictionary<UIView, TouchRecognizer> viewDictionary =
new Dictionary<UIView, TouchRecognizer>();
static Dictionary<long, TouchRecognizer> idToTouchDictionary =
new Dictionary<long, TouchRecognizer>();
public TouchRecognizer(Element element, UIView view, EffectsMaui.TouchPlatformEffect touchPlatformEffect)
{
this.element = element;
this.view = view;
this.touchPlatformEffect = touchPlatformEffect;
viewDictionary.Add(view, this);
}
public void Detach()
{
viewDictionary.Remove(view);
}
// touches = touches of interest; evt = all touches of type UITouch
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
foreach (UITouch touch in touches.Cast<UITouch>())
{
long id = ((IntPtr)touch.Handle).ToInt64();
FireEvent(this, id, EffectsMaui.TouchActionType.Pressed, touch, true);
if (!idToTouchDictionary.ContainsKey(id))
{
idToTouchDictionary.Add(id, this);
}
}
// Save the setting of the Capture property
capture = touchPlatformEffect.Capture;
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
base.TouchesMoved(touches, evt);
foreach (UITouch touch in touches.Cast<UITouch>())
{
long id = ((IntPtr)touch.Handle).ToInt64();
if (capture)
{
FireEvent(this, id, EffectsMaui.TouchActionType.Moved, touch, true);
}
else
{
CheckForBoundaryHop(touch);
if (idToTouchDictionary[id] != null)
{
FireEvent(idToTouchDictionary[id], id, EffectsMaui.TouchActionType.Moved, touch, true);
}
}
}
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
base.TouchesEnded(touches, evt);
foreach (UITouch touch in touches.Cast<UITouch>())
{
long id = ((IntPtr)touch.Handle).ToInt64();
if (capture)
{
FireEvent(this, id, EffectsMaui.TouchActionType.Released, touch, false);
}
else
{
CheckForBoundaryHop(touch);
if (idToTouchDictionary[id] != null)
{
FireEvent(idToTouchDictionary[id], id, EffectsMaui.TouchActionType.Released, touch, false);
}
}
idToTouchDictionary.Remove(id);
}
}
public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
base.TouchesCancelled(touches, evt);
foreach (UITouch touch in touches.Cast<UITouch>())
{
long id = ((IntPtr)touch.Handle).ToInt64();
if (capture)
{
FireEvent(this, id, EffectsMaui.TouchActionType.Cancelled, touch, false);
}
else if (idToTouchDictionary[id] != null)
{
FireEvent(idToTouchDictionary[id], id, EffectsMaui.TouchActionType.Cancelled, touch, false);
}
idToTouchDictionary.Remove(id);
}
}
void CheckForBoundaryHop(UITouch touch)
{
long id = ((IntPtr)touch.Handle).ToInt64();
// TODO: Might require converting to a List for multiple hits
TouchRecognizer recognizerHit = null;
foreach (UIView view in viewDictionary.Keys)
{
CGPoint location = touch.LocationInView(view);
if (new CGRect(new CGPoint(), view.Frame.Size).Contains(location))
{
recognizerHit = viewDictionary[view];
}
}
if (recognizerHit != idToTouchDictionary[id])
{
if (idToTouchDictionary[id] != null)
{
FireEvent(idToTouchDictionary[id], id, EffectsMaui.TouchActionType.Exited, touch, true);
}
if (recognizerHit != null)
{
FireEvent(recognizerHit, id, EffectsMaui.TouchActionType.Entered, touch, true);
}
idToTouchDictionary[id] = recognizerHit;
}
}
void FireEvent(TouchRecognizer recognizer, long id, EffectsMaui.TouchActionType actionType, UITouch touch, bool isInContact)
{
// Convert touch location to Xamarin.Forms Point value
CGPoint cgPoint = touch.LocationInView(recognizer.View);
Point xfPoint = new Point(cgPoint.X, cgPoint.Y);
// Get the method to call for firing events
Action<Element, EffectsMaui.TouchActionEventArgs> onTouchAction = recognizer.touchPlatformEffect.OnTouchAction;
// Call that method
onTouchAction(recognizer.element,
new EffectsMaui.TouchActionEventArgs(id, actionType, xfPoint, isInContact));
}
}
#endif
}