10

Android Drag and Drop Tutorial: Moving Views and Data

 3 years ago
source link: https://www.raywenderlich.com/24508555-android-drag-and-drop-tutorial-moving-views-and-data
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Home Android & Kotlin Tutorials

Android Drag and Drop Tutorial: Moving Views and Data

Learn how to use Android’s drag-and-drop framework to enable an advanced gesture-based user experience.

By Kushal Kumar R Aug 30 2021 · Article (25 mins) · Intermediate

5/5 1 Rating

Version

Ready to delight your users with the Android drag-and-drop framework?

Drag and drop seems like magic because it gives your app users the capability to move UI elements by performing gestures on the screen. In this tutorial, you’ll learn about the events and actions that drive this framework so you can become a drag-and-drop wizard! You’ll create an app named Masky that lets you drag a mask onto the screen and drop it over an unmasked face. More specifically, you’ll learn how to:

  1. Design drag-and-drop operations.
  2. Customize the drag shadow.
  3. Respond to drag-and-drop events.
  4. Move a view — the mask — across the screen to its new drop area.
  5. Check if the mask view is on the face.
Note: This tutorial assumes you have a basic working knowledge of the Kotlin programming language and Android development. If you need to brush up on your skills, check out our Android and Kotlin for Beginners learning path or our books, Android Apprentice and Kotlin Apprentice.

You’ll start by looking at the starter project and learning how drag and drop works.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Open the starter project in Android Studio. Build and run. You’ll see the following screen:

The app currently shows the mask and the unmasked face, but you can’t do anything with them yet. The starter project is a clean slate, ready for you to start working on it. As you follow along with the tutorial, you’ll see the app take shape.

The Drag-and-Drop Process

Android’s drag-and-drop framework lets users move data and views using graphical gestures. Users can drag and drop data across views within the same app or even from one app to another, if they have multi-window mode enabled.

In this tutorial, you’ll use framework internals like drag event classes and drag listeners to design your own drag-and-drop operations.

The Four Drag-and-Drop States

The drag-and-drop process consists of four states:

  1. Started
  2. Continuing
  3. Dropped
  4. Ended

Now, you’ll take a look at each of these states, as the diagram below illustrates.

Started State

When the user makes a UI gesture that your app recognizes as a trigger, such as a long click, the drag process begins. The app provides drag data along with a drag shadow callback as arguments to the system via startDragAndDrop().

Note: To start a drag, use startDragAndDrop() for Nougat devices and newer. For pre-Nougat devices use startDrag().

The system first displays a drag shadow, which can be either a shadow or an actual draggable view, on the device. It uses the action type ACTION_DRAG_STARTED to dispatch a drag event to all the registered drag event listeners in the current layout.

The drag event listener returns a Boolean true to continue receiving drag events. If the listener only needs to know when the drag has ended, it can opt out of receiving the drag data by returning false instead. This ensures that the listener will only receive the final drag event with the action type ACTION_DRAG_ENDED.

Note: The following DragEvent methods are not valid on the ACTION_DRAG_STARTED event action type:
  • getClipData()
  • getX()
  • getY()
  • getResult()

Continuing State

As the user continues to drag, the drag process enters the continuing state.

In this state, the system dispatches one or more drag events to registered drag event listeners.

For example, as the drag shadow enters the bounding box of the view that is registered for drag events, the system dispatches the action type ACTION_DRAG_ENTERED to the listener.

After receiving an ACTION_DRAG_ENTERED event and before it can receive an ACTION_DRAG_EXITED event, the listener receives a new ACTION_DRAG_LOCATION event as the drag continues. Here you can retrieve the current x, y coordinates of the draggable view.

Likewise, when the drag shadow leaves the bounding box, an ACTION_DRAG_EXITED action type is sent to the listener.

In your app, you’ll only deal with the ACTION_DRAG_ENTERED and ACTION_DRAG_EXITED action types.

Dropped State

When the user releases the drag shadow over a view that’s registered for drag events, the system dispatches a drag event with action type ACTION_DROP.

You retrieve data passed from this drag event as arguments in startDragAndDrop()/startDrag().

Ended State

