+3

Multitouch, Drag and Drop in Android

Như các bạn đã biết, màn hình cảm ứng của điện thoại smartphone không chỉ là chạm vào để bấm nút thay cho bàn phím mà nó còn có thể có thao tác vuốt, click nhiều lần vào 1 thời điểm... Nhờ vào các event đó mà chũng ta có thể tùy biến nhiều chức năng khác cho chiếc smartphone của mình.

Bài viết này, tôi xin giới thiệu với các bạn về cách xử lý các event chạm, kéo thả hay muiltiple touch trên Android.

I) Single touch and Multiple touch

Android cung cấp lớp MotionEvent để hỗ trợ việc cảm ứng qua sự kiện chạm vào màn hình thông qua view với phương thức onTouchEvent(). Bạn chỉ cần overide lại phương thức onTouchEvent() đối với view mà bạn muốn.

Lớp MotionEvent chứa các thông tin liên quan như: số lượng con trỏ, tọa độ X / Y, kích thướcáp lực của mỗi con trỏ. Thông tin chi tiết tham khảo tại đây

Đối với các view bình thường, bạn chỉ cần gắn sự kiện OnTouchListener và override lại hàm onTouchEvent(MotionEvent event) là có thể bắt các sự kiện và phản ứng lại như bạn muốn.

Đối số trong hàm ontouchEvent chính là đối tương MotionEvent. Đối tượng này cung cấp cho bạn hàm getAction()getActionMasked() trả ra các giá trị để bạn xác định các sự kiện. Thông thường ra sử dụng hàm getActionMasked() để bắt các sự kiện multiple touch và hàm getAction() cho sự kiện single touch.

Về cơ bản 2 hàm này trả ra các giá trị gần giống nhau:

  • MotionEvent.ACTION_DOWN : Trả về với sự kiện khi bạn bắt đầu chạm vào view
  • MotionEvent.ACTION_MOVE : Trả về khi bạn di chuyển trên view đó.
  • MotionEvent.ACTION_UP : Trả về khi bạn nhấc tay ra khỏi view.
  • MotionEvent.ACTION_CANCEL : Trả về khi event touch cancel.
  • MotionEvent.ACTION_POINTER_DOWN : Chỉ có với multi-touch, trả về vị trí của mới khi bạn multiple touch
  • MotionEvent.ACTION_POINTER_UP : Chỉ có với multi-touch, trả ra khi bạn cancel một điểm mà bạn đã chạm trước đó.

Ví dụ sau thực hiện việc tạo một SingletouchView để vẽ đường thằng và khi bạn nhắc tay ra sẽ vẽ một đường tròn ở đó. (Tham khảo tại vogella)

package com.example.rayleigh.multipletouch;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.Toast;

public class SingleTouchEventView extends View {
    private Paint paint = new Paint();
    private Path path = new Path();

    public SingleTouchEventView(Context context, AttributeSet attrs) {
        super(context, attrs);

        paint.setAntiAlias(true);
        paint.setStrokeWidth(6f);
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeJoin(Paint.Join.ROUND);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawPath(path, paint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float eventX = event.getX();
        float eventY = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
//                Toast.makeText(getContext(), "Test Down", Toast.LENGTH_SHORT).show();
                path.moveTo(eventX, eventY);
                return true;
            case MotionEvent.ACTION_MOVE:
//                Toast.makeText(getContext(), "Test Move", Toast.LENGTH_SHORT).show();
                path.lineTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_UP:
                // nothing to do
//                Toast.makeText(getContext(), "Test Up", Toast.LENGTH_SHORT).show();
                path.addCircle(eventX, eventY, 10, Path.Direction.CW);
                break;
            case MotionEvent.ACTION_CANCEL: {
//              Toast.makeText(getContext(), "Test CANCE", Toast.LENGTH_SHORT).show();
                path.addCircle(eventX, eventY, 10, Path.Direction.CW);
                break;
            }
            default:
                return false;
        }

        // Schedules a repaint.
        invalidate();
        return true;
    }
}

Với đoạn code trên ta thấy, khi bạn bắt đầu chạm vào màn hình thì ta sử dụng Paint để vẽ một đường thẳng với điểm bắt đầu là điểm ta chạm vào MotionEvent.ACTION_DOWN:. Khi ta di chuyển MotionEvent.ACTION_MOVE thì ta vẽ đường thẳng tới điểm đó. Khi ta di chuyển liên tục thì sẽ vẽ theo đường mà tay ta vuốt. Ta chỉ việc khai báo nó trong file layout xml như sau:

    <com.example.rayleigh.multipletouch.SingleTouchEventView
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:id="@+id/view"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true"
        android:background="#fffff5fc"
        android:layout_alignParentBottom="true"
        android:layout_marginTop="100dp" />

Do sử dụng máy ảo để demo lên ta không thẻ demo với multiple touch.Vì vậy mình chỉ trình bày về single touch. Về cơ bản các event hay xử lý là như nhau. Bạn chỉ cần hiểu khi sử dụng event onTouchEvent là có thể sử dụng mutiple touch thành thạo. Link tham khảo mình gửi bên trên cũng hướng dẫn cả với multiple touch.

II) Drang and drop

Khi bạn đã nắm rõ được onTouchEvent thì việc bạn làm việc với Drag and Drop cũng tương tự. Android cung cấp lớp DragEvent hỗ trợ cho việc drag and drop với việc overide phương thức onDragEvent với việc cài đặt **OnDragListener ** cho view mà bạn muốn thực hiện. DragEventcũng cung cấp cho bạn các phương thức lấy tọa độ X / Y, getAction() để cho bạn kiểm soát event của người dùng.

