Xamarin.Formsでドラッグアンドドロップによるリストの並び順の変更をやってみた

※今回作成したソースコードのリンクは、一番下に記載しています。

実装概要

ドラッグアンドドロップでリストの並び順を変更するために以下の3つのことを行いました。

  1. 画面でタップされた座標位置および、指が離れた座標位置の取得
  2. タップされてから指が離れるまでの移動距離の算出
  3. タップされた座標位置の項目を移動距離から算出した行数だけ上または下へ移動

今回は、画面で指が離れた位置座標の下にあるViewの特定方法がわからなかったため、移動距離をもとにリスト項目の入れ替えを行いました。
また、画面でタップされた座標位置および、指が離れた座標位置はドラッグアンドドロップ機能の実装により、対応を行いました。

ドラッグアンドドロップ機能の実装では、共通処理、Androidプロジェクトでの処理、iOSプロジェクトでの処理の大きく分けて3つの処理を実装しています。

ドラッグアンドドロップ機能の実装

共通の処理

作成したクラス

  • TouchEffect

画面をタップされたときに発生するイベントの呼び出し処理を実装する。
なお、イベント処理自体はAndroidiOSのそれぞれで異なるため、画面のタッチやドラッグなどのイベント発生時に行う処理は、各プロジェクトで実装を行う。

(実装イメージ)

public class TouchEffect : RoutingEffect
{
    public TouchEffect() : base("Effects.TouchEffect")
    {

    }

    public event TouchEventHundler OnTouch;

    public void OnTouchEvent(object obj, TouchEventArgs args)
    {
        OnTouch?.Invoke(obj, args);
    }
}
  • TouchEvent

画面をタップされたときに発生するイベントハンドラーを定義する。

(実装イメージ)

public class TouchEvent
{
    public delegate void TouchEventHundler(object obj, TouchEventArgs args);
}
  • TouchEventArgs

発生したイベントへ引き渡す引数を定義する
引数にイベントが発生した位置座標および、発生したイベントのタイプ(タップの開始、指が画面から離れた等)を設定できるようにすることで、イベントを捕捉した際に画面でタップされた座標位置および、画面から指が離れた座標位置の取得を行えるようにしています。

(実装イメージ)

public class TouchEventArgs
{
    public enum TouchEventType
    {
        Entered,
        Pressed,
        Moved,
        Released,
        Exited,
        Cancelled
    }

    // 画面で発生したタップイベントのID
    public long Id { get; set; }

    // 画面で発生したイベントの種類(画面のタップ、画面から指が離れた等
    public TouchEventType Type { get; set; }

    // イベントが発生した座標位置
    public Point MyPoint { get; set; }

    public bool IsInContact { private set; get; }

    public TouchEventArgs(long id, TouchEventType type, float x, float y, bool isInContact)
    {
        Id = id;
        Type = type;
        MyPoint = new Point(x, y);
        IsInContact = isInContact;
    }

    public TouchEventArgs(long id, TouchEventType type, Point point, bool isInContact)
    {
        Id = id;
        Type = type;
        MyPoint = point;
        IsInContact = isInContact;
    }
}

※TouchEventArgsクラスはイベントの引数として使用するクラスのため、タッチイベントのenumはTouchEventArgsクラスとは別クラスとしておいたほうが後々の実装は楽になると思います。

Androidプロジェクトの処理

作成したクラス

  • TouchEffect

実際にイベントを処理するクラス。

(実装イメージ)

public class TouchEffect : PlatformEffect
{
    // イベントを捕捉するView
    Android.Views.View view;

    // イベントが発生したコントロール(今回はGridを使用しています)
    Element element;

    DragDrop.View.MyEffect.TouchEffect tEffect;

    static Dictionary<Android.Views.View, TouchEffect> viewDictionary = new Dictionary<Android.Views.View, TouchEffect>();
    static Dictionary<int, TouchEffect> idToEffectDictionary = new Dictionary<int, TouchEffect>();