Finally, the system wraps up the drag operation by dispatching a drag event with the action type ACTION_DRAG_ENDED.

After receiving ACTION_DRAG_ENDED, each drag event listener should:

  1. Reset all the state or UI changes you made during the drag operation.
  2. Return a Boolean true.
  3. Optionally, check the drop success status by invoking getResult().

Now that you have a theoretical understanding of Android drag-and-drop operations, it’s time to apply this knowledge in a real app!

Designing a Drag Operation

Now, it’s time to implement drag and drop in Masky. You’ll program the app to let the user drag the mask around the screen and drop it over an unmasked face. After the user drops the mask, a toast message will notify them about whether the mask is on or off the face.

For this app, you’ll use a long click to start the drag operation.

Adding a Drag Shadow

The system displays a placeholder image to represent the actual view/data during a drag operation. This placeholder image representation is the drag shadow.

To create a drag shadow builder, you subclass View.DragShadowBuilder. You can then pass the builder as an argument to the system when you start a drag operation using startDragAndDrop() or startDrag().

The system then uses the drag shadow builder to invoke its callback methods to obtain a drag shadow.

If you don’t want to display a drag shadow, you don’t have to. You control whether the drop shadow displays or not by picking the appropriate View.DragShadowBuilder constructor:

  1. View.DragShadowBuilder(view): Accepts a View object to create a drag shadow that looks similar to the view object the user is dragging. To customize your drag shadow, you can subclass this class and override the methods, as you’ll see later.
  2. View.DragShadowBuilder(): This drag shadow builder has no parameters and will give you an invisible drag shadow. As the user drags the view, there’s no visual cue that the drag is in progress until it ends.

Your next step is to customize the drag shadow to match your requirements by subclassing View.DragShadowBuilder(view).

Customizing a Drag Shadow

When you use the default constructor, View.DragShadowBuilder(view), you’ll get a default drag shadow. The default values in a drag shadow have the same dimensions as the argument View.

The touch point is the location of the user’s finger within the drag shadow. The default touch point is at the center of the argument View.

Before you can start customizing your drag shadow, you need to import the following packages:

import android.graphics.Canvas
import android.graphics.Point
import android.view.View
import androidx.core.content.res.ResourcesCompat

Next, add MaskDragShadowBuilder by adding the following code to MainActivity.kt:

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

  //1
  private val shadow = ResourcesCompat.getDrawable(view.context.resources, R.drawable.ic_mask, view.context.theme)

  // 2
  override fun onProvideShadowMetrics(size: Point, touch: Point) {
    // 3
    val width: Int = view.width

    // 4
    val height: Int = view.height

    // 5
    shadow?.setBounds(0, 0, width, height)

    // 6
    size.set(width, height)

    // 7
    touch.set(width / 2, height / 2)
  }

  // 8
  override fun onDrawShadow(canvas: Canvas) {
    // 9
    shadow?.draw(canvas)
  }
}

This code creates a drag shadow that looks like the mask the user will drag. Here’s how it works:

  1. Sets the drag shadow appearance to match the actual mask. The drag shadow here is of a Drawable type.
  2. Invokes onProvideShadowMetrics() when you trigger startDragAndDrop() or startDrag().
  3. Defines the width of the drag shadow to be the full width of the mask View.
  4. Defines the height of the drag shadow to be the full height of the mask View.
  5. Sets the dimensions and offset position of the drag shadow on the canvas.
  6. Adjusts the size parameter’s width and height values.
  7. Sets the drag shadow touch point position to the middle of the drag shadow.
  8. Calls onDrawShadow() after calling onProvideShadowMetrics(). onDrawShadow() draws the actual drag shadow on a canvas object using the metrics from onProvideShadowMetrics().
  9. Draws the mask’s drag shadow Drawable in the Canvas.

Now that you have a drag shadow, it’s time to take care of the drag event listeners.

Implementing a Drag Event Listener

When the drag-and-drop process is in progress, the system sends a DragEvent to all the registered drag event listeners.

Next, you’ll implement View.OnDragListener to create a drag event listener object, then set the listener on the View‘s setOnDragListener().

Dispatching Drag Events