Đối với Drag and Drop thì khi chạm vào đối tượng ta sẽ tạo ra một đối tượng ảo gắn liền với ngón tay của chúng ra để khi di chuyển thì đối tượng đó sẽ tượng trưng cho view di chuyển theo. Đối tượng đó chính là ClipData.

và hàm getAction() cũng trả về các giá trị tương ứng với các event:

  • DragEvent.ACTION_DRAG_STARTED : Khi người sử dụng bắt đầu event drag
  • DragEvent.ACTION_DRAG_ENTERED :Khi bắt đầu drag view
  • DragEvent.ACTION_DRAG_LOCATION : Khi ta drag view ở trong chính view của đối tượng đó. (Một view thường có một không gian nhất định, và khi tay của ta vẫn ở trong view đó (ClipData) thì hàm getAction() sẽ trả về là drag location)
  • DragEvent.ACTION_DRAG_EXITED : Khi đối tượng hay ClipData mà di chuyển ra ngoài view cài đặt Drag and Drop thì sẽ trả về giá trị này.
  • DragEvent.ACTION_DROP: Chỉ trả về khi ta drop view mà điểm thả ClipData ra ở trong vùng của view đó.
  • DragEvent.ACTION_DRAG_ENDED : Khi ta drop đối tượng dù ở đâu cũng vậy, và event drag and drop kết thúc.

Như vậy, các bạn đã thấy lớp DragEvent chỉ cung cấp cho chúng ra các event khi chúng ta di chuyển trong view đó và khi ta ra ngoài. CÒn khi đã ở ngoài thì ta không thể bắt được các event drag đó nữa mà chỉ khi ta lại di chuyển vào view đó thì ta mới bắt được event. Như vậy, khi thoát ra ngoài ta phải xử lý thế nào?

Ở đây, mình gợi ý cho các bạn một mẹo đó chính là khi ta di chuyển đối tượng ClipData thì đồng thời ta cũng di chuyển view đi cùng, như vậy ta sẽ luôn bắt được event drag_location để xử lý. Như vậy ta sẽ di chuyển view ngay trong event drag_location đó.

Mình đã sửa lại một số xử lý ở ví dụ sau để chạy theo ý tưởng của mình. Dưới đây là đoạn code xử lý của mình với event Drag and Drop.

public boolean onDrag(View v, DragEvent event) {
                switch(event.getAction())
                {
                    case DragEvent.ACTION_DRAG_STARTED:
                        layoutParams = (RelativeLayout.LayoutParams)v.getLayoutParams();
                        Log.d(msg, "Action is DragEvent.ACTION_DRAG_STARTED");
                        int x_cord = (int) event.getX();
                        int y_cord = (int) event.getY();
                        local_x = x_cord - v.getWidth()/2;
                        local_y = y_cord -  v.getHeight()/2 - 76;
                        layoutParams.leftMargin = local_x;
                        layoutParams.topMargin = local_y;
                        v.setLayoutParams(layoutParams);

                        Log.d(msg, "View - X-Y " + (int) v.getX() + ", " + (int) v.getY());
                        Log.d(msg, "X-Y" + x_cord + ", " + y_cord);
                        Log.d(msg, "Local X-Y" + local_x + ", " + local_y);

                        break;

                    case DragEvent.ACTION_DRAG_ENTERED:
                        break;

                    case DragEvent.ACTION_DRAG_EXITED :
//                        Log.d(msg, "Action is DragEvent.ACTION_DRAG_EXITED");
                        x_cord = (int) event.getX();
                        y_cord = (int) event.getY() - 76;
                        layoutParams.leftMargin = x_cord;
                        layoutParams.topMargin = y_cord;
                        v.setLayoutParams(layoutParams);
                        Log.d(msg, "View2 - X-Y  " + (int) v.getX() + ", " + (int) v.getY());
                        Log.d(msg, "X-Y  " + x_cord + ", " + y_cord);

                        break;

                    case DragEvent.ACTION_DRAG_LOCATION  :
                        Log.d(msg, "Action is DragEvent.ACTION_DRAG_LOCATION");
                        x_cord = (int) event.getX() - v.getWidth()/2;
                        y_cord = (int) event.getY() - v.getHeight()/2;
                        layoutParams.leftMargin = (int) v.getX() + (x_cord);
                        layoutParams.topMargin = (int) v.getY() + (y_cord);
                        v.setLayoutParams(layoutParams);

                        Log.d(msg, "View - X-Y" + (int) v.getX() + ", " + (int) v.getY());
                        Log.d(msg, "X-Y" + x_cord + ", " + y_cord);

                        break;

                    case DragEvent.ACTION_DRAG_ENDED   :
                        Log.d(msg, "Action is DragEvent.ACTION_DRAG_ENDED");
                        // Do nothing
                        break;

                    case DragEvent.ACTION_DROP:
                        Log.d(msg, "ACTION_DROP event");
                        // Do nothing
                        break;
                    default: break;
                }
                return true;
            }

Việc tính toán các giá trị để set giá trị margin cho view tùy thuộc vào cách mà bạn đặt view của mình ở đâu, giá trị margin ban đầu như thế nào.

Lưu ý:

  • Với event drag_location thì getX, getY trả về giá trị X,Y với mốc 0, 0 chính là điểm trên cùng bên trái view bạn set drag and drop
  • Với event drag_start, drag_enter, drag_exit thì getX, getY trả về giá trị X, Y so với layout chính của bạn. Tức là toàn bộ màn hình.

Dưới đây là video demo và source code ứng dụng mà mình đã viết

Source code: GitHub MultipleTouch

Tham khảo:


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.