    /// <summary>
    /// イベントのアタッチ
    /// </summary>
    protected override void OnAttached()
    {
        view = Control == null ? Container : Control;
        element = Element;
        if (view != null)
        {
            view.Touch += OnTouch;
            viewDictionary.Add(view, this);
            tEffect = (DragDrop.View.MyEffect.TouchEffect)element.Effects.FirstOrDefault(e => e is DragDrop.View.MyEffect.TouchEffect);
        }
    }

    /// <summary>
    /// イベントのデタッチ
    /// </summary>
    protected override void OnDetached()
    {
        if (viewDictionary.ContainsKey(view))
        {
            view.Touch -= OnTouch;
            viewDictionary.Remove(view);
        }
    }

    private void OnTouch(object obj, Android.Views.View.TouchEventArgs ev)
    {
        Android.Views.View senderView = obj as Android.Views.View;
        senderView.GetLocationOnScreen(twoIntArray);

        // タップポインターのID
        int actID = ev.Event.ActionIndex;

        // タップした指を識別するID
        int pointID = ev.Event.GetPointerId(actID);

        // 相対座標を取得する場合
        float x = ev.Event.GetX(actID);
        float y = ev.Event.GetY(actID);

        // 絶対座標を取得する場合
        float x_Raw = ev.Event.RawX;
        float y_Raw = ev.Event.RawY;

        Point screenPointerCoords = new Point(x_Raw,
                                              y_Raw);

        DragDrop.View.MyEffect.TouchEventArgs args;

        switch (ev.Event.Action)
        {
            case MotionEventActions.ButtonPress:
            case MotionEventActions.Down:
            case MotionEventActions.PointerDown:

                // 画面をタップした場合の処理
                if(!idToEffectDictionary.ContainsKey(pointID))
                {
                    idToEffectDictionary.Add(pointID, this);
                    FireEvent(this, pointID, View.MyEffect.TouchEventArgs.TouchEventType.Pressed, screenPointerCoords, true);
                }

                break;

            case MotionEventActions.ButtonRelease:
            case MotionEventActions.Up:
            case MotionEventActions.Pointer1Up:

                // 画面から指が離れた場合の処理
                if (idToEffectDictionary[pointID] != null)
                {
                    FireEvent(this, pointID, View.MyEffect.TouchEventArgs.TouchEventType.Released, screenPointerCoords, false);
                    idToEffectDictionary.Remove(pointID);
                }

                break;

            case MotionEventActions.Move:

                // 画面をタップしたまま、指を移動した場合の処理
                // Moveイベントは複数同時に取得される場合があるため、ループで処理する
                for(var i = 0; i < ev.Event.PointerCount;i++)
                {
                    pointID = ev.Event.GetPointerId(actID);

                    if (idToEffectDictionary[pointID] != null)
                    {
                        FireEvent(this, pointID, View.MyEffect.TouchEventArgs.TouchEventType.Moved, screenPointerCoords, true);
                    }

                }

                break;

            case MotionEventActions.Cancel:

                // AndroidではMoveの途中で時々キャンセルされるため、キャンセル時も画面から指が離れた場合の処理を実装
                if (idToEffectDictionary[pointID] != null)
                {
                    FireEvent(this, pointID, View.MyEffect.TouchEventArgs.TouchEventType.Released, screenPointerCoords, false);
                }

                idToEffectDictionary.Remove(pointID);

                break;
        }
    }

    /// <summary>
    /// イベントの実行
    /// </summary>
    private void FireEvent(TouchEffect touchEffect, int id, DragDrop.View.MyEffect.TouchEventArgs.TouchEventType eventType, Point pointerLocation, bool isInContact)
    {
        var args = new DragDrop.View.MyEffect.TouchEventArgs(id, eventType, pointerLocation, isInContact);
        tEffect.OnTouchEvent(element, args);
    }
}
iOSプロジェクトの処理

作成したクラス

  • TouchEffect

実際にイベントを処理するクラス。
iOSでは、Recognizerクラスを生成してイベントの処理を行う。