The system uses DragEvent to dispatch a drag event along with its associated data and action type. The action type in the DragEvent updates the registered listeners on what’s happening in the drag-and-drop process.

DragEvent defines six action type constants:

  1. ACTION_DRAG_STARTED
  2. ACTION_DRAG_ENTERED
  3. ACTION_DRAG_LOCATION
  4. ACTION_DRAG_EXITED
  5. ACTION_DROP
  6. ACTION_DRAG_ENDED

You can read the summary of each action type in the official Android documentation.

To access the action type constants, invoke getAction() on DragEvent.

DragEvent also contains optional data items valid for a specific action type. Read more about them in the official Android documentation.

Starting a Drag Operation

Before starting a drag operation, familiarize yourself with activity_main.xml. This file contains the primary layout for the drag-and-drop feature you’ll implement.

Focus on the following views:

  • Mask view: The ImageView.
  • Unmasked face: The Bugdroid mascot ImageView.
  • Mask drop area: The ConstraintLayout, which is the complete area within the dashed line boundary that represents the bounding box for drag enter/exit events.

Open MainActivity.kt and add attachViewDragListener() to it. attachViewDragListener() defines the set of actions to perform to start a drag operation.

Next, import the following packages:

import android.content.ClipData
import android.content.ClipDescription
import android.os.Build
import android.os.Bundle

Then, call attachViewDragListener() in the onCreate() lifecycle callback of your activity:

private fun attachViewDragListener() {

  // 1
  binding.mask.setOnLongClickListener { view: View ->

  // 2
  val item = ClipData.Item(maskDragMessage)

  // 3
  val dataToDrag = ClipData(
      maskDragMessage,
      arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN),
      item
  )

  // 4
  val maskShadow = MaskDragShadowBuilder(view)

  // 5
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
     //support pre-Nougat versions
     @Suppress("DEPRECATION")
     view.startDrag(dataToDrag, maskShadow, view, 0)
  } else {
     //supports Nougat and beyond
     view.startDragAndDrop(dataToDrag, maskShadow, view, 0)
  }

  // 6
  view.visibility = View.INVISIBLE

  //7
  true
 }
}

The code above adds drag start capabilities to the app. Here's how:

  1. The current layout calls for you to drag the mask view across other views. To do this, you set a long-click listener on the mask view that triggers the drag start.
  2. Create ClipData and ClipData.Item, which represent the data the user is dragging.
  3. Within ClipData, you include the array of MIME types the data represents, with the help of ClipDescription. If you don't want to support data movement, you can simply pass a null value for the MIME types.
  4. Instantiate a drag shadow builder. You don't need to customize the drag shadow, so you use
    the default View.DragShadowBuilder(view).
  5. Here, you supply ClipData, the drag shadow builder and a mask view that you want to drag as arguments to startDrag().
  6. Hide the mask view when the drag starts. Only the drag shadow should be visible during a drag operation.
  7. Return a Boolean true to tell the system that the click event was successful.

Build and run. Now, you can finally move the mask.

At this moment, however, your app only supports the drag operation. When you release the mask, it disappears. To let the user drop the mask, you'll add code to respond to events next.

Responding to Drag Events

To respond to drag events, you need to register a drag event listener with the maskDropArea view. Only views that have drag event listeners set can respond to drag events.

To start, open MainActivity.kt And add the following:

// 1
private val maskDragListener = View.OnDragListener { view, dragEvent ->

  //2
  val draggableItem = dragEvent.localState as View

  //3
  when (dragEvent.action) {
    DragEvent.ACTION_DRAG_STARTED -> {
      true
    }
    DragEvent.ACTION_DRAG_ENTERED -> {
      view.invalidate()
      true
    }
    DragEvent.ACTION_DRAG_LOCATION -> {
      true
    }
    DragEvent.ACTION_DRAG_EXITED -> {
      true
    }
    DragEvent.ACTION_DROP -> {
      true
    }
    DragEvent.ACTION_DRAG_ENDED -> {
      true
    }
    else -> {
      false
    }
  }
}

