뷰로 드래그 앤 드롭 구현

드래그 스타트를 트리거할 수 있는 이벤트에 응답하고 드롭 이벤트를 응답하여 사용하여 뷰에서 드래그 앤 드롭 프로세스를 구현할 수 있습니다.

드래그 시작

사용자는 일반적으로 드래그할 항목을 터치하거나 길게 클릭하여 동작으로 드래그를 시작합니다.

View에서 이를 처리하려면 ClipData 객체와 이동할 데이터의 ClipData.Item 객체를 만듭니다. ClipData의 일부로 ClipData 내의 ClipDescription 객체에 저장된 메타데이터를 제공합니다. 데이터 이동을 나타내지 않는 드래그 앤 드롭 작업의 경우 실제 객체 대신 null를 사용할 수 있습니다.

예를 들어, 이 코드 스니펫은 ImageView의 태그(또는 라벨)가 포함된 ClipData 객체를 생성하여 ImageView의 길게 터치 동작에 응답하는 방법을 보여줍니다.

Kotlin

// Create a string for the ImageView label.
val IMAGEVIEW_TAG = "icon bitmap"
...
val imageView = ImageView(context).apply {
    // Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
    setImageBitmap(iconBitmap)
    tag = IMAGEVIEW_TAG
    setOnLongClickListener { v ->
        // Create a new ClipData. This is done in two steps to provide
        // clarity. The convenience method ClipData.newPlainText() can
        // create a plain text ClipData in one step.

        // Create a new ClipData.Item from the ImageView object's tag.
        val item = ClipData.Item(v.tag as? CharSequence)

        // Create a new ClipData using the tag as a label, the plain text
        // MIME type, and the already-created item. This creates a new
        // ClipDescription object within the ClipData and sets its MIME type
        // to "text/plain".
        val dragData = ClipData(
            v.tag as? CharSequence,
            arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
            item)

        // Instantiate the drag shadow builder. We use this imageView object
        // to create the default builder.
        val myShadow = View.DragShadowBuilder(view: this)

        // Start the drag.
        v.startDragAndDrop(dragData,  // The data to be dragged.
                            myShadow,  // The drag shadow builder.
                            null,      // No need to use local data.
                            0          // Flags. Not currently used, set to 0.
        )

        // Indicate that the long-click is handled.
        true
    }
}

Java

// Create a string for the ImageView label.
private static final String IMAGEVIEW_TAG = "icon bitmap";
...
// Create a new ImageView.
ImageView imageView = new ImageView(context);

// Set the bitmap for the ImageView from an icon bitmap defined elsewhere.
imageView.setImageBitmap(iconBitmap);

// Set the tag.
imageView.setTag(IMAGEVIEW_TAG);

// Set a long-click listener for the ImageView using an anonymous listener
// object that implements the OnLongClickListener interface.
imageView.setOnLongClickListener( v -> {

    // Create a new ClipData. This is done in two steps to provide clarity. The
    // convenience method ClipData.newPlainText() can create a plain text
    // ClipData in one step.

    // Create a new ClipData.Item from the ImageView object's tag.
    ClipData.Item item = new ClipData.Item((CharSequence) v.getTag());

    // Create a new ClipData using the tag as a label, the plain text MIME type,
    // and the already-created item. This creates a new ClipDescription object
    // within the ClipData and sets its MIME type to "text/plain".
    ClipData dragData = new ClipData(
            (CharSequence) v.getTag(),
            new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN },
            item);

    // Instantiate the drag shadow builder. We use this imageView object
    // to create the default builder.
    View.DragShadowBuilder myShadow = new View.DragShadowBuilder(imageView);

    // Start the drag.
    v.startDragAndDrop(dragData,  // The data to be dragged.
                            myShadow,  // The drag shadow builder.
                            null,      // No need to use local data.
                            0          // Flags. Not currently used, set to 0.
    );

    // Indicate that the long-click is handled.
    return true;
});

드래그 시작에 응답

드래그 작업 중 시스템은 드래그 이벤트를 현재 레이아웃에 포함된 View 객체의 드래그 이벤트 리스너로 전송합니다. 리스너는 DragEvent.getAction()를 호출하여 작업 유형을 가져오는 방식으로 반응합니다. 드래그 시작 시 이 메서드는 ACTION_DRAG_STARTED를 반환합니다.

