On Android, I use a ListView and I want to be able to reorder its items using drag and drop. I know there are different implementation of a “drag and drop listview”, however I want to use the Drag and Drop framework coming since API level 11.
It started very well until I wanted to scroll my ListView while doing a drag and drop. As it is written in the example below, for now, I check on top of which list element I am, so if its position is not between ListView.getLastVisiblePosition() and ListView.getFirstVisiblePosition() I use a ListView.smoothScrollToPosition() to view the other list items.
It is a first implementation but it works quite well.
The problem arises while scrolling: some elements do not answer to the drag and drop events – DragEvent.ACTION_DRAG_ENTERED and the others – when I am on top of them. It is due to the way the ListView manages its item views: it tries to recycle the item views that are not visible any more.
It is all right and it works, but sometimes the getView() of the ListAdapter returns a new object. Since it is new, this object missed the DragEvent.ACTION_DRAG_STARTED so it does not answer to the other DragEvent events!
Here is an example. In this case, if I start a drag and drop with a long click on a list item and if I drag it, the majority of items will have a green background if I am on top of them ; but some don’t.
Any idea about making them subscribe to the Drag and drop event mechanism even if they missed DragEvent.ACTION_DRAG_STARTED?
// Somewhere I have a ListView that use the MyViewAdapter
// MyListView _myListView = ...
// _myListView.setAdapter(new MyViewAdapter(getActivity(), ...));
_myListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view);
view.startDrag(null, shadowBuilder, _myListView.getItemAtPosition(position), 0);
return true;
}
});
class MyViewAdapter extends ArrayAdapter<MyElement> {
public MyViewAdapter(Context context, List<TimedElement> objects) {
super(context, 0, objects);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View myElementView = convertView;
if (myElementView == null) {
/* If the code is executed here while scrolling with a drag and drop,
* the new view is not associated to the current drag and drop events */
Log.d("app", "Object created!");
// Create view
// myElementView = ...
// Prepare drag and drop
myElementView.setOnDragListener(new MyElementDragListener());
}
// Associates view and position in ListAdapter, needed for drag and drop
myElementView.setTag(R.id.item_position, position);
// Continue to prepare view
// ...
return timedElementView;
}
private class MyElementDragListener implements View.OnDragListener {
@Override
public boolean onDrag(View v, DragEvent event) {
final int action = event.getAction();
switch(action) {
case DragEvent.ACTION_DRAG_STARTED:
return true;
case DragEvent.ACTION_DRAG_ENTERED:
v.setBackgroundColor(Color.GREEN);
v.invalidate();
return true;
case DragEvent.ACTION_DRAG_LOCATION:
int targetPosition = (Integer)v.getTag(R.id.item_position);
if (event.getY() < v.getHeight()/2 ) {
Log.i("app", "top "+targetPosition);
}
else {
Log.i("app", "bottom "+targetPosition);
}
// To scroll in ListView while doing drag and drop
if (targetPosition > _myListView.getLastVisiblePosition()-2) {
_myListView.smoothScrollToPosition(targetPosition+2);
}
else if (targetPosition < _myListView.getFirstVisiblePosition()+2) {
_myListView.smoothScrollToPosition(targetPosition-2);
}
return true;
case DragEvent.ACTION_DRAG_EXITED:
v.setBackgroundColor(Color.BLUE);
v.invalidate();
return true;
case DragEvent.ACTION_DROP:
case DragEvent.ACTION_DRAG_ENDED:
default:
break;
}
return false;
}
}
}
I did not solved this recycling problem, but I found a possible workaround still using the Drag & Drop framework. The idea is to change of perspective: instead of using a
OnDragListeneron eachViewin the list, it can be used on theListViewdirectly.Then the idea is to find on top of which item the finger is while doing the Drag & Drop, and to write the related display code in the
ListAdapterof theListView. The trick is then to find on top of which item view we are, and where the drop is done.In order to do that, I set as an
idto each view created by the adapter itsListViewposition – withView.setId(), so I can find it later using a combination ofListView.pointToPosition()andListView.findViewById().As a drag listener example (which is, I remind you, applied on the
ListView), it can be something like that: