快速拖动后 OnD​​ragListener 未收到回调

OnDragListener not receiving callbacks after dragged fastly

首先,这个问题可能看起来像 this question, and looks like recorded as issue here 的重复,但在查看细节后,您会发现它是完全不同的。

设置

我正在尝试实现一个简单的拖放功能:当我将 ImageView A 拖到 ImageView B 时,它们将交换它们的背景颜色。

您可以下载这个小测试项目here。下面描述的问题可以通过克隆它并在您的 phone.

上构建来重现

如果你只想看代码,请看MainActivity

问题

如果拖放速度较慢(例如每次拖放 0.5 秒),效果很好。

但是,如果你尝试以你最快的速度拖放(比如一秒4到5次,反正你最快的速度),很快你会发现拖放动作不能再进行了。通过查看日志,我发现 none 的拖动回调被触发 .

03-12 10:41:53.782 (...) I/dragEvent: view.startDragAndDrop has been called
03-12 10:41:53.787 (...) I/dragEvent: ACTION_DRAG_STARTED received
03-12 10:41:53.787 (...) I/dragEvent: ACTION_DRAG_STARTED received
03-12 10:41:53.789 (...) I/dragEvent: ACTION_DRAG_ENTERED received
03-12 10:41:53.789 (...) I/dragEvent: ACTION_DRAG_LOCATION received
03-12 10:41:53.796 (...) I/dragEvent: ACTION_DRAG_LOCATION received
03-12 10:41:53.802 (...) I/dragEvent: ACTION_DROP received
03-12 10:41:53.815 (...) I/dragEvent: ACTION_DRAG_ENDED received
03-12 10:41:53.815 (...) I/dragEvent: ACTION_DRAG_ENDED received
03-12 10:41:54.014 (...) I/dragEvent: view.startDragAndDrop has been called //Since this call, no callbacks were received

此时,应用程序不会冻结。 OnTouchListener 仍在接收回调。
拖放冻结是因为我有一个标志来指示拖放是否正在进行,并且由于未触发 ACTION_DRAG_ENDED,因此该标志停留在 true 处,因此不再有拖放操作可以执行。

我真的不知道为什么,因为我已经记录了所有内容,每一行代码都是 运行 正确的,我希望收到来自 Android 框架的回调,但它不是......

为什么我认为它应该是可以解决的

实际上,这个小测试项目是我试图克隆 this 应用程序中实现的拖放图像交换。而且我无法在他们的应用程序中重现此问题...所以我认为我的代码中一定有问题;或其他解决方法。

查看下面的代码片段

private void setupTouchListeners () {
    View.OnTouchListener listener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                return true;
            }
            if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
                boolean beyondLeft = event.getX() <= 0;
                boolean beyondRight = event.getX() >= v.getWidth();
                boolean beyondTop = event.getY() <= 0;
                boolean beyondBottom = event.getY() >= v.getHeight();
                if (beyondLeft || beyondRight || beyondTop || beyondBottom) {
                    startDragAndDrop(v);
                    return true;
                } else {
                    // done changes here, simply changed flag to false and return false
                    isDragging=false;
                    return false;
                }
            }
            return false;
        }
    };
    ivGrid1.setOnTouchListener(listener);
    ivGrid2.setOnTouchListener(listener);
    ivGrid3.setOnTouchListener(listener);
    ivGrid4.setOnTouchListener(listener);
}

它有颜色的逻辑问题,这不是这个问题的一部分,通过进行上述更改,您将能够以您想要的任何速度拖动。

编码愉快:)

您的快速拖动导致了您未处理的快速移动,并且拖放工具没有通知您它没有通过正式结束拖动来继续进行。以下更新 MainActivity.java 有以下变化:

  1. 许多处理与拖动无关的代码已被删除。我这样做只是为了简化此处的发布,没有其他原因。
  2. 引入 GestureDetectorCompat 以检测何时发生了投掷行为。我对此没有做任何事情,只是记录它发生了。您可能需要考虑在代码中使用手势检测工具进行触摸。
  3. 检查是否存在 ACTION_UP 事件以在拖动未完成时重置布局 (isDragging == true)。

开始拖动后,flings 似乎有一个混乱的结局。下面的代码可以帮助您解决问题。

MainActivity.java

public class MainActivity extends AppCompatActivity {

    ImageView ivGrid1, ivGrid2, ivGrid3, ivGrid4;
    private GestureDetectorCompat mDetector;

    //Drag and Drop
    View dragOriginView;
    Drawable whiteDrawable;
    Drawable tempDrawableStorage;
    boolean isDragging = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mDetector = new GestureDetectorCompat(this, new MyGestureListener());

