Перетаскивание, ListView и элемент Представления, которые пропускают событие ACTION_DRAG_STARTED

На Android я использую ListView и я хочу, чтобы иметь возможность изменять порядок своих элементов, используя перетаскивание. Я знаю, что существует различная реализация «перетаскивания списка», однако я хочу использовать инфраструктуру Drag and Drop, начиная с уровня API 11 .

Это началось очень хорошо, пока я не захотел прокрутить список ListView при перетаскивании. Поскольку это написано в примере ниже, на данный момент я проверяю верхний элемент списка, которым я являюсь, поэтому, если его позиция не находится между ListView.getLastVisiblePosition() и ListView.getFirstVisiblePosition() я использую ListView.smoothScrollToPosition() для Просмотрите другие элементы списка.

Это первая реализация, но она работает очень хорошо.

Проблема возникает при прокрутке: некоторые элементы не отвечают на события перетаскивания – DragEvent.ACTION_DRAG_ENTERED а остальные – когда я на них сверху. Это связано с тем, как ListView управляет своими представлениями элементов: он пытается переработать просмотры элементов, которые больше не видны.

Все в порядке, и это работает, но иногда getView() ListAdapter возвращает новый объект. Поскольку он новый, этот объект пропустил DragEvent.ACTION_DRAG_STARTED поэтому он не отвечает на другие события DragEvent !

Вот пример. В этом случае, если я начну перетаскивание с помощью длинного клика по элементу списка, и если я его перетащил, то большинство элементов будет иметь зеленый фон, если я нахожусь поверх них; Но некоторые нет.

Любая идея о том, чтобы заставить их подписаться на механизм перетаскивания, даже если они пропустили 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; } } } 

    Я не решил эту проблему утилизации, но я нашел возможное обходное решение, все еще использующее структуру Drag & Drop. Идея заключается в изменении перспективы: вместо использования OnDragListener в каждом View в списке он может использоваться непосредственно в ListView .

    Тогда идея состоит в том, чтобы найти поверх этого элемента палец при выполнении Drag & Drop и написать соответствующий отображаемый код в ListAdapter ListView . Трюк заключается в том, чтобы найти поверх того, что мы видим, и где это происходит.

    Чтобы сделать это, я установил в качестве id для каждого представления, созданного адаптером, его позицию ListView – с помощью View.setId() , поэтому я могу найти его позже, используя комбинацию ListView.pointToPosition() и ListView.findViewById() ,

    Как пример прослушивателя перетаскивания (который, я напоминаю вам, применяется к ListView ), это может быть что-то вроде этого:

     // Initalize your ListView private ListView _myListView = new ListView(getContext()); // Start drag when long click on a ListView item _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; } }); // Set the adapter and drag listener _myListView.setOnDragListener(new MyListViewDragListener()); _myListView.setAdapter(new MyViewAdapter(getActivity())); // Classes used above private class MyViewAdapter extends ArrayAdapter<Object> { public MyViewAdapter (Context context, List<TimedElement> objects) { super(context, 0, objects); } @Override public View getView(int position, View convertView, ViewGroup parent) { View myView = convertView; if (myView == null) { // Instanciate your view } // Associates view and position in ListAdapter, needed for drag and drop myView.setId(position); return myView; } } private class MyListViewDragListener 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_DROP: // We drag the item on top of the one which is at itemPosition int itemPosition = _myListView.pointToPosition((int)event.getX(), (int)event.getY()); // We can even get the view at itemPosition thanks to get/setid View itemView = _myListView.findViewById(itemPosition ); /* If you try the same thing in ACTION_DRAG_LOCATION, itemView * is sometimes null; if you need this view, just return if null. * As the same event is then fired later, only process the event * when itemView is not null. * It can be more problematic in ACTION_DRAG_DROP but for now * I never had itemView null in this event. */ // Handle the drop as you like return true; } } }