드래그 이벤트 리스너는 작업 유형이 ACTION_DRAG_STARTED인 이벤트에 응답하여 다음을 실행해야 합니다.

  1. DragEvent.getClipDescription()를 호출하고 반환된 ClipDescription의 MIME 유형 메서드를 사용하여 리스너가 드래그되는 데이터를 수락할 수 있는지 확인합니다.

    드래그 앤 드롭 작업이 데이터 이동을 나타내지 않는 경우 이 작업은 필요하지 않을 수 있습니다.

  2. 드래그 이벤트 리스너가 드롭을 허용할 수 있는 경우 true를 반환하여 시스템에 드래그 이벤트를 계속 리스너로 전송하도록 알려야 합니다. 리스너가 드롭을 수락할 수 없으면 리스너는 false를 반환해야 합니다. 그러면 시스템에서 ACTION_DRAG_ENDED를 전송하여 드래그 앤 드롭 작업을 종료할 때까지 리스너로 드래그 이벤트 전송을 중지합니다.

ACTION_DRAG_STARTED 이벤트의 경우 getClipData(), getX(), getY(), getResult() DragEvent 메서드는 유효하지 않습니다.

드래그 중 이벤트 처리

드래그 작업 중에 ACTION_DRAG_STARTED 드래그 이벤트에 대한 응답으로 true를 반환하는 드래그 이벤트 리스너는 드래그 이벤트를 계속 수신합니다. 드래그 중에 리스너가 수신하는 드래그 이벤트 유형은 드래그 섀도우의 위치와 리스너의 View의 표시 여부에 따라 다릅니다. 리스너는 주로 드래그 이벤트를 사용하여 View의 모양을 변경해야 하는지 결정합니다.

드래그 작업 중, DragEvent.getAction()은 다음 세 가지 값 중 하나를 반환합니다.

  • ACTION_DRAG_ENTERED: 터치 포인트(화면에서 사용자 손가락 또는 마우스 아래 있는 지점)가 리스너 View의 경계 상자에 들어가면 리스너가 이 이벤트 작업 유형을 수신합니다.
  • ACTION_DRAG_LOCATION: 리스너는 ACTION_DRAG_ENTERED 이벤트를 수신하면 ACTION_DRAG_EXITED 이벤트를 수신할 때까지 터치 포인트가 움직일 때마다 새 ACTION_DRAG_LOCATION 이벤트를 수신합니다. getX()getY() 메서드는 터치 포인트의 X 및 Y 좌표를 반환합니다.
  • ACTION_DRAG_EXITED: 이 이벤트 작업 유형은 이전에 ACTION_DRAG_ENTERED를 수신하는 리스너로 전송됩니다. 이 이벤트는 드래그 섀도우 터치 포인트가 리스너 View의 경계 상자 내부에서 외부로 이동할 때 전송됩니다.

드래그 이벤트 리스너는 이러한 작업 유형에 반응할 필요가 없습니다. 리스너가 시스템에 값을 반환하더라도 이 값은 무시됩니다.

다음은 이러한 작업 유형 각각에 응답하는 데 관한 몇 가지 가이드라인입니다.

  • 리스너는 ACTION_DRAG_ENTERED 또는 ACTION_DRAG_LOCATION의 응답으로 뷰가 잠재적 드롭 타겟임을 나타내는 View의 모양을 변경할 수 있습니다.
  • 작업 유형이 ACTION_DRAG_LOCATION인 이벤트는 터치 포인트의 위치에 해당하는 getX()getY()의 유효한 데이터를 포함합니다. 리스너는 이 정보를 사용하여 터치 포인트의 View 모양을 변경하거나 사용자가 콘텐츠를 드롭할 수 있는 정확한 위치를 결정할 수 있습니다.
  • ACTION_DRAG_EXITED에 대한 응답으로 리스너는 ACTION_DRAG_ENTERED 또는 ACTION_DRAG_LOCATION에 대한 응답으로 적용되는 모양 변경사항을 재설정해야 합니다. 이는 View가 곧 드롭이 일어날 수 있는 드롭 타겟이 아님을 사용자에게 나타냅니다.

드롭에 응답

사용자가 View 위에 드래그 섀도우를 놓고 View에서 이전에 드래그되는 콘텐츠를 허용할 수 있다고 보고하면 시스템은 작업 유형 ACTION_DROP와 함께 드래그 이벤트를 View에 전송합니다.

드래그 이벤트 리스너는 다음을 실행해야 합니다.

  1. getClipData()를 호출하여 startDragAndDrop() 호출에서 원래 제공된 ClipData 객체를 가져오고 데이터를 처리합니다. 드래그 앤 드롭 작업이 데이터 이동을 나타내지 않는 경우 이 작업은 필요하지 않습니다.

  2. 불리언 true를 반환하여 드롭이 성공적으로 처리되었음을 나타내거나 그렇지 않은 경우 false를 반환합니다. 반환된 값은 최종 ACTION_DRAG_ENDED 이벤트에 관해 getResult()에서 반환한 값이 됩니다. 시스템이 ACTION_DROP 이벤트를 전송하지 않으면 ACTION_DRAG_ENDED 이벤트의 getResult()에서 반환된 값은 false입니다.

ACTION_DROP 이벤트의 경우 getX()getY()는 드롭을 수신하는 View의 좌표계를 사용하여 드롭 시 터치 포인트의 XY 위치를 반환합니다.

사용자가 드래그 이벤트 리스너가 드래그 이벤트를 수신하지 않는 View, 앱 UI의 빈 영역 또는 애플리케이션 외부의 영역 위에 드래그 섀도우를 놓을 수 있지만 Android는 작업 유형이 ACTION_DROP인 이벤트를 전송하지 않으며 ACTION_DRAG_ENDED 이벤트만 전송합니다.

드래그 종료에 응답

사용자가 드래그 섀도우를 놓으면 즉시 시스템에서 작업 유형이 ACTION_DRAG_ENDED인 드래그 이벤트를 애플리케이션의 모든 드래그 이벤트 리스너로 전송합니다. 이는 드래그 작업이 완료되었음을 나타냅니다.

각 드래그 이벤트 리스너는 다음을 실행해야 합니다.

  1. 작업 중에 리스너의 모양이 변경되면 사용자에게 작업이 완료되었음을 시각적으로 표시하기 위해 기본 모양으로 재설정해야 합니다.
  2. 리스너에서 선택적으로 getResult()를 호출하여 작업에 관한 자세한 정보를 확인할 수 있습니다. 리스너가 작업 유형이 ACTION_DROP인 이벤트에 대한 응답으로 true를 반환하면 getResult()는 불리언 true를 반환합니다. 그 외 모든 경우에서 getResult()는 시스템이 ACTION_DROP 이벤트를 전송하지 않는 경우를 포함하여 불리언 false를 반환합니다.
  3. 드롭 작업이 성공적으로 완료되었음을 나타내려면 리스너는 불리언 true를 시스템에 반환해야 합니다. false를 반환하지 않으면 소스로 돌아가는 그림자를 표시하는 시각적 신호가 사용자에게 작업이 실패했음을 제안할 수 있습니다.

드래그 이벤트에 응답: 예

모든 드래그 이벤트는 드래그 이벤트 메서드나 리스너에서 수신합니다. 다음 코드 스니펫은 드래그 이벤트에 응답하는 예입니다.

Kotlin

val imageView = ImageView(this)

// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->

    // Handle each of the expected events.
    when (e.action) {
        DragEvent.ACTION_DRAG_STARTED -> {
            // Determine whether this View can accept the dragged data.
            if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                (v as? ImageView)?.setColorFilter(Color.BLUE)

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate()

                // Return true to indicate that the View can accept the dragged
                // data.
                true
            } else {
                // Return false to indicate that, during the current drag and
                // drop operation, this View doesn't receive events again until
                // ACTION_DRAG_ENDED is sent.
                false
            }
        }
        DragEvent.ACTION_DRAG_ENTERED -> {
            // Apply a green tint to the View.
            (v as? ImageView)?.setColorFilter(Color.GREEN)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }

        DragEvent.ACTION_DRAG_LOCATION ->
            // Ignore the event.
            true
        DragEvent.ACTION_DRAG_EXITED -> {
            // Reset the color tint to blue.
            (v as? ImageView)?.setColorFilter(Color.BLUE)

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate()

            // Return true. The value is ignored.
            true
        }
        DragEvent.ACTION_DROP -> {
            // Get the item containing the dragged data.
            val item: ClipData.Item = e.clipData.getItemAt(0)

            // Get the text data from the item.
            val dragData = item.text

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()

            // Turn off color tints.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Return true. DragEvent.getResult() returns true.
            true
        }

        DragEvent.ACTION_DRAG_ENDED -> {
            // Turn off color tinting.
            (v as? ImageView)?.clearColorFilter()

            // Invalidate the view to force a redraw.
            v.invalidate()

            // Do a getResult() and display what happens.
            when(e.result) {
                true ->
                    Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
                else ->
                    Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
            }.show()

            // Return true. The value is ignored.
            true
        }
        else -> {
            // An unknown action type is received.
            Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
            false
        }
    }
}

Java

View imageView = new ImageView(this);

// Set the drag event listener for the View.
imageView.setOnDragListener( (v, e) -> {

    // Handle each of the expected events.
    switch(e.getAction()) {

        case DragEvent.ACTION_DRAG_STARTED:

            // Determine whether this View can accept the dragged data.
            if (e.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {

                // As an example, apply a blue color tint to the View to
                // indicate that it can accept data.
                ((ImageView)v).setColorFilter(Color.BLUE);

                // Invalidate the view to force a redraw in the new tint.
                v.invalidate();

                // Return true to indicate that the View can accept the dragged
                // data.
                return true;

            }

            // Return false to indicate that, during the current drag-and-drop
            // operation, this View doesn't receive events again until
            // ACTION_DRAG_ENDED is sent.
            return false;

        case DragEvent.ACTION_DRAG_ENTERED:

            // Apply a green tint to the View.
            ((ImageView)v).setColorFilter(Color.GREEN);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DRAG_LOCATION:

            // Ignore the event.
            return true;

        case DragEvent.ACTION_DRAG_EXITED:

            // Reset the color tint to blue.
            ((ImageView)v).setColorFilter(Color.BLUE);

            // Invalidate the view to force a redraw in the new tint.
            v.invalidate();

            // Return true. The value is ignored.
            return true;

        case DragEvent.ACTION_DROP:

            // Get the item containing the dragged data.
            ClipData.Item item = e.getClipData().getItemAt(0);

            // Get the text data from the item.
            CharSequence dragData = item.getText();

            // Display a message containing the dragged data.
            Toast.makeText(this, "Dragged data is " + dragData, Toast.LENGTH_LONG).show();

            // Turn off color tints.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Return true. DragEvent.getResult() returns true.
            return true;

        case DragEvent.ACTION_DRAG_ENDED:

            // Turn off color tinting.
            ((ImageView)v).clearColorFilter();

            // Invalidate the view to force a redraw.
            v.invalidate();

            // Do a getResult() and displays what happens.
            if (e.getResult()) {
                Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG).show();
            }

            // Return true. The value is ignored.
            return true;

        // An unknown action type is received.
        default:
            Log.e("DragDrop Example","Unknown action type received by View.OnDragListener.");
            break;
    }

    return false;

});

드래그 그림자 맞춤설정

View.DragShadowBuilder의 메서드를 재정의하여 맞춤설정된 myDragShadowBuilder를 정의할 수 있습니다. 다음 코드 스니펫은 TextView에 작은 직사각형 회색 드래그 섀도우를 만듭니다.

Kotlin

private class MyDragShadowBuilder(view: View) : View.DragShadowBuilder(view) {

    private val shadow = ColorDrawable(Color.LTGRAY)

    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    override fun onProvideShadowMetrics(size: Point, touch: Point) {

            // Set the width of the shadow to half the width of the original
            // View.
            val width: Int = view.width / 2

            // Set the height of the shadow to half the height of the original
            // View.
            val height: Int = view.height / 2

            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height)

            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height)

            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2)
    }

    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    override fun onDrawShadow(canvas: Canvas) {

            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas)
    }
}

Java

private static class MyDragShadowBuilder extends View.DragShadowBuilder {

    // The drag shadow image, defined as a drawable object.
    private static Drawable shadow;

    // Constructor.
    public MyDragShadowBuilder(View view) {

            // Store the View parameter.
            super(view);

            // Create a draggable image that fills the Canvas provided by the
            // system.
            shadow = new ColorDrawable(Color.LTGRAY);
    }

    // Define a callback that sends the drag shadow dimensions and touch point
    // back to the system.
    @Override
    public void onProvideShadowMetrics (Point size, Point touch) {

            // Define local variables.
            int width, height;

            // Set the width of the shadow to half the width of the original
            // View.
            width = getView().getWidth() / 2;

            // Set the height of the shadow to half the height of the original
            // View.
            height = getView().getHeight() / 2;

            // The drag shadow is a ColorDrawable. Set its dimensions to
            // be the same as the Canvas that the system provides. As a result,
            // the drag shadow fills the Canvas.
            shadow.setBounds(0, 0, width, height);

            // Set the size parameter's width and height values. These get back
            // to the system through the size parameter.
            size.set(width, height);

            // Set the touch point's position to be in the middle of the drag
            // shadow.
            touch.set(width / 2, height / 2);
    }

    // Define a callback that draws the drag shadow in a Canvas that the system
    // constructs from the dimensions passed to onProvideShadowMetrics().
    @Override
    public void onDrawShadow(Canvas canvas) {

            // Draw the ColorDrawable on the Canvas passed in from the system.
            shadow.draw(canvas);
    }
}