Short version:
DragEvent
s of an already running drag-and-drop operation?There's How to register a DragEvent while already inside one and have it listen in the current DragEvent?, but I'd really like a cleaner solution.
The suggested GONE->VISIBLE workaround is quite complex to get "right", because you need to make sure to only use it when a list item becomes visible and not unconditionally on all current list view items. In this the hack is slightly leaky without even more workaround code to get it right.
Long version:
I have a ListView
. The elements of the ListView
are custom View`s that contain dragable symbols (small boxes), e.g. similar to this:
It is possible to drag the small boxes between the items of the ListView
, like sorting elements into boxes. The drag handler on the list items is more or less trivial:
@Override
public boolean onDragEvent(DragEvent event)
{
if ((event.getLocalState() instanceof DragableSymbolView)) {
final DragableSymbolView draggedView = (DragableSymbolView) event.getLocalState();
if (draggedView.getTag() instanceof SymbolData) {
final SymbolData symbol = (SymbolData) draggedView.getTag();
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
return true;
case DragEvent.ACTION_DRAG_ENTERED:
setSelected(true);
return true;
case DragEvent.ACTION_DRAG_ENDED:
case DragEvent.ACTION_DRAG_EXITED:
setSelected(false);
return true;
case DragEvent.ACTION_DROP:
setSelected(false);
// [...] remove symbol from soruce box and add to current box
requestFocus();
break;
}
}
}
return super.onDragEvent(event);
}
Dragging starts when holding the pointer over a symbol and starting to drag (i.e. moving it beyond a small threshold).
Now, however, the screen size may not be enough to contain all boxes and thus the ListView
needs to scroll. I found out the hard way that I need to do implement the scrolling on my own since ListView
does not automatically scroll while dragging.
In comes ListViewScrollingDragListener
:
public class ListViewScrollingDragListener
implements View.OnDragListener {
private final ListView _listView;
public static final int DEFAULT_SCROLL_BUFFER_DIP = 96;
public static final int DEFAULT_SCROLL_DELTA_UP_DIP = 48;
public static final int DEFAULT_SCROLL_DELTA_DOWN_DIP = 48;
private int _scrollDeltaUp;
private int _scrollDeltaDown;
private boolean _doScroll = false;
private boolean _scrollActive = false;
private int _scrollDelta = 0;
private int _scrollDelay = 250;
private int _scrollInterval = 100;
private int _scrollBuffer;
private final Rect _visibleRect = new Rect();
private final Runnable _scrollHandler = new Runnable() {
@Override
public void run()
{
if (_doScroll && (_scrollDelta != 0) && _listView.canScrollVertically(_scrollDelta)) {
_scrollActive = true;
_listView.smoothScrollBy(_scrollDelta, _scrollInterval);
_listView.postDelayed(this, _scrollInterval);
} else {
_scrollActive = false;
}
}
};
public ListViewScrollingDragListener(final ListView listView, final boolean attach)
{
_scrollBuffer = UnitUtil.dipToPixels(listView, DEFAULT_SCROLL_BUFFER_DIP);
_scrollDeltaUp = -UnitUtil.dipToPixels(listView, DEFAULT_SCROLL_DELTA_UP_DIP);
_scrollDeltaDown = UnitUtil.dipToPixels(listView, DEFAULT_SCROLL_DELTA_DOWN_DIP);
_listView = listView;
if (attach) {
_listView.setOnDragListener(this);
}
}
public ListViewScrollingDragListener(final ListView listView)
{
this(listView, true);
}
protected void handleDragLocation(final float x, final float y)
{
_listView.getGlobalVisibleRect(_visibleRect);
if (_visibleRect.contains((int) x, (int) y)) {
if (y < _visibleRect.top + _scrollBuffer) {
_scrollDelta = _scrollDeltaUp;
_doScroll = true;
} else if (y > _visibleRect.bottom - _scrollBuffer) {
_scrollDelta = _scrollDeltaDown;
_doScroll = true;
} else {
_doScroll = false;
_scrollDelta = 0;
}
if ((_doScroll) && (!_scrollActive)) {
_scrollActive = true;
_listView.postDelayed(_scrollHandler, _scrollDelay);
}
}
}
public ListView getListView()
{
return _listView;
}
@Override
public boolean onDrag(View v, DragEvent event)
{
/* hide sequence controls during drag */
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_ENTERED:
_doScroll = true;
break;
case DragEvent.ACTION_DRAG_EXITED:
case DragEvent.ACTION_DRAG_ENDED:
case DragEvent.ACTION_DROP:
_doScroll = false;
break;
case DragEvent.ACTION_DRAG_LOCATION:
handleDragLocation(event.getX(), event.getY());
break;
}
return true;
}
}
This basically scrolls the ListView
when you you drag near the upper or lower borders of its visible area. It's not perfect, but it's good enough.
However, there's a catch:
When the list scrolls to a previously invisible element, that element does not receive DragEvent
s. It does not get selected (highlighted) when dragging a symbol over it nor does it accept drops.
Any ideas on how to make the "scrolled in" views receive DragEvent
s from the already active drag-and-drop operation?
So the fundamental problem is that ViewGroup
(that ListView
extends) caches a List of children to notify of DragEvent
. Moreover, it only populates this cache when receiving ACTION_DRAG_STARTED. For more details peruse the source code here.
On to the solution! Instead of listening for drop events on the individual rows of ListView
, we're going to listen to them on ListView
itself. Then, based on the coordinates of the events, we'll figure out which row the dragged view is being dragged from/to or hovered over. When the drop occurs, we'll perform the transaction of removing from the previous row and adding to the new row.
private void init(Context context) {
setAdapter(new RandomIconAdapter()); // Adapter that contains our data set
setOnDragListener(new ListDragListener());
mListViewScrollingDragListener = new ListViewScrollingDragListener(this, false);
}
ListViewScrollingDragListener mListViewScrollingDragListener;
private class ListDragListener implements OnDragListener {
// The view that our dragged view would be dropped on
private View mCurrentDropZoneView = null;
private int mDropStartRowIndex = -1;
@Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_LOCATION:
// Update the active drop zone based on the position of the event
updateCurrentDropZoneView(event);
// Funnel drag events to separate listener to handle scrolling near edges
mListViewScrollingDragListener.onDrag(v, event);
if( mDropStartRowIndex == -1 ) // Only initialize once per drag->drop gesture
{
mDropStartRowIndex = indexOfChild(mCurrentDropZoneView) + getFirstVisiblePosition();
log("mDropStartRowIndex %d", mDropStartRowIndex);
}
break;
case DragEvent.ACTION_DRAG_ENDED:
case DragEvent.ACTION_DRAG_EXITED:
mCurrentDropZoneView = null;
mDropStartRowIndex = -1;
break;
case DragEvent.ACTION_DROP:
// Update our data set based on the row that the dragged view was dropped in
int finalDropRow = indexOfChild(mCurrentDropZoneView) + getFirstVisiblePosition();
updateDataSetWithDrop(mDropStartRowIndex, finalDropRow);
// Let adapter update ui
((BaseAdapter)getAdapter()).notifyDataSetChanged();
break;
}
// The ListView handles ALL drag events all the time. Fine for now since we don't need to
// drag -> drop outside of the ListView.
return true;
}
private void updateDataSetWithDrop(int fromRow, int toRow) {
log("updateDataSetWithDrop fromRow %d and toRow %d", fromRow, toRow);
sIconsForListItems[fromRow]--;
sIconsForListItems[toRow]++;
}
// NOTE: The DragEvent in local to DragDropListView, as are children coordinates
private void updateCurrentDropZoneView(DragEvent event) {
View previousDropZoneView = mCurrentDropZoneView;
mCurrentDropZoneView = findFrontmostDroppableChildAt(event.getX(), event.getY());
log("mCurrentDropZoneView updated to %d for x/y : %f/%f with action %d",
mCurrentDropZoneView == null ? -1 : indexOfChild(mCurrentDropZoneView) + getFirstVisiblePosition(),
event.getX(), event.getY(), event.getAction());
if (mCurrentDropZoneView != previousDropZoneView) {
if (previousDropZoneView != null) previousDropZoneView.setSelected(false);
if (mCurrentDropZoneView != null) mCurrentDropZoneView.setSelected(true);
}
}
}
/**
* The next four methods are utility methods taken from Android Source Code. Most are package-private on View
* or ViewGroup so I'm forced to replicate them here. Original source can be found:
* http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.0_r1/android/view/ViewGroup.java#ViewGroup.findFrontmostDroppableChildAt%28float%2Cfloat%2Candroid.graphics.PointF%29
*/
private View findFrontmostDroppableChildAt(float x, float y) {
int childCount = this.getChildCount();
for(int i=0; i<childCount; i++)
{
View child = getChildAt(i);
if (isTransformedTouchPointInView(x, y, child)) {
return child;
}
}
return null;
}
static public boolean isTransformedTouchPointInView(float x, float y, View child) {
PointF point = new PointF(x, y);
transformPointToViewLocal(point, child);
return pointInView(child, point.x, point.y);
}
static public void transformPointToViewLocal(PointF pointToModify, View child) {
pointToModify.x -= child.getLeft();
pointToModify.y -= child.getTop();
}
static public boolean pointInView(View v, float localX, float localY) {
return localX >= 0 && localX < (v.getRight() - v.getLeft())
&& localY >= 0 && localY < (v.getBottom() - v.getTop());
}
static final int[] sIconsForListItems;
static final int NUM_LIST_ITEMS = 50;
static final int MAX_NUM_ICON_PER_ELEMENT = 8;
static {
sIconsForListItems = new int[NUM_LIST_ITEMS];
for (int i=0; i < NUM_LIST_ITEMS; i++)
{
sIconsForListItems[i] = (getRand(MAX_NUM_ICON_PER_ELEMENT));
}
}
private static final String TAG = DragDropListView.class.getSimpleName();
private static void log(String format, Object... args) {
Log.d(TAG, String.format(format, args));
}
Lots of comments so hopefully the code is self-documenting. A few notes:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With