Quantcast
Channel: Xamarin.Forms — Xamarin Community Forums
Viewing all articles
Browse latest Browse all 77050

Reorder list view items with drag and drop

$
0
0

I am trying to implement items reordering with drag and drop on a list view since I have tried to look for a working solution for Xamarin.Forms, but I failed.
Since I am particularly interested on implementing it for Android platform for now, I found a port that someone made form a working example on Java to Xamarin.Android, but obviously that doesn't work for Xamarin.Forms so I am trying to port that to Forms.

I was able to get an almost working code since I can start the drag&drop, but have some issues:

  • after I reorder the items the indices are completely wrong
  • if I start dragging an item down, if I try to bring it up again, without releasing the touch, all the items get messy because the indices are wrong.
  • if I drag an item to its position and release the touch the item I was moving disappears
  • after I have dragged one item to its position then I can't starting dragging others (I can but it will start from the wrong location because of the wrong indices)

Can someone with more experience on Xamarin help me?

This is the code I adapted from the original Xamarin.Android implementation (from pnavk/Drag-and-Drop-ListView on GitHub, sorry but I can't post links yet).

// Start content of ReorderableListView.cs on PCL

using System;
using Xamarin.Forms;

namespace MyApp.Controls
{
    public class ItemReorderedEventArgs : EventArgs
    {
        public object ItemA { get; protected set; } = null;
        public int NewIndexA { get; protected set; } = -1;

        public object ItemB { get; protected set; } = null;
        public int NewIndexB { get; protected set; } = -1;

        public ItemReorderedEventArgs(object itemA, int newIndexA, object itemB, int newIndexB)
        {
            ItemA = itemA;
            NewIndexA = newIndexA;

            ItemB = itemB;
            NewIndexB = newIndexB;
        }
    }

    public class ReorderableListView : ListView
    {
        public delegate void ItemsReorderedEvent(object s, ItemReorderedEventArgs e);
        public event ItemsReorderedEvent ItemsReordered = null;

        public void SendItemsReordered(object itemA, int newIndexA, object itemB, int newIndexB)
        {
            ItemsReordered?.Invoke(this, new ItemReorderedEventArgs(itemA, newIndexA, itemB, newIndexB));
        }
    }
}

// End content of ReorderableListView.cs on PCL

// --------------------------------------------------------
// --------------------------------------------------------

// Start content of ReorderableListViewRenderer.cs on Droid

using System.Linq;
using MyApp;
using MyApp.Controls;
using MyApp.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

using Android.Widget;
using Android.Animation;
using Android.Views;
using Android.Graphics.Drawables;
using Android.Graphics;
using Android.Content;
using Android.Util;
using System;

[assembly: ExportRenderer(typeof(ReorderableListView), typeof(ReorderableListViewRenderer))]
namespace MyApp.Droid.Renderers
{
    public class ReorderableListViewRenderer : ListViewRenderer, Android.Views.View.IOnTouchListener, ITypeEvaluator
    {
        const int LINE_THICKNESS = 1;
        const int INVALID_ID = -1;
        const int INVALID_POINTER_ID = -1;

        int _lastEventY = -1;
        int _downY = -1;
        int _downX = -1;
        int _totalOffset = 0;
        int _activePointerId = INVALID_POINTER_ID;

        bool _cellIsMobile = false;

        long _aboveItemId = INVALID_ID;
        long _mobileItemId = INVALID_ID;
        long _belowItemId = INVALID_ID;

        Android.Views.View _mobileView;
        Rect _hoverCellCurrentBounds;
        Rect _hoverCellOriginalBounds;

        BitmapDrawable _hoverCell;

        public ReorderableListViewRenderer()
        {
            LongClickable = true;
            SetWillNotDraw(false);
        }

        protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.ListView> e)
        {
            base.OnElementChanged(e);

            if(e.OldElement != null)
            {
                // Unsubscribe
                Control.ItemLongClick -= HandleItemLongClick;
            }

            if(e.NewElement != null)
            {
                // Subscribe
                Control.ItemLongClick += HandleItemLongClick;
                Control.SetOnTouchListener(this);
            }
        }

        IListAdapter GetAtapter()
        {
            HeaderViewListAdapter hlvAdapter = Control.Adapter as HeaderViewListAdapter;
            return hlvAdapter.WrappedAdapter;
        }

        void HandleItemLongClick(object sender, AdapterView.ItemLongClickEventArgs e)
        {
            _totalOffset = 0;

            int position = Control.PointToPosition(_downX, _downY);

            if (position < 0 || !LongClickable)
                return;

            int itemNum = position - Control.FirstVisiblePosition;

            Android.Views.View selectedView = Control.GetChildAt(itemNum);
            _mobileItemId = GetAtapter().GetItemId(position);
            _hoverCell = GetAndAddHoverView(selectedView);
            selectedView.Visibility = ViewStates.Invisible;

            _cellIsMobile = true;

            UpdateNeighborViewsForID(_mobileItemId);
        }

        void HandleHoverAnimatorUpdate(object sender, ValueAnimator.AnimatorUpdateEventArgs e)
        {
            Invalidate();
        }

        void HandleHoverAnimationStart(object sender, EventArgs e)
        {
            Enabled = false;
        }

        void HandleHoverAnimationEnd(object sender, EventArgs e)
        {
            _aboveItemId = INVALID_ID;
            _mobileItemId = INVALID_ID;
            _belowItemId = INVALID_ID;

            _hoverCell = null;
            Enabled = true;

            Invalidate();

            // Send message to list view owner with changed indices

            _mobileView.Visibility = ViewStates.Visible;
        }

        BitmapDrawable GetAndAddHoverView(Android.Views.View view)
        {
            int w = view.Width;
            int h = view.Height;
            int top = view.Top;
            int left = view.Left;

            Bitmap b = GetBitmapWithBorder(view);

            BitmapDrawable drawable = new BitmapDrawable(Resources, b);

            _hoverCellOriginalBounds = new Rect(left, top, left + w, top + h);
            _hoverCellCurrentBounds = new Rect(_hoverCellOriginalBounds);

            drawable.SetBounds(left, top, left + w, top + h);

            return drawable;
        }

        static Bitmap GetBitmapWithBorder(Android.Views.View view)
        {
            Bitmap bitmap = GetBitmapFromView(view);
            Canvas can = new Canvas(bitmap);

            can.DrawBitmap(bitmap, 0, 0, null);

            return bitmap;
        }

        static Bitmap GetBitmapFromView(Android.Views.View view)
        {
            try
            {
                Bitmap bitmap = Bitmap.CreateBitmap(view.Width, view.Height, Bitmap.Config.Argb8888);
                Canvas canvas = new Canvas(bitmap);

                view.Draw(canvas);

                return bitmap;
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            return default(Bitmap);
        }

        void UpdateNeighborViewsForID(long itemID)
        {
            int position = GetPositionForID(itemID);
            _aboveItemId = GetAtapter().GetItemId(position - 1);
            _belowItemId = GetAtapter().GetItemId(position + 1);
        }

        public Android.Views.View GetViewForID(long itemID)
        {
            for (int i = 1; i < Control.ChildCount - 1; i++)
            {
                Android.Views.View view = Control.GetChildAt(i);

                int position = Control.FirstVisiblePosition + i;
                long id = GetAtapter().GetItemId(position);

                if (id == itemID)
                    return view;
            }

            return null;
        }

        public int GetPositionForID(long itemID)
        {
            Android.Views.View view = GetViewForID(itemID);
            if (view == null)
                return -1;
            return Control.GetPositionForView(view);
        }

        protected override void OnDraw(Canvas canvas)
        {
            base.OnDraw(canvas);

            if (_hoverCell != null)
                _hoverCell.Draw(canvas);
        }

        public bool OnTouch(Android.Views.View view, MotionEvent e)
        {
            try
            {
                switch(e.Action)
                {
                    case MotionEventActions.Down:
                        _downX = (int)e.GetX();
                        _downY = (int)e.GetY();
                        _activePointerId = e.GetPointerId(0);
                        break;
                    case MotionEventActions.Move:
                        if (_activePointerId == INVALID_POINTER_ID)
                            break;

                        int pointerIndex = e.FindPointerIndex(_activePointerId);
                        _lastEventY = (int)e.GetY(pointerIndex);

                        int deltaY = _lastEventY - _downY;

                        if(_cellIsMobile)
                        {
                            Enabled = false;
                            _hoverCellCurrentBounds.OffsetTo(_hoverCellOriginalBounds.Left, _hoverCellOriginalBounds.Top + deltaY + _totalOffset);
                            _hoverCell.SetBounds(_hoverCellCurrentBounds.Left, _hoverCellCurrentBounds.Top, _hoverCellCurrentBounds.Right, _hoverCellCurrentBounds.Bottom);
                            Invalidate();
                            HandleCellSwitch();
                        }

                        break;
                    case MotionEventActions.Up:
                        TouchEventsEnded();
                        break;
                    case MotionEventActions.Cancel:
                        TouchEventsCancelled();
                        break;
                    default:
                        break;
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine($"Error processing OnTouchEvent in ReorderableListView - Message: {ex.Message}");
                Console.WriteLine($"Error processing OnTouchEvent in ReorderableListView - StackTrace: {ex.StackTrace}");
            }

            return false;
        }

        void HandleCellSwitch()
        {
            try
            {
                int deltaY = _lastEventY - _downY;
                int deltaYTotal = _hoverCellOriginalBounds.Top + _totalOffset + deltaY;

                Android.Views.View belowView = GetViewForID(_belowItemId);
                Android.Views.View mobileView = GetViewForID(_mobileItemId);
                Android.Views.View aboveView = GetViewForID(_aboveItemId);

                bool isBelow = (belowView != null) && (deltaYTotal > belowView.Top);
                bool isAbove = (aboveView != null) && (deltaYTotal < aboveView.Top);

                Console.WriteLine($"Below id: {_belowItemId} - Above id: {_aboveItemId} - Below view null: {belowView == null} - Above view null {aboveView == null} - Is below: {isBelow} - Is above: {isAbove}");

                if(mobileView != null && (isBelow || isAbove))
                {
                    Android.Views.View switchView = isBelow ? belowView : aboveView;
                    var itemId = isBelow ? _belowItemId : _aboveItemId;

                    var diff = mobileView.Top - switchView.Top;

                    Console.WriteLine($"Item id: {itemId} - Item translationY: {switchView.TranslationY} - Item position Y: {switchView.Top}");

                    ObjectAnimator anim = ObjectAnimator.OfFloat(switchView, "TranslationY", switchView.TranslationY, switchView.TranslationY + diff);
                    anim.SetDuration(100);
                    anim.Start();

                    _mobileItemId = Control.GetPositionForView(switchView);

                    UpdateNeighborViewsForID(_mobileItemId);

                    anim.AnimationEnd += (sender, e) =>
                    {
                        Console.WriteLine($"Item id: {itemId} - Item translationY: {switchView.TranslationY} - Item position Y: {switchView.Top}");
                    };
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine($"Error switching cells in ReorderableListView - Message: {ex.Message}");
                Console.WriteLine($"Error switching cells in ReorderableListView - StackTrace: {ex.StackTrace}");
            }
        }

        void TouchEventsEnded()
        {
            _mobileView = GetViewForID(_mobileItemId);

            if(_cellIsMobile)
            {
                _cellIsMobile = false;
                _activePointerId = INVALID_POINTER_ID;

                _hoverCellCurrentBounds.OffsetTo(_hoverCellOriginalBounds.Left, _mobileView.Top);

                ObjectAnimator hoverViewAnimator = ObjectAnimator.OfObject(_hoverCell, "Bounds", this, _hoverCellCurrentBounds);
                hoverViewAnimator.Update += HandleHoverAnimatorUpdate;
                hoverViewAnimator.AnimationStart += HandleHoverAnimationStart;
                hoverViewAnimator.AnimationEnd += HandleHoverAnimationEnd;
                hoverViewAnimator.Start();
            }
            else
            {
                TouchEventsCancelled();
            }
        }

        public Java.Lang.Object Evaluate(float fraction, Java.Lang.Object startValue, Java.Lang.Object endValue)
        {
            var startValueRect = startValue as Rect;
            var endValueRect = endValue as Rect;

            return new Rect(
                Interpolate(startValueRect.Left, endValueRect.Left, fraction),
                Interpolate(startValueRect.Top, endValueRect.Top, fraction),
                Interpolate(startValueRect.Right, endValueRect.Right, fraction),
                Interpolate(startValueRect.Bottom, endValueRect.Bottom, fraction)
            );
        }

        public int Interpolate(int start, int end, float fraction)
        {
            return (int)(start + fraction * (end - start));
        }

        void TouchEventsCancelled()
        {
            _mobileView = GetViewForID(_mobileItemId);

            if(_cellIsMobile)
            {
                _aboveItemId = INVALID_ID;
                _mobileItemId = INVALID_ID;
                _belowItemId = INVALID_ID;
                _hoverCell = null;

                Invalidate();
            }

            if (_mobileView != null)
                _mobileView.Visibility = ViewStates.Visible;

            Enabled = true;
            _cellIsMobile = false;
            _activePointerId = INVALID_POINTER_ID;
        }
    }
}

// End content of ReorderableListViewRenderer.cs on Droid

Viewing all articles
Browse latest Browse all 77050

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>