Android - RecyclerView Swipe Animation not going away - android

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);

Related

RecyclerView on drop

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")
}
}
}

ItemTouchHelper.SimpleCallBack limit width of swipe

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.

List element with swipe actions - buttons not clickable

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
}
}

Android DataBinding item position in a recyclerView

I have a Fragment with a ViewModel (so Mvvm) and in the xml I have 2 RecyclerView with both having their own Adapter using the same itemList because I want to see the same data in 2 different layout.
Before switching to Mvvm, I could reference the adapter and the recyclerView and could reach "getChildAdapterPosition"
But right now this is quite impossible it seem. I made some custom BindingAdapter to bind my Adapter, a CustomPageSnaperHelper and a scrollListener. but now I can't seem to be able to sync the 2 to show the same item even if i scroll the bottom one or the top one.
Gonna post some code snippet, but not sure which one would help more:
#JvmStatic #BindingAdapter("adapter")
fun <T : RecyclerView.ViewHolder> setRvAdapter(rv: RecyclerView, adapter: RecyclerView.Adapter<T>?) {
if(adapter != null) rv.adapter = adapter
}
#JvmStatic #BindingAdapter("pageSnapper")
fun setPageSnapper(rv: RecyclerView, cs: CustomSnapHelper?){
cs?.attachToRecyclerView(rv)
}
#JvmStatic #BindingAdapter("rvScrollListener")
fun setRvScrollListener(rv: RecyclerView, sl: RecyclerView.OnScrollListener?){
if (sl == null) return
rv.addOnScrollListener(sl)
}
Xml:
<androidx.recyclerview.widget.RecyclerView
adapter="#{vm.OCardAdapter}"
pageSnapper="#{vm.OCardPageSnaper}"
rvScrollListener="#{vm.OCardScrollListener}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/top_tabs"
tools:listItem="#layout/card_layout" />
<androidx.recyclerview.widget.RecyclerView
adapter="#{vm.OCarouselAdapter}"
pageSnapper="#{vm.OCarouselPageSnaper}"
rvScrollListener="#{vm.OCarouselScrollListener}"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:background="#color/colorWhite"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:listItem="#layout/carousel_layout"/>
To be fair I could paste in more of my code, but I don't think it could help more ish.
val scrollListenerCard = object: RecyclerView.OnScrollListener(){
var mScrolled = false
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
}
}
You could create
#InverseBindingAdapter(attribute = "currentPosition")
public static int getCurrentPosition(final RecyclerView rv) {
return ((LinearLayoutManager) rv.getLayoutManager()).findFirstVisibleItemPosition();
}
#BindingAdapter(value = "currentPositionAttributeChanged")
public static void setListener(final RecyclerView rv, final InverseBindingListener l) {
final int prevPos = ((LinearLayoutManager)rv.getLayoutManager()).findFirstVisibleItemPosition();
rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy == 0) {
return;
}
int newPosition = linearLayoutManager.findFirstVisibleItemPosition();
if (previousPosition != newPosition) {
l.onChange();
}
}
});
}
#BindingAdapter("currentPosition")
public void setCurrentPosition(final RecyclerView rv, final int pos) {
// no need to do anything here - setter required by databinding
}
These would give you access to the positions, or you can set these listeners by accessing bindings inside your fragment/activity and update your viewModel from there.
I'm not sure that I understand your problem well but if I had to create two rw-s that are always scrolled to the same position with databindig, than I would give adapter A to scroll listener B as a constructor parameter (and inversely too) and this way I would call the adapter's scrollTo() method when the listener fires.
class MyScrollListener(private val recyclerView: RecyclerView): RecyclerView.OnScrollListener(){
var mScrolled = false
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
this.recyclerView.smoothScrollToPosition(newState)
}
}
val scrollListenerCard = MyScrollListener(recyclerView)
Based on Mirza's answer above, I came up with this in kotlin, which works nicely:
#JvmStatic
#InverseBindingAdapter(attribute = "currentPosition", event = "currentPositionAttributeChanged")
fun getCurrentPosition(rv: RecyclerView): Int {
return (rv.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
#JvmStatic
#BindingAdapter(value = ["currentPositionAttributeChanged"])
fun setListener(rv: RecyclerView, l: InverseBindingListener) {
val layoutManager = (rv.layoutManager as LinearLayoutManager?)!!
var prevPos = layoutManager.findFirstVisibleItemPosition()
rv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy == 0) {
return
}
val newPos = layoutManager.findFirstVisibleItemPosition()
if (prevPos != newPos) {
prevPos = newPos
l.onChange()
}
}
})
}
#JvmStatic
#BindingAdapter("currentPosition")
fun setCurrentPosition(rv: RecyclerView, pos: Int) {
(rv.layoutManager as LinearLayoutManager).scrollToPosition(pos)
}
I use it like this:
ViewModel has:
val currentPos = MutableLiveData<Int>()
And the layout has:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
currentPosition="#={viewModel.currentPos}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager">

Drag Shadow Using Item Touch Helper android

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.

Categories

Resources