Trying to migrate Xamarin Forms Effect functionality to Maui

Vince L 41 Reputation points
2022-08-20T16:25:36.133+00:00

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  
}  
Developer technologies .NET .NET MAUI
{count} votes

Accepted answer
  1. Yonglun Liu (Shanghai Wicresoft Co,.Ltd.) 50,126 Reputation points Microsoft External Staff
    2022-08-22T08:56:14.087+00:00

    Hello,

    I've tested your code, and make it running correctly.

    You could refer to the following steps:

    Step1: Please register your effect in builder of the MauiProgram.cs at first:

       builder.ConfigureEffects(effects =>   
       {  
            effects.Add<TouchEffect, TouchPlatformEffect>();  
       })  
    

    Otherwise you will get the following error message:

    No service for type 'Microsoft.Maui.Controls.Hosting.EffectsFactory' has been registered.

    Step2: Please modify TouchEffect touchEffect = new TouchEffect(); to TouchPlatformEffect touchEffect = new TouchPlatformEffect();, due to there is no property named TouchAction in TouchEffect class.

    After that, the program will successfully run, and could correctly call the OnTouchEffectAction method.

    Best Regards,

    Alec Liu.


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.


0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.