Xamarin.Forms - Snap behavior

256 Views Asked by At

Some years ago I made a class in Xamarin.iOS which was simply a UIView that snaps to the edges of the screen.

public class MovableUIButton : UIView
{
    private UIPanGestureRecognizer PanGesture;

    private UIButton btnShoppingCart;

    private UIDynamicAnimator animator;
    private PointF snapPoint;
    private bool isInitialized = false;
    private UISnapBehavior snap;

    // offsets used to position image relative to touch point while being dragged
    private float dx = 0;
    private float dy = 0;

    public event EventHandler TouchUpInside;

    public MovableUIButton(CGRect rect) : base(rect)
    {
        Initialize();
    }

    public MovableUIButton(IntPtr handle) : base(handle)
    {
        Initialize();
    }

    private void Initialize()
    {
        PanGesture = new UIPanGestureRecognizer(DidPan);
        this.AddGestureRecognizer(PanGesture);

        // Make it round
        this.Layer.CornerRadius = this.Bounds.Width / 2;
        this.BackgroundColor = UIColor.FromRGB(89f / 255f, 157f / 255f, 255f / 255f);

        // Set the Border
        this.Layer.BorderColor = UIColor.White.CGColor;
        this.Layer.BorderWidth = 3;

        // Set a Shadow
        this.Layer.ShadowColor = UIColor.White.CGColor;
        this.Layer.ShadowOpacity = .5f;
        this.Layer.ShadowRadius = 8.0f;
        this.Layer.ShadowOffset = new System.Drawing.SizeF(0f, 0f);

        btnShoppingCart = new UIButton(Bounds);
        btnShoppingCart.Font = FontAwesome.Font(30);
        btnShoppingCart.SetTitle(BaseFontAwesome.FAShoppingCart, UIControlState.Normal);
        btnShoppingCart.UserInteractionEnabled = true;
        btnShoppingCart.TouchUpInside += BtnShoppingCart_TouchUpInside;
        this.AddSubview(btnShoppingCart);
    }

    private void InitializeAnimator()
    {
        snapPoint = new PointF((float)SetX(Superview.Bounds), (float)SetY(Superview.Bounds));
        animator = new UIDynamicAnimator(Superview);
        isInitialized = true;
    }

    private void DidPan()
    {
        if (isInitialized == false)
        {
            InitializeAnimator();
        }

        if ((PanGesture.State == UIGestureRecognizerState.Began || PanGesture.State == UIGestureRecognizerState.Changed) && (PanGesture.NumberOfTouches == 1))
        {
            // remove any previosuly applied snap behavior to avoid a flicker that will occur if both the gesture and physics are operating on the view simultaneously
            if (snap != null)
                animator.RemoveBehavior(snap);

            var p0 = PanGesture.LocationInView(Superview);

            if (dx == 0)
                dx = (float)(p0.X - this.Center.X);

            if (dy == 0)
                dy = (float)(p0.Y - this.Center.Y);

            // this is where the offsets are applied so that the location of the image follows the point where the image is touched as it is dragged,
            // otherwise the center of the image would snap to the touch point at the start of the pan gesture
            var p1 = new PointF((float)(p0.X - dx), (float)(p0.Y - dy));

            this.Center = p1;
        }
        else if (PanGesture.State == UIGestureRecognizerState.Ended)
        {
            // reset offsets when dragging ends so that they will be recalculated for next touch and drag that occurs
            dx = 0;
            dy = 0;

            snapPoint = new PointF((float)SetX(Superview.Bounds), (float)SetY(Superview.Bounds));

            SnapImageIntoPlace((System.Drawing.PointF)PanGesture.LocationInView(Superview));
        }
    }

    void SnapImageIntoPlace(PointF touchPoint)
    {
        snap = new UISnapBehavior(this, snapPoint);
        animator.AddBehavior(snap);
    }

    private nfloat SetX(CGRect superBounds)
    {
        nfloat x = 0f;

        if (this.Center.X > superBounds.Width / 2)
        {
            x = superBounds.Width - 20;
        }
        else
        {
            x = 20;
        }

        return x;
    }

    private nfloat SetY(CGRect superBounds)
    {
        nfloat y = 0f;

        if (this.Center.Y < 20)
        {
            y = 20;
        }
        else if (this.Center.Y > superBounds.Height - 20)
        {
            y = superBounds.Height - 20;
        }
        else
        {
            return this.Center.Y;
        }

        return y;
    }

    public override void TouchesBegan(NSSet touches, UIEvent evt)
    {
        base.TouchesBegan(touches, evt);
    }

    private void BtnShoppingCart_TouchUpInside(object sender, EventArgs e)
    {
        TouchUpInside?.Invoke(this, null);
    }
}

This class contains a UISnapBehavior object which is not available in Xamarin.Forms. Because I want to port this to Xamarin.Forms, is there an easy workaround or a similar class to UISnapBehavior?

1

There are 1 best solutions below

0
On

Not sure if you're still waiting for an answer to this two years later, but hopefully this can still be useful to someone.

You should be able to take this class and put it in your .iOS project and create a custom renderer for a custom control in you pcl. something like this:

Custom Renderer in your iOS project:

[assembly: ExportRenderer(typeof(MovableUIButtonView), typeof(MovableButtonRenderer_iOS))]
namespace Test.iOS.Renderers
{
    public class MovableButtonRenderer_iOS : ViewRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<View> e)
        {
            if(Control == null)
                SetNativeControl(new MovableUIButton(Bounds));

            base.OnElementChanged(e);
        }
    }
}

Custom Control to be put into the pcl:

namespace Test.Controls
{
    public class MovableUIButtonView : ContentView
    {
    }
}

the XAML on whatever page you use it on:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:local="clr-namespace:Test.Controls"
             mc:Ignorable="d"
             x:Class="Test.TestVW">

    <Grid>
        <local:MovableUIButtonView></local:MovableUIButtonView>
    </Grid>

</ContentPage>