The code above creates a drag event listener. Here's how it works:

  1. Creates an instance of View.OnDragListener and assigns it to a variable, maskDragListener.
  2. Retrieves a reference to the mask view.
  3. A drag event listener can access getAction() to read the action type. Upon receiving the drag event, you match the action type to perform appropriate tasks.

Notice that all the branches in the when expression return a Boolean true, except the else branch.

Next, add the following in onCreate():

binding.maskDropArea.setOnDragListener(maskDragListener)

You take a reference to the mask drop area view that will respond to drag events. You then pass the drag event listener, maskDragListener, into the view's setOnDragListener().

Handling Events During the Drag

Right now, there's no visual indication to show where the user can or can't drop the mask. Your next step will be to handle events during the drag process to change the drop area view depending on where the mask is.

Add the following code snippets to your maskDragListener.

DragEvent.ACTION_DRAG_ENTERED -> {
  binding.maskDropArea.alpha = 0.3f
  true
}

The code above dims the drop area view when the mask enters the drop area bounds. This indicates to the user that the mask is within the drop area.

DragEvent.ACTION_DRAG_EXITED -> {
  binding.maskDropArea.alpha = 1.0f
  draggableItem.visibility = View.VISIBLE
  view.invalidate()
  true
}

This code resets the drop area view opacity to 1.0f and resets the visibility of the mask view to VISIBLE when it exits the drop area bounds. This visually indicates that dragging outside the bounds after dropping doesn't work.

Now, you're ready to let the user drop the mask.

Handling a Drop Operation

When the user releases the mask drag shadow, the system needs to dispatch a drag event with the action type ACTION_DROP to the View with the listener. To implement this, add the following code snippet to your maskDragListener.

DragEvent.ACTION_DROP -> {
  //1
  binding.maskDropArea.alpha = 1.0f
  //2
  if (dragEvent.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
    val draggedData = dragEvent.clipData.getItemAt(0).text
    //TODO : perform any action on the draggedData
  }
  //3
  true
}

The code above performs the following actions on the drag event listener:

  1. Resets the drop area view opacity to 1.0f when the user drops the mask.
  2. Optionally reads the data from ClipData via getClipData().
  3. After the processing completes successfully, return a Boolean true or a Boolean false. ACTION_DRAG_ENDED will return this value when you call getResult().

The user can release the drag shadow on any view, but the system dispatches a drag event only if the drop area has an active drag event listener indicating that it's ready to accept the drop.

Responding to Drag End Events

When the user drops the drag shadow, the system dispatches a drag event to all the registered drag event listeners with an action type of ACTION_DRAG_ENDED.

Receiving ACTION_DRAG_ENDED marks the end of the drag operation.

Setting the View Visibility

Right after dispatching ACTION_DROP, the system needs to dispatch ACTION_DRAG_ENDED. Add the following code snippet to your maskDragListener to set the visibility of the mask view:

DragEvent.ACTION_DRAG_ENDED -> {
  draggableItem.visibility = View.VISIBLE
  view.invalidate()
  true
}

When the drag starts, you set the visibility of the mask view to invisible. When the mask is in its new position, you need to redraw the mask. That's what the code above does.

Next, you'll see how to move the object to its new position when the user drops it.

Moving the Draggable View to a New Position

When the user drops the mask's draggable view onto the drop area, you need to position the mask in the drop location. That's your next goal.

Retrieving the Drop's X and Y Position

You'll start by getting the X and Y position of the drag shadow release location from the drag event. This lets you position the mask in the drop area. To do this, call the following code from within the drag event listener implementation for the ACTION_DROP event:

dragEvent.x
dragEvent.y

To retrieve the x, y coordinates from the drag event object, use the getX() and getY() getter methods. The x, y coordinates from the drag event match the last position of the mask drag shadow before the user dropped it.

Updating the Draggable View's Position

In Android, every view on the canvas starts calculating its position from the left-top corner and ends at the bottom-right corner.

If you use the x, y coordinates from the drag event to position the mask, it will anchor to the left-top corner coordinates.

Instead, you want to anchor the center of the mask to the x, y coordinates of the last touch point location. To achieve this, you need to make the following changes before updating the mask's new position.

