如何使列表元素在滚动后获得 DragEvents
How to make list elements get DragEvents after scrolling
短版:
- 有没有办法让新创建的视图接收
DragEvent
个已经 运行 的拖放操作?
有 How to register a DragEvent while already inside one and have it listen in the current DragEvent?,但我真的想要一个更简洁的解决方案。
获得建议的 GONE->VISIBLE 解决方法非常复杂 "right",因为您需要确保仅在列表项变得可见时才使用它,而不是无条件地在所有当前列表视图项上使用。在此 hack 略有漏洞,没有更多的解决方法代码来使其正确。
长版:
我有一个ListView
。 ListView
的元素是包含可拖动符号(小框)的自定义视图,例如与此类似:
可以在 ListView
的项目之间拖动小方框,就像将元素分类到方框中一样。列表项上的拖动处理程序或多或少是微不足道的:
@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);
}
将指针悬停在符号上并开始拖动(即将其移动到一个小阈值之外)时,拖动开始。
然而,现在屏幕尺寸可能不足以包含所有框,因此 ListView
需要滚动。我发现我需要自己实现滚动的困难方法,因为 ListView
不会在拖动时自动滚动。
进来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;
}
}
当您在其可见区域的上边界或下边界附近拖动时,这基本上会滚动 ListView
。虽然不完美,但已经足够好了。
但是,有一个问题:
当列表滚动到之前不可见的元素时,该元素不会收到 DragEvent
。在其上拖动符号时它不会被选中(突出显示),也不接受掉落。
关于如何使 "scrolled in" 视图从已经激活的拖放操作接收 DragEvent
有什么想法吗?
所以根本问题是 ViewGroup
(ListView
扩展)缓存了一个子列表以通知 DragEvent
。此外,它仅在接收到 ACTION_DRAG_STARTED 时填充此缓存。有关详细信息,请阅读源代码 here.
关于解决方案!我们不是在 ListView
的各个行上监听放置事件,而是在 ListView
本身上监听它们。然后,根据事件的坐标,我们将找出被拖动的视图被拖动 from/to 或悬停在哪一行。当发生下降时,我们将执行从上一行中删除并添加到新行的事务。
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));
}
很多评论,希望代码是自文档化的。一些注意事项:
- RandomIconAdapter 只是一个扩展 BaseAdapter 并由 sIconsForListItems 支持的基本适配器。
- ListViewScrollingDragListener 与提示中的相同
- 在 GS6 5.0.2 上测试
短版:
- 有没有办法让新创建的视图接收
DragEvent
个已经 运行 的拖放操作?
有 How to register a DragEvent while already inside one and have it listen in the current DragEvent?,但我真的想要一个更简洁的解决方案。
获得建议的 GONE->VISIBLE 解决方法非常复杂 "right",因为您需要确保仅在列表项变得可见时才使用它,而不是无条件地在所有当前列表视图项上使用。在此 hack 略有漏洞,没有更多的解决方法代码来使其正确。
长版:
我有一个ListView
。 ListView
的元素是包含可拖动符号(小框)的自定义视图,例如与此类似:
可以在 ListView
的项目之间拖动小方框,就像将元素分类到方框中一样。列表项上的拖动处理程序或多或少是微不足道的:
@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);
}
将指针悬停在符号上并开始拖动(即将其移动到一个小阈值之外)时,拖动开始。
然而,现在屏幕尺寸可能不足以包含所有框,因此 ListView
需要滚动。我发现我需要自己实现滚动的困难方法,因为 ListView
不会在拖动时自动滚动。
进来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;
}
}
当您在其可见区域的上边界或下边界附近拖动时,这基本上会滚动 ListView
。虽然不完美,但已经足够好了。
但是,有一个问题:
当列表滚动到之前不可见的元素时,该元素不会收到 DragEvent
。在其上拖动符号时它不会被选中(突出显示),也不接受掉落。
关于如何使 "scrolled in" 视图从已经激活的拖放操作接收 DragEvent
有什么想法吗?
所以根本问题是 ViewGroup
(ListView
扩展)缓存了一个子列表以通知 DragEvent
。此外,它仅在接收到 ACTION_DRAG_STARTED 时填充此缓存。有关详细信息,请阅读源代码 here.
关于解决方案!我们不是在 ListView
的各个行上监听放置事件,而是在 ListView
本身上监听它们。然后,根据事件的坐标,我们将找出被拖动的视图被拖动 from/to 或悬停在哪一行。当发生下降时,我们将执行从上一行中删除并添加到新行的事务。
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));
}
很多评论,希望代码是自文档化的。一些注意事项:
- RandomIconAdapter 只是一个扩展 BaseAdapter 并由 sIconsForListItems 支持的基本适配器。
- ListViewScrollingDragListener 与提示中的相同
- 在 GS6 5.0.2 上测试