【问题标题】:How to make list elements get DragEvents after scrolling如何让列表元素在滚动后获取 DragEvents
【发布时间】:2015-07-01 18:29:15
【问题描述】:

短版:

  • 有没有办法让新创建的视图接收DragEvents 已经运行的拖放操作?

How to register a DragEvent while already inside one and have it listen in the current DragEvent?,但我真的想要一个更清洁的解决方案。

建议的 GONE->VISIBLE 解决方法要“正确”是相当复杂的,因为您需要确保仅在列表项变得可见时才使用它,而不是无条件地在所有当前列表视图项上使用。在这种情况下,hack 有点漏洞,没有更多的解决方法代码来让它正确。

加长版:

我有一个ListViewListView 的元素是包含可拖动符号(小框)的自定义视图,例如类似这样:

可以在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。它并不完美,但已经足够了。

但是,有一个问题:

当列表滚动到以前不可见的元素时,该元素不会收到DragEvents。将符号拖到其上时,它不会被选中(突出显示),也不接受放置。

关于如何使“滚动”视图从已经活动的拖放操作中接收DragEvents 的任何想法?

【问题讨论】:

    标签: android android-listview scroll drag-and-drop


    【解决方案1】:

    所以根本问题是ViewGroup(即ListView 扩展)缓存了一个子列表以通知DragEvent。此外,它仅在接收 ACTION_DRAG_STARTED 时填充此缓存。更多详情请阅读源代码here

    解决方案!我们不会在 ListView 的各个行上监听 drop 事件,而是在 ListView 本身上监听它们。然后,根据事件的坐标,我们将确定被拖动的视图正在从/到哪一行或悬停在哪一行。当 drop 发生时,我们将执行从上一行删除并添加到新行的事务。

    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));
    }
    

    很多 cmets 所以希望代码是自记录的。几点说明:

    • RandomIconAdapter 只是一个扩展 BaseAdapter 并由 sIconsForListItems 支持的基本适配器。
    • ListViewScrollingDragListener 和提示中的一样。
    • 在 GS6 5.0.2 上测试

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-11-19
      • 1970-01-01
      • 2010-10-30
      • 2013-06-19
      • 2022-11-15
      相关资源
      最近更新 更多