Create a reference to the draggable mask view: val draggableItem = dragEvent.localState as View.

To update the draggable mask view's x, y coordinates, first import androidx.constraintlayout.widget.ConstraintLayout. Then, add the following code snippet to your maskDragListener's DragEvent.ACTION_DROP branch:

//1
draggableItem.x = dragEvent.x - (draggableItem.width / 2)
//2
draggableItem.y = dragEvent.y - (draggableItem.height / 2)

//3
val parent = draggableItem.parent as ConstraintLayout
//4
parent.removeView(draggableItem)

//5
val dropArea = view as ConstraintLayout
//6
dropArea.addView(draggableItem)
//7
true

This code aligns the center of the mask with the last touch point before the drop. It also removes the mask from its previous location and adds it to the new location. Going through it step by step, you:

  1. Reposition the mask with the updated x coordinate. Subtract half the width of the mask view from the drag event's x coordinate. Then, assign the difference in value to the x coordinate of draggableItem.
  2. Reposition the mask with the updated y coordinate. Subtract half the height of the mask view from the drag event's y coordinate. Assign the difference in value to the y coordinate of draggableItem.
  3. Take a reference to the mask's parent viewGroup.
  4. Remove the mask from the parent viewGroup.
  5. Take a reference to the new viewGroup, the mask drop area.
  6. Add the mask view to this new viewGroup.
  7. Return a Boolean true to indicate the drop operation succeeded.

At this point, the drag-and-drop operation works, but you have one final improvement to make.

Indicating Whether the Mask Is on the Face

To spice things up, your next step is to let your app indicate whether the mask is on or off the face.

Invoke a new method, checkIfMaskIsOnFace(dragEvent: DragEvent), in the maskDragListener's DragEvent.ACTION_DROP branch, just before returning true:

DragEvent.ACTION_DROP -> {
  ...
  checkIfMaskIsOnFace(dragEvent)
  true
}

In checkIfMaskIsOnFace() you will create a reference to the face view, the Bugdroid mascot, to measure the bounds of the view. If the drag event's x and y coordinates are within the bounds of the face view, then the mask is on the face.

Next, add the following variables in MainActivity.kt:

private val maskOn = "Bingo! Mask On"
private val maskOff = "Mask off"

These will serve as the text values for the toast message that checkIfMaskIsOnFace() will make.

Now it's time to create checkIfMaskIsOnFace(dragEvent: DragEvent). Start by importing android.widget.Toast.

Implement checkIfMaskIsOnFace(dragEvent: DragEvent) in MainActivity.kt to display the appropriate toast message:

private fun checkIfMaskIsOnFace(dragEvent: DragEvent) {
  //1
  val faceXStart = binding.faceArea.x
  val faceYStart = binding.faceArea.y

  //2
  val faceXEnd = faceXStart + binding.faceArea.width
  val faceYEnd = faceYStart + binding.faceArea.height
  //3
  val toastMsg = if (dragEvent.x in faceXStart..faceXEnd && dragEvent.y in faceYStart..faceYEnd){
    maskOn
  } else {
    maskOff
  }
  //4
  Toast.makeText(this, toastMsg, Toast.LENGTH_SHORT).show()
}

Here's what the code above does:

  1. Defines the x, y coordinates of the left-top point of the Bugdroid mascot.
  2. Adds the face's width and height to the left-top point of the face to calculate the bottom end point's x, y coordinates.
  3. Checks whether the mask's drop location is within the face bounds. This lets you set an appropriate toast message.
  4. Displays a toast message indicating whether the mask is on the face or not.

Build and run. You'll see the toast message telling you whether the mask is on or off the face.

Congratulations! You've now completed the Masky app and learned how to use drag and drop in your Android apps.

Where to Go From Here?

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

In this tutorial, you learned how to use the Android drag-and-drop framework to move views and data across views.

Android also supports inter-app drag-and-drop operations. Check the official docs to learn how to take your drag-and-drop skills to the next level.

Want to learn more? Check out our Android Animations video course.

If you have any suggestions or questions, join the discussion below.

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!

Average Rating

5/5

Add a rating for this content

Sign in to add a rating
1 rating

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK