ItemTouchhelper Class in android helps only to move the entire view around the recyclerview. Is it possible to make a shadow of a view to be dragged(The original view to be in its place-fixed) using the Item touchHelper class?
I overrode onChildDraw and made my own shadow
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
if (isCurrentlyActive) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
// make shadown
isRotated = true;
}
} else {
// view is going back to orig
if (isRotated) {
// undo shadow
}
}
}
Here's a simple way to achieve shadow on the dragged item (for API >= 21 since it relies on elevation).
Declare an attribute in attrs.xml:
<attr name="draggedItemElevation" format="dimension"/>
Set the attribute value in your theme:
<style name="AppTheme" parent="...">
<item name="draggedItemElevation">2dp</item>
</style>
Use this callback for the ItemTouchHelper:
class DragItemTouchHelperCallback : ItemTouchHelper.Callback() {
// Get elevation in pixels from ?attr/draggedItemElevation.
private val elevation = context.obtainStyledAttributes(
intArrayOf(R.attr.editDraggedItemElevation)).use {
it.getDimensionPixelSize(0, 0).toFloat()
}
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
val view = viewHolder.itemView
view.translationX = dX
view.translationY = dY
if (isCurrentlyActive) {
ViewCompat.setElevation(view, elevation)
}
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
val view = viewHolder.itemView
view.translationX = 0f
view.translationY = 0f
ViewCompat.setElevation(view, 0f)
}
override fun getMovementFlags(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder): Int {
// ...
}
override fun onMove(recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder): Boolean {
// ...
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// ...
}
}
This will temporarily change the dragged item elevation to ?attr/draggedItemElevation, then set it back to 0 when the item is dropped.
So, you just want a "preview" drag with a shadow...?
A DragShadow is easily achieved, when you call startDrag:
view.startDrag(clipData, new View.DragShadowBuilder(v), null, 0); //clipData can be null.
For an excellent tutorial on how to handle Drag & Drop, I recommend the one from Paul Burke. He defines a background, to show the user on which position the view will be placed.
If this does not answer your question, please provide more detail in your question.
Related
I have RecyclerView Drag & Drop feature, but I'd like to do some calculations onDrop. When I put my expensiveFunction() in onMove() it's triggered at every position change until the drag is over. That's a big overkill. Is there a way to trigger function on drag end?
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
private var simpleCallback = object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP.or(ItemTouchHelper.DOWN), 0) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val startPosition = viewHolder.absoluteAdapterPosition
val endPosition = target.absoluteAdapterPosition
Collections.swap(itemList, startPosition, endPosition)
recyclerView.adapter?.notifyItemMoved(startPosition, endPosition)
expensiveFunction()
return true
}
}
You could override onSelectedChanged() which get called when the ViewHolder swiped or dragged by the ItemTouchHelper.
To catch the drop action examine the actionState value to be ItemTouchHelper.ACTION_STATE_IDLE:
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
when (actionState) {
// when the item is dropped
ItemTouchHelper.ACTION_STATE_IDLE -> {
Log.d(TAG, "Item is dropped")
}
}
}
RecyclerView drag & drop works perfectly by attaching ItemTouchHelper to RecyclerView. Like below:
abstract class RecyclerViewDragDetector : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.alpha = 0.5f
}
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.alpha = 1.0f
}
}
I am able to drag & drop items using above code, but the problem is while dragging an item, that position remains blank just like empty. My requirement is to keep a background place holder visible while dragging an item.
I want something like below image.
Any suggestion is really really appreciated.
I am trying to build a swipe option in RecyclerView item. The problem is that, When I swipe to the right the whole item is gone. This is not how I want it to be.
This is the example which I want the swipe action to work. When the user swipes to right after particular threshold I want the swipe width to stop and when the user release, I want it to go back.
Currently the user can swipe to the right more than a particular width as shown in the first gif file. I want to limit that and make sure that the user can only swipe to a certain limit and when the user releases it goes back.
Current Code:
val myCallback = object: ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
super.onChildDraw(c, recyclerView, viewHolder,
dX , dY , actionState, isCurrentlyActive)
c.clipRect(0f, viewHolder.itemView.top.toFloat(),
dX , viewHolder.itemView.bottom.toFloat())
if(dX < c.width / 3)
c.drawColor(Color.GRAY)
else
c.drawColor(Color.RED)
val textMargin = resources.getDimension(R.dimen.text_margin)
.roundToInt()
trashBinIcon.bounds = Rect(
textMargin,
viewHolder.itemView.top + textMargin,
textMargin + trashBinIcon.intrinsicWidth,
viewHolder.itemView.top + trashBinIcon.intrinsicHeight
+ textMargin
)
trashBinIcon.draw(c)
}
}
I tried ItemTouchHelper : Limit swipe width of ItemTouchHelper.SimpleCallBack on RecyclerView solution but didn't work.
I have a list item with three swipe actions which looks like this:
The regular list item and the buttons are two different layouts defined in xml.
To reveal the button actions I use ItemTouchHelper.SimpleCallback. In onChildDraw I tell the item list item's x-axis to be only drawn until it reaches the width of the button controls.
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val foreground = (viewHolder as? NachrichtViewHolder)?.binding?.nachrichtListItem
val background = (viewHolder as? NachrichtViewHolder)?.binding?.background
val x: Float = when {
dX.absoluteValue > background?.measuredWidth?.toFloat() ?: dX -> background?.measuredWidth?.toFloat()
?.unaryMinus() ?: dX
else -> dX
}
getDefaultUIUtil().onDraw(
c,
recyclerView,
foreground,
x,
dY,
actionState,
isCurrentlyActive
)
}
Here is an abbreviated layout file demonstrating the way I built the ui:
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/background"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:clickable="#{backgroundVisible}"
android:focusable="#{backgroundVisible}"
android:focusableInTouchMode="#{backgroundVisible}"
android:elevation="#{backgroundVisible ? 4 : 0}">
<ImageButton
android:id="#+id/actionReply"/>
<ImageButton
android:id="#+id/actionShare"/>
<ImageButton
android:id="#+id/actionDelete"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/nachrichtListItem"
android:elevation="#{backgroundVisible ? 0 : 4}"
android:clickable="#{!backgroundVisible}"
android:focusable="#{!backgroundVisible}"
android:focusableInTouchMode="#{!backgroundVisible}">
<!-- regular list item -->
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
My problem is that the buttons are not clickable.
What I tried so far:
set elevation to bring element on top
set items clickable depending on the visibility state of the buttons
This can be seen in the layout file. I want to define the elements inside xml and not draw them manually if possible.
The problem is solved. ItemTouchHelper.SimpleCallback swallows all your touch events. So you need to register a TouchListener for the buttons. The buttons come in my case from xml. Inspired by this I came up with the following solution:
#SuppressLint("ClickableViewAccessibility")
class NachrichtItemSwipeCallback(private val recyclerView: RecyclerView) :
ItemTouchHelper.SimpleCallback(0, LEFT) {
private val itemTouchHelper: ItemTouchHelper
private var binding: ListItemNachrichtBinding? = null
private var lastSwipedPosition: Int = -1
init {
// Disable animations as they don't work with custom list actions
(this.recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
this.recyclerView.setOnTouchListener { _, touchEvent ->
if (lastSwipedPosition < 0) return#setOnTouchListener false
if (touchEvent.action == MotionEvent.ACTION_DOWN) {
val viewHolder =
this.recyclerView.findViewHolderForAdapterPosition(lastSwipedPosition)
val swipedItem: View = viewHolder?.itemView ?: return#setOnTouchListener false
val rect = Rect()
swipedItem.getGlobalVisibleRect(rect)
val point = Point(touchEvent.rawX.toInt(), touchEvent.rawY.toInt())
if (rect.top < point.y && rect.bottom > point.y) {
// Consume touch event directly
val buttons =
binding?.buttonActionBar?.children
.orEmpty()
.filter { it.isClickable }
.toList()
val consumed = consumeTouchEvents(buttons, point.x, point.y)
if (consumed) {
animateClosing(binding?.nachrichtListItem)
}
return#setOnTouchListener false
}
}
return#setOnTouchListener false
}
this.itemTouchHelper = ItemTouchHelper(this)
this.itemTouchHelper.attachToRecyclerView(this.recyclerView)
}
// Only for drag & drop functionality
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onChildDraw(
canvas: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
binding = (viewHolder as? NachrichtViewHolder)?.binding
val foreground = binding?.nachrichtListItem
val background = binding?.buttonActionBar
val backgroundWidth = background?.measuredWidth?.toFloat()
// only draw until start of action buttons
val x: Float = when {
dX.absoluteValue > backgroundWidth ?: dX -> backgroundWidth?.unaryMinus() ?: dX
else -> dX
}
foreground?.translationX = x
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
this.lastSwipedPosition = viewHolder.adapterPosition
recyclerView.adapter?.notifyItemChanged(this.lastSwipedPosition)
}
private fun animateClosing(
foreground: ConstraintLayout?
) {
foreground ?: return
ObjectAnimator.ofFloat(foreground, "translationX", 0f).apply {
duration = DURATION_ANIMATION
start()
}.doOnEnd { applyUiWorkaround() }
}
// See more at https://stackoverflow.com/a/37342327/3734116
private fun applyUiWorkaround() {
itemTouchHelper.attachToRecyclerView(null)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
private fun consumeTouchEvents(
views: List<View?>,
x: Int,
y: Int
): Boolean {
views.forEach { view: View? ->
val viewRect = Rect()
view?.getGlobalVisibleRect(viewRect)
if (viewRect.contains(x, y)) {
view?.performClick()
return true
}
}
return false
}
companion object {
private const val DURATION_ANIMATION: Long = 250
}
}
I have setup a swipe gesture on the recyclerView item in order to edit it. I've done it like this:
private val swipeHelper = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition //get position which is swipe
if (direction == ItemTouchHelper.RIGHT) {
… // do stuff
}
}
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
val icon: Bitmap
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val itemView = viewHolder.itemView
val height: Float = (itemView.bottom.toFloat() - itemView.top.toFloat())
val width = height / 3
if (dX > 0) {
paint.color = Color.parseColor("#FF9300")
val background = RectF(itemView.left.toFloat(), itemView.top.toFloat(), dX, itemView.bottom.toFloat())
c.drawRect(background, paint)
icon = BitmapFactory.decodeResource(resources, R.drawable.pencil)
val iconDest = RectF(
itemView.left.toFloat() + width,
itemView.top.toFloat() + width,
itemView.left.toFloat() + 2*width,
itemView.bottom.toFloat() - width)
c.drawBitmap(icon, null, iconDest, paint)
}
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
The problem I am facing, and that I cannot figure out how to resolve is that after the swipe, since I am not deleting the element, the animatino won't reset until I completely restart the activity.
Here is an explanatory image:
Has anyone ever dealt with this problem? Any Idea on how to fix it without having to restart the whole activity?
Call notifyItemChanged on you adapter. This will reset the adapter.
You can Do this in your Activity by calling notifyDatasetChanged(); or notifyItemChanged(); :
ItemTouchHelper.SimpleCallback callback = new RecyclerItemTouchHelper(this, 0, ItemTouchHelper.LEFT
, new RecyclerItemTouchHelper.RecyclerItemTouchHelperListener() {
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction, final int position) {
if (direction == ItemTouchHelper.LEFT) {
// do what you want here and after that call the function below:
recyclerAdapter.notifyItemChanged(position);
}
}
});
new ItemTouchHelper(callback).attachToRecyclerView(recyclerView);