        ivGrid1 = findViewById(R.id.grid_1);
        ivGrid2 = findViewById(R.id.grid_2);
        ivGrid3 = findViewById(R.id.grid_3);
        ivGrid4 = findViewById(R.id.grid_4);

        setupTouchListeners();
        setupDragListeners();

        whiteDrawable = new ColorDrawable(ContextCompat.getColor(this, android.R.color.white));
    }

    private void setupTouchListeners() {
        View.OnTouchListener listener = new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getActionMasked();

                switch (action) {
                    case MotionEvent.ACTION_DOWN:
                        return true;

                    case MotionEvent.ACTION_MOVE:
                        boolean beyondLeft = event.getX() <= 0;
                        boolean beyondRight = event.getX() >= v.getWidth();
                        boolean beyondTop = event.getY() <= 0;
                        boolean beyondBottom = event.getY() >= v.getHeight();
                        if (beyondLeft || beyondRight || beyondTop || beyondBottom) {
                            startDragAndDrop(v);
                            return true;
                        }
                        isDragging = false;
                        break;

                    case MotionEvent.ACTION_UP:
                        if (isDragging) {
                            dragOriginView.setBackground(tempDrawableStorage);
                            tempDrawableStorage = null;
                            v.setAlpha(1.0f);
                            isDragging = false;
                        }
                }
                return false;
            }
        };
        ivGrid1.setOnTouchListener(listener);
        ivGrid2.setOnTouchListener(listener);
        ivGrid3.setOnTouchListener(listener);
        ivGrid4.setOnTouchListener(listener);
    }

    private void startDragAndDrop(View view) {
        if (isDragging) return;
        isDragging = true;
        dragOriginView = view;
        view.setAlpha(0.5f);
        ClipData.Item item = new ClipData.Item((String) view.getTag());
        ClipData dragData = new ClipData((String) view.getTag(), new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN},
                                         item);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            view.startDragAndDrop(dragData, new View.DragShadowBuilder(view), null, 0);
        } else {
            view.startDrag(dragData, new View.DragShadowBuilder(view), null, 0);
        }
        Log.i("dragEvent", "view.startDragAndDrop has been called");
        tempDrawableStorage = view.getBackground();
        view.setBackground(whiteDrawable);
    }

    private void setupDragListeners() {
        View.OnDragListener listener = new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                Log.i("dragEvent", "<<<<<<<<<<<<<<called");
                switch (event.getAction()) {
                    case ACTION_DRAG_STARTED:
                        Log.i("dragEvent", "ACTION_DRAG_STARTED received");
                        return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
                    case ACTION_DRAG_ENTERED:
                        Log.i("dragEvent", "ACTION_DRAG_ENTERED received");
                        v.setAlpha(0.5f);
                        return true;
                    case ACTION_DRAG_LOCATION:
                        Log.i("dragEvent", "ACTION_DRAG_LOCATION received");
                        return true;
                    case ACTION_DRAG_EXITED:
                        Log.i("dragEvent", "ACTION_DRAG_EXITED received");
                        v.setAlpha(1f);
                        return true;
                    case ACTION_DROP:
                        Log.i("dragEvent", "ACTION_DROP received");
                        v.setAlpha(1f);
                        swapColor(v);
                        return true;
                    case ACTION_DRAG_ENDED:
                        Log.i("dragEvent", "ACTION_DRAG_ENDED received");
                        if (!event.getResult() && v == dragOriginView) {
                            dragOriginView.setBackground(tempDrawableStorage);
                            tempDrawableStorage = null;
                        }
                        v.setAlpha(1f);
                        isDragging = false;
                    default:
                        //Do nothing
                }
                return false;
            }
        };
        ivGrid1.setOnDragListener(listener);
        ivGrid2.setOnDragListener(listener);
        ivGrid3.setOnDragListener(listener);
        ivGrid4.setOnDragListener(listener);
    }

    private void swapColor(View dragTarget) {
        if (dragOriginView == dragTarget) {
            //Restore color
            dragOriginView.setBackground(tempDrawableStorage);
            tempDrawableStorage = null;
            return;
        }
        Drawable targetBackground = dragTarget.getBackground();
        dragOriginView.setBackground(targetBackground);
        dragTarget.setBackground(tempDrawableStorage);
        tempDrawableStorage = null;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        this.mDetector.onTouchEvent(event);
        return super.onTouchEvent(event);
    }

    class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
        private static final String DEBUG_TAG = "Gestures";

        @Override
        public boolean onDown(MotionEvent event) {
            Log.d(DEBUG_TAG, "onDown: " + event.toString());
            return true;
        }

        @Override
        public boolean onFling(MotionEvent event1, MotionEvent event2,
                               float velocityX, float velocityY) {
            Log.d(DEBUG_TAG, "onFling: " + ((event1 != null) ? event1.toString() : "" + event2.toString()));
            return true;
        }
    }
}