(実装イメージ)

public class TouchEffect : PlatformEffect
{
    UIView view;
    TouchRecognizer touchRecognizer;

    /// <summary>
    /// イベントのアタッチ
    /// </summary>
    protected override void OnAttached()
    {
        view = Control == null ? Container : Control;
        view.UserInteractionEnabled = true;

        DragDrop.View.MyEffect.TouchEffect effect = (DragDrop.View.MyEffect.TouchEffect)Element.Effects.FirstOrDefault(e => e is DragDrop.View.MyEffect.TouchEffect);

        if (effect != null && view != null)
        {
            touchRecognizer = new TouchRecognizer(Element, view, effect);

            view.AddGestureRecognizer(touchRecognizer);
        }
    }

    /// <summary>
    /// イベントのデタッチ
    /// </summary>
    protected override void OnDetached()
    {
        if (touchRecognizer != null)
        {

            touchRecognizer.Detach();
            view.RemoveGestureRecognizer(touchRecognizer);
        }
    }
}
  • TouchRecognizer

イベント処理を実装したクラス。
このクラスで画面でタップされた場合の座標位置の取得や、画面から指が離れた座標位置の取得などを行っている。

(実装イメージ)

public class TouchRecognizer : UIGestureRecognizer
{
    private Element element;        // Forms element for firing events
    private UIView view;            // iOS UIView 
    private DragDrop.View.MyEffect.TouchEffect touchEffect;

    static Dictionary<UIView, TouchRecognizer> viewDictionary =
        new Dictionary<UIView, TouchRecognizer>();

    static Dictionary<long, TouchRecognizer> idToTouchDictionary =
        new Dictionary<long, TouchRecognizer>();

    public TouchRecognizer(Element element, UIView view, DragDrop.View.MyEffect.TouchEffect touchEffect)
    {
        this.element = element;
        this.view = view;
        this.touchEffect = touchEffect;

        viewDictionary.Add(view, this);
    }

    public void Detach()
    {
        viewDictionary.Remove(view);
    }

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

        foreach (UITouch touch in touches.Cast<UITouch>())
        {
            long id = touch.Handle.ToInt64();
            FireEvent(this, id, DragDrop.View.MyEffect.TouchEventArgs.TouchEventType.Pressed, touch, true);

            if (!idToTouchDictionary.ContainsKey(id))
            {
                idToTouchDictionary.Add(id, this);
            }
        }
    }

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

        foreach (UITouch touch in touches.Cast<UITouch>())
        {
            long id = touch.Handle.ToInt64();

            FireEvent(idToTouchDictionary[id], id, DragDrop.View.MyEffect.TouchEventArgs.TouchEventType.Moved, touch, true);
        }
    }

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

        foreach (UITouch touch in touches.Cast<UITouch>())
        {
            long id = touch.Handle.ToInt64();

            FireEvent(idToTouchDictionary[id], id, DragDrop.View.MyEffect.TouchEventArgs.TouchEventType.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 = touch.Handle.ToInt64();

            if (idToTouchDictionary[id] != null)
            {
                FireEvent(idToTouchDictionary[id], id, DragDrop.View.MyEffect.TouchEventArgs.TouchEventType.Released, touch, false);
            }
            idToTouchDictionary.Remove(id);
        }
    }

    private void FireEvent(TouchRecognizer recognizer, long id, DragDrop.View.MyEffect.TouchEventArgs.TouchEventType 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, DragDrop.View.MyEffect.TouchEventArgs> onTouchAction = recognizer.touchEffect.OnTouchEvent;

        // Call that method
        onTouchAction(recognizer.element,
            new DragDrop.View.MyEffect.TouchEventArgs(id, actionType, xfPoint, isInContact));
    }
}

ドラッグアンドドロップ機能を使用する

ドラッグアンドドロップ機能はxamlおよび、コードビハインドで上記で実装したドラッグアンドドロップのeffectを実装することで実現しています。