Reputation: 2318
I have implemented onClick listener to my ViewHolder for my RecyclerView
But when I perform very fast double taps or mouse clicks, it performs the task (opens a seperate fragment in this case) twice or three times.
here is my code
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView tvTitle, tvDescription;
public ViewHolder(View itemView) {
super(itemView);
itemView.setClickable(true);
itemView.setOnClickListener(this);
tvTitle = (TextView) itemView.findViewById(R.id.tv_title);
tvDescription = (TextView) itemView.findViewById(R.id.tv_description);
}
@Override
public void onClick(View v) {
mListener.onClick(FRAGMENT_VIEW, getAdapterPosition()); // open FRAGMENT_VIEW
}
}
Any ideas on how to prevent such behaviour?
Upvotes: 22
Views: 16097
Reputation: 36
I tried putting this inside my recyclerview
android:splitMotionEvents="false"
But that doesn't solve the issue if you double tap on the item.
Since I'm adding my fragments to backstack I implemented it like this:
private void addFragmentToBackstack(Fragment fragment, String tag) {
if (getSupportFragmentManager().executePendingTransactions()) {
// If there are pending transactions, wait for them to be executed
return;
}
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
// Fragment already exists in the back stack, do nothing
return;
}
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fragment_container_view, fragment)
.addToBackStack(tag)
.commit();
}
And if you are not adding a Fragment but showing an Activity when pressing on recyclerview item, then you can solve that by adding launchMode inside AndroidManifest.xml
android:launchMode="singleTask"
Just my two cents, hope it helps someone.
Upvotes: 0
Reputation: 91
I had the same problem but I was using Kotlin and solved it by using Throttling Class with debounce like this:
class ThrottleFirst(
private val interval: Long = 300
){
private var debounceJob: Job? = null
fun debounce(scope: CoroutineScope, operation: suspend ()->Unit){
if (debounceJob == null) {
debounceJob = scope.launch {
operation()
delay(interval)
debounceJob = null
}
}
}
}
And then calling my adapter click listener like this:
wallpaperAdapter = WallpaperAdapter(requireContext()){wallpaper->
throttleFirst.debounce(lifecycleScope){
bottomSheet = DownloadBottomSheet(wallpaper)
bottomSheet?.dialog?.window?.setBackgroundDrawableResource(Color.TRANSPARENT)
getGoogleInterstitialAd {
if (StorageHelper.backgroundExist(wallpaper.id, context)) {
startSetActivity(wallpaper)
} else {
bottomSheet?.show(
requireActivity().supportFragmentManager,
DownloadBottomSheet.TAG
)
}
}
}
}
My Adapter class:
class WallpaperAdapter(
val context: Context,
val listener: (Wallpaper) -> Unit
) : RecyclerView.Adapter<WallpaperAdapter.WallpaperViewHolder>(
)
Upvotes: -1
Reputation: 256
Fast taps (clicks) on RecyclerView can cause two situations-
A single item of RecyclerView clicked multiple times. This can cause the destination fragment to be created multiple times thereby making a single fragment stack up multiple times, disrupting the user's smooth experience.
Multiple items of the RecyclerView being clicked at a time. This can cause the undesired behavior of the app. (Opening multiple fragments again.)
Both situations should be handled for a proper app running experience. To prevent 1st situation you can use the logic that within a definite interval of time if the item is clicked more than once then it should not go for creating a new fragment. Here is the code below-
class ViewHolder extends RecyclerView.ViewHolder{
//Suppose your item is a CardView
private CardView cardView;
private static final long TIME_INTERVAL_GAP=500;
private long lastTimeClicked=System.currentTimeMillis();
public ViewHolder(@NonNull View itemView)
{
cardView=itemView.findViewById(R.id.card_view);
cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long now=System.currentTimeMillis();
//check if cardView is clicked again within the time interval gap
if(now-lastTimeClicked<TIME_INTERVAL_GAP)
return; //no action to perform if it is within the interval gap.
mLastClickTime=now;
//... Here your code to open a new fragment
}
});
}
}
Solution for 2nd scenario- RecyclerView has a method setMotionEventSplittingEnabled(boolean split)
The documentation says-
Enable or disable the splitting of MotionEvents to multiple children during touch event dispatch. This behavior is enabled by default for applications that target an SDK version of HONEYCOMB or newer.
When this option is enabled MotionEvents may be split and dispatched to different child views depending on where each pointer initially went down. This allows for user interactions such as scrolling two panes of content independently, chording of buttons and performing independent gestures on different pieces of content. Split set to true to allow MotionEvents to be split and dispatched to multiple child views. and set it to false to only allow one child view to be the target of.
So in your code just add a line of code-
recyclerView.setMotionEventSplittingEnabled(false);
These will surely solve the problem caused due to fast taps on the RecyclerView and prevent your app to stack up the same fragment unnecessarily.
Upvotes: 1
Reputation: 71
Too late but it can work for other people:
recyclerAdapter.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int id = ....
if(id == 1){
view.setClickable(false); //add this
Intent a = new Intent...
startActivity(a);
}else if(id == 2){
view.setClickable(false);
Intent b = ...
startActivity(b);
}
}
});
Fragment - onResume()
@Override
public void onResume() {
super.onResume();
Objects.requireNonNull(getActivity()).invalidateOptionsMenu();
recyclerView.setAdapter(recyclerAdapter); //add this
}
It works for me, i don't know if it's correct.
Upvotes: 0
Reputation: 95
I know this is late and the answer has already been given, but I found that this similar issue for my case was due to a third party library Material Ripple Layout. By default it enables a delay call to onClick and allows for multiple request to be made to onClick so when the animation finishes all those clicks get registered at once and opens multiple dialogs.
this setting cancels the delay and fixed the problem for me in my case.
app:mrl_rippleDelayClick="false"
Upvotes: 1
Reputation: 1677
boolean canStart = true;
ViewHolder.dataText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (canStart) {
canStart = false; // do canStart false
// Whatever you want to do and not have run twice due to double tap
}
}
}
public void setCanStart(boolean can){
canStart = can;
}
@Override
public void onResume() {
super.onResume();
mAdapter.setCanStart(true);
}
Hope it will help :)
Upvotes: 1
Reputation: 1129
You can make class implementing View.OnClickListener
public class DoubleClickHelper implements View.OnClickListener {
private long mLastClickTime = System.currentTimeMillis();
private static final long CLICK_TIME_INTERVAL = 300;
private Callback callback;
public DoubleClickHelper(Callback callback) {
this.callback = callback;
}
@Override
public void onClick(View v) {
long now = System.currentTimeMillis();
if (now - mLastClickTime < CLICK_TIME_INTERVAL) {
return;
}
mLastClickTime = now;
callback.handleClick();
}
public interface Callback {
void handleClick();
}
}
And than use it like:
ivProduct.setOnClickListener(new DoubleClickHelper(() -> listener.onProductInfoClick(wItem)));
Upvotes: 0
Reputation: 8461
I repurposed the DebouncingOnClickListener from Butterknife to debounce clicks within a specified time, in addition to preventing clicks on multiple views.
To use, extend it and implement doOnClick
.
DebouncingOnClickListener.kt
import android.view.View
/**
* A [click listener][View.OnClickListener] that debounces multiple clicks posted in the
* same frame and within a time frame. A click on one view disables all view for that frame and time
* span.
*/
abstract class DebouncingOnClickListener : View.OnClickListener {
final override fun onClick(v: View) {
if (enabled && debounced) {
enabled = false
lastClickTime = System.currentTimeMillis()
v.post(ENABLE_AGAIN)
doClick(v)
}
}
abstract fun doClick(v: View)
companion object {
private const val DEBOUNCE_TIME_MS: Long = 1000
private var lastClickTime = 0L // initially zero so first click isn't debounced
internal var enabled = true
internal val debounced: Boolean
get() = System.currentTimeMillis() - lastClickTime > DEBOUNCE_TIME_MS
private val ENABLE_AGAIN = { enabled = true }
}
}
Upvotes: 0
Reputation: 1443
If you are using Kotlin you can go with this based on Money's answer
class CodeThrottle {
companion object {
const val MIN_INTERVAL = 300
}
private var lastEventTime = System.currentTimeMillis()
fun throttle(code: () -> Unit) {
val eventTime = System.currentTimeMillis()
if (eventTime - lastEventTime > MIN_INTERVAL) {
lastEventTime = eventTime
code()
}
}
}
Create this object in your view holder
private val codeThrottle = CodeThrottle()
Then do the following in your bind
name.setOnClickListener { codeThrottle.throttle { listener.onCustomerClicked(customer, false) } }
Putting whatever code you need called in place of
listener.onCustomerClicked(customer, false)
Upvotes: 6
Reputation: 8281
Add below attributes in your theme
<item name="android:splitMotionEvents">false</item>
<item name="android:windowEnableSplitTouch">false</item>
This will prevent multiple tap at same time.
Upvotes: 3
Reputation: 6892
The most straightforward approach here would be using setMotionEventSplittingEnabled(false)
in your RecyclerView
.
By default, this is set to true in RecyclerView
, allowing multiple touches to be processed.
When set to false, this ViewGroup
method prevents the RecyclerView
children to receive the multiple clicks, only processing the first one.
See more about this here.
Upvotes: 28
Reputation: 346
You can modify it like this.
public class ViewHolder extends RecyclerView.ViewHolder implements
View.OnClickListener {
TextView tvTitle, tvDescription;
private long mLastClickTime = System.currentTimeMillis();
private static final long CLICK_TIME_INTERVAL = 300;
public ViewHolder(View itemView) {
super(itemView);
itemView.setClickable(true);
itemView.setOnClickListener(this);
tvTitle = (TextView) itemView.findViewById(R.id.tv_title);
tvDescription = (TextView) itemView
.findViewById(R.id.tv_description);
}
@Override
public void onClick(View v) {
long now = System.currentTimeMillis();
if (now - mLastClickTime < CLICK_TIME_INTERVAL) {
return;
}
mLastClickTime = now;
mListener.onClick(FRAGMENT_VIEW, getAdapterPosition()); // open
// FRAGMENT_VIEW
}
}
Upvotes: 31
Reputation: 1362
This is a very annoying behavior. I have to use an extra flag to prevent this in my work.
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView tvTitle, tvDescription;
private boolean clicked;
public ViewHolder(View itemView) {
super(itemView);
itemView.setClickable(true);
itemView.setOnClickListener(this);
tvTitle = (TextView) itemView.findViewById(R.id.tv_title);
tvDescription = (TextView) itemView.findViewById(R.id.tv_description);
}
@Override
public void onClick(View v) {
if(clicked){
return;
}
clicked = true;
v.postDelay(new Runnable(){
@Override
public void run(View v){
clicked = false;
}
},500);
mListener.onClick(FRAGMENT_VIEW, getAdapterPosition()); // open FRAGMENT_VIEW
}
}
Upvotes: 7