This commit is contained in:
Adrian Kuta 2024-06-26 00:22:44 +02:00
parent 5a41df0b16
commit 6f93d0c234
6 changed files with 347 additions and 0 deletions

1
build-logic/convention/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

1
dragdrop/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

56
dragdrop/build.gradle.kts Normal file
View File

@ -0,0 +1,56 @@
import com.vanniktech.maven.publish.SonatypeHost
plugins {
alias(libs.plugins.convention.android.library)
alias(libs.plugins.convention.android.library.publish)
}
android {
namespace = "dev.adriankuta.unbounddragdrop"
version = "0.0.2"
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
mavenPublishing {
coordinates("dev.adriankuta", "unbound-drag-drop", version.toString())
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
signAllPublications()
pom {
name = "Unbound Drag & Drop"
description =
"Unbound Drag & Drop enhances your Android apps by enabling drag and drop across multiple RecyclerViews, unlike the default single RecyclerView restriction. This feature allows users to seamlessly move items between different RecyclerViews, offering a more flexible and intuitive user experience."
inceptionYear = "2024"
url = "https://github.com/AdrianKuta/Unbound-Drag-Drop"
licenses {
license {
name.set("MIT License")
url.set("https://github.com/AdrianKuta/Unbound-Drag-Drop/blob/master/LICENSE")
distribution.set("repo")
}
}
developers {
developer {
id = "AdrianKuta"
name = "Adrian Kuta"
url = "https://adriankuta.dev/"
}
}
scm {
url = "https://github.com/AdrianKuta/Unbound-Drag-Drop"
connection = "scm:git:git://github.com/AdrianKuta/Unbound-Drag-Drop.git"
developerConnection = "scm:git:ssh://git@github.com/AdrianKuta/Unbound-Drag-Drop.git"
}
}
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

View File

@ -0,0 +1,136 @@
package dev.adriankuta.unbounddragdrop
import android.content.ClipData
import android.view.HapticFeedbackConstants
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.DRAG_FLAG_OPAQUE
import androidx.recyclerview.widget.RecyclerView.ViewHolder
/**
* Helper class to handle drag and drop functionality in a RecyclerView.
*
* @param callback A Callback object to handle the drag and drop events.
*/
class DragDropHelper(callback: Callback) :
RecyclerView.OnChildAttachStateChangeListener {
private var mRecyclerView: RecyclerView? = null
private var recyclerItemClickListener: RecyclerItemClickListener? = null
private val dropListener = DropListener(callback)
private val onItemLongClickListener: RecyclerItemClickListener.OnItemLongClickListener by lazy {
object : RecyclerItemClickListener.OnItemLongClickListener {
/**
* Called when an item is long-clicked. Starts the drag operation.
*
* @param view The view that was long-clicked.
* @param position The position of the item in the adapter.
*/
override fun onItemLongClick(view: View, position: Int) {
val data = ClipData.newPlainText("", "")
val shadowBuilder = View.DragShadowBuilder(view)
view.startDragAndDrop(data, shadowBuilder, view, DRAG_FLAG_OPAQUE)
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
}
}
/**
* Attaches the DragDropHelper to the specified RecyclerView.
*
* @param recyclerView The RecyclerView to attach to.
*/
fun attachToRecyclerView(recyclerView: RecyclerView?) {
if (mRecyclerView === recyclerView) {
return // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks()
}
mRecyclerView = recyclerView
mRecyclerView?.let {
setupCallbacks()
}
}
/**
* Sets up the necessary callbacks for the RecyclerView.
*/
private fun setupCallbacks() {
mRecyclerView?.apply {
recyclerItemClickListener = RecyclerItemClickListener(
context,
this,
onItemLongClickListener
).also {
mRecyclerView?.addOnItemTouchListener(it)
}
addOnChildAttachStateChangeListener(this@DragDropHelper)
setOnDragListener(dropListener)
}
}
/**
* Removes the callbacks from the RecyclerView.
*/
private fun destroyCallbacks() {
recyclerItemClickListener?.let {
mRecyclerView?.removeOnItemTouchListener(it)
}
mRecyclerView?.removeOnChildAttachStateChangeListener(this)
mRecyclerView?.setOnDragListener(null)
}
/**
* Called when a child view is attached to the RecyclerView.
*
* @param view The child view that was attached.
*/
override fun onChildViewAttachedToWindow(view: View) {
view.setOnDragListener(dropListener)
}
/**
* Called when a child view is detached from the RecyclerView.
*
* @param view The child view that was detached.
*/
override fun onChildViewDetachedFromWindow(view: View) {
view.setOnDragListener(null)
}
/**
* Abstract class to handle drag and drop events.
*/
abstract class Callback {
/**
* Called when an item is moved within or between RecyclerViews.
*
* @param recyclerView The RecyclerView containing the dragged item.
* @param viewHolder The ViewHolder of the dragged item.
* @param targetRecyclerView The RecyclerView where the item is dropped.
* @param targetViewHolder The ViewHolder of the target position.
* @return True if the move was handled, false otherwise.
*/
abstract fun onMove(
recyclerView: RecyclerView,
viewHolder: ViewHolder,
targetRecyclerView: RecyclerView,
targetViewHolder: ViewHolder?
): Boolean
/**
* Called when an item has been dropped.
*
* @param recyclerView The RecyclerView containing the dragged item.
* @param viewHolder The ViewHolder of the dragged item.
* @param targetRecyclerView The RecyclerView where the item is dropped.
* @param targetViewHolder The ViewHolder of the target position.
*/
abstract fun onMoved(
recyclerView: RecyclerView,
viewHolder: ViewHolder,
targetRecyclerView: RecyclerView,
targetViewHolder: ViewHolder?
)
}
}

View File

@ -0,0 +1,66 @@
package dev.adriankuta.unbounddragdrop
import android.view.DragEvent
import android.view.View
import android.view.View.OnDragListener
import androidx.recyclerview.widget.RecyclerView
internal class DropListener(private val callback: DragDropHelper.Callback) : OnDragListener {
/**
* Handles drag events on the target view.
*
* @param targetView The view that is being dragged over or dropped onto.
* @param event The drag event.
* @return True if the event was handled, false otherwise.
*/
override fun onDrag(targetView: View?, event: DragEvent?): Boolean {
targetView ?: return false
event?.let {
if (it.action == DragEvent.ACTION_DROP) {
val sourceView = it.localState as View
val sourceRecyclerView = sourceView.parent as RecyclerView
val sourcePosition = sourceRecyclerView.getChildAdapterPosition(sourceView)
val sourceViewHolder =
sourceRecyclerView.findViewHolderForAdapterPosition(sourcePosition)
?: return false
val targetRecyclerView = getRecyclerView(targetView) ?: return false
val targetAdapter = targetRecyclerView.adapter
val targetPosition = if (targetView is RecyclerView) {
targetAdapter?.itemCount ?: 0
} else {
targetRecyclerView.getChildAdapterPosition(targetView)
}
val targetViewHolder =
targetRecyclerView.findViewHolderForAdapterPosition(targetPosition)
if (callback.onMove(
sourceRecyclerView,
sourceViewHolder,
targetRecyclerView,
targetViewHolder
)
) {
callback.onMoved(
sourceRecyclerView,
sourceViewHolder,
targetRecyclerView,
targetViewHolder
)
}
}
}
return true
}
/**
* Retrieves the RecyclerView associated with the given view.
*
* @param view The view to check.
* @return The RecyclerView if found, null otherwise.
*/
private fun getRecyclerView(view: View): RecyclerView? {
return view as? RecyclerView ?: view.parent as? RecyclerView
}
}

View File

@ -0,0 +1,87 @@
package dev.adriankuta.unbounddragdrop
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.recyclerview.widget.RecyclerView
internal class RecyclerItemClickListener(
context: Context,
private val recyclerView: RecyclerView,
private val listener: OnItemLongClickListener?
) : RecyclerView.OnItemTouchListener {
private val gestureDetector: GestureDetector =
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
/**
* Called when a long press gesture is detected.
*
* @param e The motion event triggering the long press.
*/
override fun onLongPress(e: MotionEvent) {
val childView = recyclerView.findChildViewUnder(e.x, e.y)
if (childView != null && listener != null) {
listener.onItemLongClick(
childView,
recyclerView.getChildAdapterPosition(childView)
)
}
}
/**
* Called when a single tap up gesture is detected.
*
* @param e The motion event triggering the single tap.
* @return True to indicate the event is handled.
*/
override fun onSingleTapUp(e: MotionEvent): Boolean {
return true
}
})
/**
* Interface definition for a callback to be invoked when an item in this
* RecyclerView has been long clicked.
*/
interface OnItemLongClickListener {
/**
* Called when an item has been long clicked.
*
* @param view The view that was clicked.
* @param position The position of the view in the adapter.
*/
fun onItemLongClick(view: View, position: Int)
}
/**
* Intercept touch events to determine if a long press has occurred.
*
* @param rv The RecyclerView.
* @param e The motion event.
* @return True if the event is intercepted, false otherwise.
*/
override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
val childView = rv.findChildViewUnder(e.x, e.y)
return childView != null && childView.isLongClickable && gestureDetector.onTouchEvent(e)
}
/**
* Handle touch events (not needed for this implementation).
*
* @param rv The RecyclerView.
* @param e The motion event.
*/
override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {
// Not needed
}
/**
* Request to disallow intercepting touch events (not needed for this implementation).
*
* @param disallowIntercept True to disallow intercepting touch events.
*/
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
// Not needed
}
}