Creating Drag-and-Drop Features Using XML

While modern Android development often leans towards Jetpack Compose and Kotlin, there are still scenarios where understanding and implementing drag-and-drop features using XML is essential. Drag-and-drop functionality allows users to interact with UI elements in a more intuitive and engaging way. This blog post explores how to create drag-and-drop features using XML-based layouts and Java/Kotlin code.

Understanding Drag-and-Drop in Android XML

Drag-and-drop functionality in Android using XML relies on several core components provided by the Android framework. Key classes and concepts include:

  • Drag Events: The framework provides drag events that are dispatched to view objects within an application. These events signify the start, ongoing actions, and completion of a drag-and-drop operation.
  • Drag Listeners: These listeners are implemented by view objects to handle drag events. They determine how a view reacts when an object is dragged onto it or dragged away.
  • ClipData and ClipDescription: Used to define the data being dragged, including its MIME type. ClipData carries the data, while ClipDescription provides metadata about the clip.

Setting Up Drag-and-Drop in XML and Java/Kotlin

Implementing drag-and-drop typically involves:

  1. Configuring the draggable view.
  2. Setting up drag listeners for target views.
  3. Implementing the necessary callbacks.

Step 1: Configure the Draggable View (XML)

First, define the draggable view in your XML layout. For example, a simple TextView:


<TextView
    android:id="@+id/draggable_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Drag Me!"
    android:background="#FF5722"
    android:padding="16dp"
    android:textColor="#FFFFFF" />

Step 2: Initialize and Configure the Draggable View (Java/Kotlin)

In your Activity or Fragment, initialize the view and set an OnLongClickListener to start the drag operation.

Java:


import android.content.ClipData;
import android.content.ClipDescription;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

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

        TextView draggableTextView = findViewById(R.id.draggable_view);
        draggableTextView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                ClipData.Item item = new ClipData.Item((CharSequence) v.getTag());
                String mimeType = ClipDescription.MIMETYPE_TEXT_PLAIN;
                ClipData data = new ClipData(v.getTag().toString(), new String[]{mimeType}, item);

                View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(v);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    v.startDragAndDrop(data, shadowBuilder, v, 0);
                } else {
                    v.startDrag(data, shadowBuilder, v, 0);
                }
                return true;
            }
        });

        draggableTextView.setTag("Draggable Data");
    }
}

Kotlin:


import android.content.ClipData
import android.content.ClipDescription
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val draggableTextView: TextView = findViewById(R.id.draggable_view)
        draggableTextView.setOnLongClickListener { v ->
            val item = ClipData.Item(v.tag as? CharSequence)
            val mimeType = ClipDescription.MIMETYPE_TEXT_PLAIN
            val data = ClipData(v.tag.toString(), arrayOf(mimeType), item)

            val shadowBuilder = View.DragShadowBuilder(v)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                v.startDragAndDrop(data, shadowBuilder, v, 0)
            } else {
                v.startDrag(data, shadowBuilder, v, 0)
            }
            true
        }

        draggableTextView.tag = "Draggable Data"
    }
}

Explanation:

  • ClipData: Holds the data to be dragged. It consists of one or more ClipData.Item objects.
  • ClipDescription: Describes the type of data in the ClipData. Here, we’re using plain text (MIMETYPE_TEXT_PLAIN).
  • DragShadowBuilder: Provides a visual representation of the dragged data (a shadow).
  • startDragAndDrop/startDrag: Starts the drag operation. startDragAndDrop is preferred for API level 24 and above, while startDrag is used for older versions.

Step 3: Configure the Drop Target View (XML)

Define the target view where the draggable view can be dropped.


<TextView
    android:id="@+id/drop_target_view"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:text="Drop Here!"
    android:background="#64B5F6"
    android:gravity="center"
    android:textColor="#FFFFFF" />

Step 4: Set Up Drag Listeners for the Target View (Java/Kotlin)

In your Activity or Fragment, set up the OnDragListener for the drop target view.

Java:


import android.content.ClipDescription;
import android.os.Bundle;
import android.view.DragEvent;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

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

        TextView draggableTextView = findViewById(R.id.draggable_view);
        TextView dropTargetView = findViewById(R.id.drop_target_view);

        draggableTextView.setOnLongClickListener(new View.OnLongClickListener() {
            // Same implementation as before
        });

        draggableTextView.setTag("Draggable Data");

        dropTargetView.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                switch (event.getAction()) {
                    case DragEvent.ACTION_DRAG_STARTED:
                        return event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
                    case DragEvent.ACTION_DRAG_ENTERED:
                        v.setBackgroundColor(0xFF00FF00); // Highlight
                        break;
                    case DragEvent.ACTION_DRAG_EXITED:
                        v.setBackgroundColor(0xFF64B5F6); // Reset
                        break;
                    case DragEvent.ACTION_DROP:
                        ClipData.Item item = event.getClipData().getItemAt(0);
                        String dragData = item.getText().toString();
                        Toast.makeText(MainActivity.this, "Dropped: " + dragData, Toast.LENGTH_SHORT).show();
                        break;
                    case DragEvent.ACTION_DRAG_ENDED:
                        v.setBackgroundColor(0xFF64B5F6); // Ensure background is reset
                        break;
                    default:
                        return false;
                }
                return true;
            }
        });
    }
}

Kotlin:


import android.content.ClipDescription
import android.os.Bundle
import android.view.DragEvent
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val draggableTextView: TextView = findViewById(R.id.draggable_view)
        val dropTargetView: TextView = findViewById(R.id.drop_target_view)

        draggableTextView.setOnLongClickListener { v ->
            // Same implementation as before
            val item = ClipData.Item(v.tag as? CharSequence)
            val mimeType = ClipDescription.MIMETYPE_TEXT_PLAIN
            val data = ClipData(v.tag.toString(), arrayOf(mimeType), item)

            val shadowBuilder = View.DragShadowBuilder(v)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                v.startDragAndDrop(data, shadowBuilder, v, 0)
            } else {
                v.startDrag(data, shadowBuilder, v, 0)
            }
            true
        }

        draggableTextView.tag = "Draggable Data"

        dropTargetView.setOnDragListener { v, event ->
            when (event.action) {
                DragEvent.ACTION_DRAG_STARTED -> {
                    event.clipDescription?.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) ?: false
                }
                DragEvent.ACTION_DRAG_ENTERED -> {
                    v.setBackgroundColor(0xFF00FF00.toInt()) // Highlight
                    true
                }
                DragEvent.ACTION_DRAG_EXITED -> {
                    v.setBackgroundColor(0xFF64B5F6.toInt()) // Reset
                    true
                }
                DragEvent.ACTION_DROP -> {
                    val item = event.clipData?.getItemAt(0)
                    val dragData = item?.text.toString()
                    Toast.makeText(this, "Dropped: $dragData", Toast.LENGTH_SHORT).show()
                    true
                }
                DragEvent.ACTION_DRAG_ENDED -> {
                    v.setBackgroundColor(0xFF64B5F6.toInt()) // Ensure background is reset
                    true
                }
                else -> false
            }
        }
    }
}

Explanation of the OnDragListener:

  • ACTION_DRAG_STARTED: Called when a drag operation starts. Verify that the target can accept the type of data being dragged.
  • ACTION_DRAG_ENTERED: Called when the dragged view enters the target view’s bounds.
  • ACTION_DRAG_EXITED: Called when the dragged view exits the target view’s bounds.
  • ACTION_DROP: Called when the dragged view is dropped on the target. Retrieve the data and handle it accordingly.
  • ACTION_DRAG_ENDED: Called when the drag operation ends, whether successful or not. Reset any UI changes.

Complete Example: activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/draggable_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Drag Me!"
        android:background="#FF5722"
        android:padding="16dp"
        android:textColor="#FFFFFF"
        android:layout_marginBottom="16dp"/>

    <TextView
        android:id="@+id/drop_target_view"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:text="Drop Here!"
        android:background="#64B5F6"
        android:gravity="center"
        android:textColor="#FFFFFF" />

</LinearLayout>

Advanced Scenarios

Multiple Draggable Items

You can set up multiple draggable views, each with different tags and data, and manage them in the same way. Ensure each view has a unique tag that corresponds to its data.

Different Types of Data

Handle different types of data by specifying different MIME types in the ClipDescription and appropriately handling the data in the ACTION_DROP case. For example, you could drag images or structured data (JSON) by serializing and deserializing the data accordingly.

Conclusion

Creating drag-and-drop features using XML and Java/Kotlin is a powerful way to enhance user interaction in Android applications. By understanding the core components like ClipData, ClipDescription, and drag listeners, developers can implement intuitive and engaging user experiences. While modern tools like Jetpack Compose offer alternative approaches, familiarity with the XML-based method remains valuable for maintaining legacy apps and understanding the underlying framework. The techniques outlined in this guide enable you to build robust and flexible drag-and-drop functionality in your Android projects, catering to various scenarios and data types.