I am trying to add functionality of swipe to delete as well as to show bottom sheet pop-up if RecyclerView item is long pressed. I am using ItemTouchHelper.SimpleCallback for swipe to delete and ItemTouchListener for showing pop-up on long press of item.
Problem is that when I am swiping the item to delete its also detecting long press.
What I want is that it should ignore long press when item is being swiped.
I have ItemTouchHelper class which extends Simplecallback for swipe to delete. Following is the code to attach recyclerview for swipe to delete.
ItemTouchHelper.SimpleCallback itemTouchHelperCallback = new RecyclerItemTouchHelper(0, ItemTouchHelper.LEFT, this);
new ItemTouchHelper(itemTouchHelperCallback).attachToRecyclerView(recyclerView);
Follwing is code to add listener for long click event.
recyclerView.addOnItemTouchListener(new NotesRecyclerTouchListener(getApplicationContext(), recyclerView, new NotesRecyclerTouchListener.ClickListener() {
#Override
public void onLongClick(View view, int position) {
Note note = notesList.get(position);
Toast.makeText(getApplicationContext(), note.getTitle() + " is log pressed!", Toast.LENGTH_SHORT).show();
View sheetView = MainActivity.this.getLayoutInflater().inflate(R.layout.view_bottom_sheet_dialog, null);
BottomSheetDialog dialog = new BottomSheetDialog(MainActivity.this);
dialog.setContentView(sheetView);
dialog.show();
}
}));
onSwiped will be called when item will be completely swiped. If you will start to swipe element but then move it back, the method will not be called. So you shouldn't use this method to mark the end of swiping.
You can use isCurrentlyActive from onChildDraw like this:
var swiping: Boolean = false
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)
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
swiping = isCurrentlyActive
}
}
It will be true when you move element and false after swipe or cancelation of swipe.
As #DavidVelasquez has suggested you should set up a flag when swipe begins and act depending on it's state in your onLongClick() But onSwiped() is not the way to go. Instead you should use ItemTouchHelper.SimpleCallback#onChildDraw() method to detect when the swipe beings and onSwiped() method to detect when it ends.
Eg.
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int,isCurrentlyActive: Boolean) {
if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE){
setupMyFlag()
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
clearMyFlag()
}
And then just check this flag in your onLongClick()
Related
I am currently trying to implement a RecyclerView list with drag and drop reordering. For this I use the ItemTouchHelper.SimpleCallback
class SoftkeyScreenListReorderHelperCallback(
private val adapter: SoftkeyScreenListAdapter
) : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.START or ItemTouchHelper.END, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return adapter.itemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
}
My Adapter got the itemMoved() method, which is called in the onMove() method in the callback. Here I just swap the items and notify the adapter about the change.
fun itemMoved(fromPosition: Int, toPosition: Int): Boolean {
Collections.swap(list, fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
return true
}
For my RecyclerView I implemented the following
binding.recyclerview.apply {
[...] // adapter init
myAdapter.setHasStableIds(true)
adapter = myAdapter
val touchHelper = ItemTouchHelper(SoftkeyScreenListReorderHelperCallback(adapter as SoftkeyScreenListAdapter))
touchHelper?.attachToRecyclerView(this)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
setHasFixedSize(true)
}
It works, but I always get flickering for the items below (after) the new item position. Assume I have 5 Items {1,2,3,4,5} and want to swap 1 with 3, then 4 and 5 are flickering. 1, 2 and 3 don't.
I already set the recyclerview size fixed, enabled stable ids and disabled animations, but it does not help. Does anyone has a clue what could be the reason for that and how to fix?
try this
recyclerView.getItemAnimator().setChangeDuration(0);
Possibly, you are getting or loading your data from a place or using a library that requires some time. If that is the case, this answer could help
Try this:
After adapter initialization:
adapter.setHasStableIds(true);
In adapter class:
#Override
public long getItemId(int position) {
return itemList.get(position).hashCode();
}
After swiping an item in RecyclerView I want it to go back without swiping it back manually.
Here is an swipeable item in RecyclerView.
Item in RecyclerView
Swiping...
After swipe event I want this item to go back, as if it was swiped not far enough, but event must happen. How can I do this?
After swipe
Here is my SwipeHelper, which keeps background static:
abstract class ProfileSwipeHelper : ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return true
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (viewHolder != null) {
ItemTouchHelper.Callback.getDefaultUIUtil().onSelected((viewHolder as ProfilesAdapter.ViewHolder).foreground)
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
getDefaultUIUtil().onDraw(c, recyclerView,
(viewHolder as ProfilesAdapter.ViewHolder).foreground, dX, dY,
actionState, isCurrentlyActive)
}
override fun onChildDrawOver(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder?,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
getDefaultUIUtil().onDrawOver(
c, recyclerView,
(viewHolder as ProfilesAdapter.ViewHolder).foreground, dX, dY,
actionState, isCurrentlyActive)
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
getDefaultUIUtil().clearView((viewHolder as ProfilesAdapter.ViewHolder).foreground)
}
}
And here onSwiped event in main activity, only with Toast:
//Main Activity
val context : Context = this
val deleteSwipeHandler1 = object : ProfileSwipeHelper() {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
Toast.makeText(context, "swiped", Toast.LENGTH_SHORT).show()
}
}
ItemTouchHelper(deleteSwipeHandler1).attachToRecyclerView(rv_profiles)
You can use Multiswipe library. Read complete explanations here. If you want to use Java read this. Below is
a concise explanation without using complete options of this library:
add jitpack and Multiswipe library to your project:
in settings.gradle or root build.gradle:
repositories {
//...
maven { url 'https://jitpack.io' }
}
in app's build.gradle:
dependencies {
implementation 'com.github.ygngy:multiswipe:1.2.1'
}
implement MultiSwipe in ViewHolder:
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import com.github.ygngy.multiswipe.MultiSwipe
import com.github.ygngy.multiswipe.LeftSwipeList
import com.github.ygngy.multiswipe.RightSwipeList
class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view), MultiSwipe {
var mLeftSwipeList: LeftSwipeList? = null
var mRightSwipeList: RightSwipeList? = null
// todo other ViewHolder codes...
fun bind() {
// Each swipe contains of at least an id and an icon
val likeSwipe = Swipe(
context = context, // context used to extract default colors and margins from resources
id = SWIPE_TO_LIKE_ID, // swipe id will be sent to onSwipeDone when swipe is completed
activeIcon = getDrawable(R.drawable.ic_like_24)!!, // swipe icon
activeLabel = getString(R.string.like), // OPTIONAL swipe label
acceptIcon = getDrawable(R.drawable.ic_like_accept_24)!!,// OPTIONAL icon used when swipe displacement is greater than "accept boundary"
acceptLabel = getString(R.string.like_accept),// OPTIONAL label used when swipe swipe displacement is greater than "accept boundary"
inactiveIcon = getDrawable(R.drawable.ic_disabled_like_24)!!// OPTIONAL icon used when this swipe could be next swipe
)
// Create other swipes (shareSwipe, copySwipe, ...) in a similar way.
// If row has left swipes, create left swipe list in the desired order like below:
mLeftSwipeList = LeftSwipeList (shareSwipe, copySwipe, cutSwipe)
// If row has right swipes, create right swipe list in the desired order like below:
mRightSwipeList = RightSwipeList (likeSwipe, editSwipe, delSwipe)
}
// Don't recreate swipes or any object here
override val leftSwipeList: LeftSwipeList?
get() = mLeftSwipeList
// Don't recreate swipes or any object here
override val rightSwipeList: RightSwipeList?
get() = mRightSwipeList
// Here handle swipe event and/or return some data to MultiSwipeListener
override fun onSwipeDone(swipeId: Int): Any? {
// Instead you may choose to only return data
// from this method to consume event at Activity or Fragment
when(swipeId) {
SWIPE_TO_SHARE_ID -> {
// todo share
}
SWIPE_TO_COPY_ID -> {
// todo copy
}
//...
}
return MyData()// return any data to Activity or Fragment
}
}
attach library to recyclerview at activity or fragment:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.github.ygngy.multiswipe.MultiSwipeListener
import com.github.ygngy.multiswipe.SwipeDirection
import com.github.ygngy.multiswipe.multiSwiping // importing extension function
class DemoActivity : AppCompatActivity() {
// todo other activity codes...
override fun onCreate(savedInstanceState: Bundle?) {
// todo other onCreate codes...
// attaching Multiswipe to RecycerView
recyclerView.multiSwiping(
swipeThreshold = 0.5f, // OPTIONAL, the fraction of view for complete swipe threshold
object: MultiSwipeListener { // OPTIONAL listener
// This method is called after onSwipeDone of ViewHolder
// and data is the returned value of onSwipeDone of ViewHolder
override fun onSwipeDone(swipeId: Int, data: Any?) {
// data is the return value of "ViewHolder.onSwipeDone"
// cast to data you returned from "ViewHolder.onSwipeDone"
val myData = data as MyData?
when(swipeId) {
SWIPE_TO_SHARE_ID -> shareItem(myData)
SWIPE_TO_COPY_ID -> copyItem(myData)
//...
}
}
/***
This method will be called when direction changes in each swipe.
This method could be used to hide on screen widgets such as FABs.
direction may be:
- START (when user opens start side of view),
- END (when user opens end side of view),
- NONE (when swipe is closing without user interaction)
***/
override fun swiping(direction: SwipeDirection, swipeListSize: Int) {
// here i hide FAB when user is swiping end side actively
if (direction == SwipeDirection.END) fab.hide() else fab.show()
}
})
}
}
My vertical RecyclerView (looking like a simple list) in Kotlin scrolls neatly to the next element when I press a button. 👍🏻
However when the next element is off screen (for example because in the meantime the user scrolled to a different position with a gesture) the problem is that this doesn't work anymore. Instead, it automatically scrolls all the way to the bottom of the RecyclerView.
Any idea how to fix that? I'd appreciate it!
Thanks in advance for your efforts.
I'm overriding SmoothScrollLinearLayoutManager:
class SmoothScrollLinearLayoutManager(private val mContext: Context, orientation: Int, reverseLayout: Boolean) : LinearLayoutManager(mContext, orientation, reverseLayout) {
override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State?,
position: Int) {
val smoothScroller = object : TopSnappedSmoothScroller(recyclerView.context) {
//This controls the direction in which smoothScroll looks for your view
override fun computeScrollVectorForPosition(targetPosition: Int): PointF {
return PointF(0f, 1f)
}
//This returns the milliseconds it takes to scroll one pixel.
protected override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
}
}
smoothScroller.targetPosition = position
Log.i("Target", smoothScroller.targetPosition.toString())
startSmoothScroll(smoothScroller)
}
private open inner class TopSnappedSmoothScroller(context: Context) : LinearSmoothScroller(context) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
return this#SmoothScrollLinearLayoutManager
.computeScrollVectorForPosition(targetPosition)
}
override fun getVerticalSnapPreference(): Int {
return LinearSmoothScroller.SNAP_TO_START
}
}
private open inner class CenterSnappedSmoothScroller(context: Context) : LinearSmoothScroller(context) {
override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
return this#SmoothScrollLinearLayoutManager
.computeScrollVectorForPosition(targetPosition)
}
override fun getVerticalSnapPreference(): Int {
return LinearSmoothScroller.SNAP_TO_END
}
}
// Scrolling speed
companion object {
private val MILLISECONDS_PER_INCH = 110f
}
}
The button triggers this function to scroll to the next element in the list (RecyclerView):
private fun fastForwardTapped() {
// Update selected position in RecyclerView
selectedPosition += 1
recyclerView.smoothScrollToPosition(selectedPosition)
}
It had to do with how I overrode LinearLayoutManager.
I ended up using this solution:
https://stackoverflow.com/a/36784136/8400139
It works.
I am trying to hide a RelativeLayout when I scroll up and show it when I scroll down. onScroll works fine and is invoked every time until View is set to GONE.
final RelativeLayout placeHeaderMain = findViewById(R.id.place_header_main);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
// Scrolling up
placeHeaderMain.setVisibility(View.GONE);
} else {
// Scrolling down
placeHeaderMain.setVisibility(View.VISIBLE);
}
}
});
I want my listener to continue working after setting the View to Gone in order to make it Visible when scrolled down.
Thanks in advance.
Are there enough items to be scrolled?
That code above won't be triggered if dy == 0. It could be not enough items to make the scroll and it will return dy equal to 0, father more it won't to call onScroll(...)
What dy do you have when RelativeLayout has hidden?
Try to check that method below:
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
Try to set the view to INVISIBLE and not to GONE. when you set any view to View.GONE he is invisible and it doesn't take any space inside your layout , but when you set a view to View.INVISIBLE like before he will be invisible but unlike View.GONE your view still takes up space inside the layout.
I want to add animations to the itemView, however they only run if the item is currently moving. For example the item starts to fadeOut until the alpha is 0, but if the item stops moving, the animation pauses and the item only fades out halfway. After that, the item needs to start moving again for the animation to complete.
I know onDraw typically only gets called when something changes, but I guess the OS doesn't realize the style of the View is changed as well, so it doesn't call it.
I tried adding invalidate() at the end so I can force an update but it doesn't do anything.
How can I fix this?
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (actionState == ACTION_STATE_SWIPE) {
if (optionsState == OptionState.UPVOTE) {
drawUpvoteOption(c, viewHolder.itemView, dX);
} else if (optionsState == OptionState.DOWNVOTE) {
if (!isRightSwipeMaxed(dX)) drawDownvoteOption(c, viewHolder.itemView, dX);
else drawDownvoteOption(c, viewHolder.itemView, MAX_RIGHT_SWIPE_DX);
}
setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
if (dX == 0) {
//This only works if the item is moving
viewHolder.itemView.animate().alpha(1f);
optionsState = OptionState.NONE;
}
if (!isRightSwipeMaxed(dX)) {
viewHolder.itemView.setTranslationX(dX);
} else {
viewHolder.itemView.setTranslationX(MAX_RIGHT_SWIPE_DX);
//This only works if the item is moving
viewHolder.itemView.animate().alpha(0);
}
//Doesn't do anything
viewHolder.itemView.invalidate();
}
I had the same issue, invalidate() on the itemView did nothing.
You have to do the invalidate() on the parent recyclerView for that to work :
viewHolder.animatedRecyclerView.invalidate()