SSP
SSP

Reputation: 459

Problem in detecting swipe gesture on recyclerView, MotionEvent e1 is null?

I have a recyclerView along with a tabLayout. when user navigates through tabs, the recyclerView is loaded with new items. What I want now, is add the swipe feature so when the user swipes to right or left on the recyclerView, the app switches between tabs (and reload recyclerView with new Items). I have had a tough time trying to efficiently detect swipe (right/left) gestures on a recyclerView. I don't want to use ViewPager because I want to keep my app light and fast. besides the only feature I want, is only swipe. So here is the last method I have used:

OnSwipeTouchListener onSwipeTouchListener = new OnSwipeTouchListener(getActivity())
    {
        public void onSwipeRight() {
            //update recyclerview with new data
        }

        public void onSwipeLeft(){
            //update recyclerview with new data
        }
    };

b.rcvItems.setOnTouchListener(onSwipeTouchListener);



b.rcvItems.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            if (dy > 0) {
                int threshold = 20;
                int count = adapter.getItemCount();
                LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                int lastVisible = layoutManager.findLastVisibleItemPosition();
                if (lastVisible >= count - threshold) { 
                    //load more items
                }
            }
        }
    });

And here is the class OnSwipeTouchListener within which is the class GestureListener:

public class OnSwipeTouchListener implements View.OnTouchListener {
private final GestureDetector gestureDetector;

protected OnSwipeTouchListener(Context ctx) {
    gestureDetector = new GestureDetector(ctx, new GestureListener());
}

@Override
public boolean onTouch(View v, MotionEvent event) {
    return gestureDetector.onTouchEvent(event);
}

public void onSwipeRight() {
}

public void onSwipeLeft() {
}

public void onSwipeTop() {
}

public void onSwipeBottom() {
}





private final class GestureListener extends GestureDetector.SimpleOnGestureListener {
    private static final int SWIPE_THRESHOLD = 23;
    private static final int SWIPE_VELOCITY_THRESHOLD = 0;

    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        boolean result = false;
        try {
            float diffY = e2.getY() - e1.getY();
            float diffX = e2.getX() - e1.getX();
            if (Math.abs(diffX) > Math.abs(diffY)) {
                if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
                    if (diffX > 0) {
                        onSwipeRight();
                    } else {
                        onSwipeLeft();
                    }
                    result = true;
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return result;
    }
}
}

Now the problem is when the Fragment is loaded, in the first place, the swipe feature doesn't work. But after I scroll the recyclerView, or sometimes after I try to swipe many times, it starts to detect the swipes! And then it goes soft and smooth. I checked and noticed that the first parameter MotionEvent e1 in the method onFling() is null until I first scroll my recyclerView or after many swipe attempts. Still at some launches, the swipe feature works fine even in the very first place! (It's rare but it happens). It's so hacky and I can't even figure out the right behavior. How can I fix the issue?

Side note: Sometime the app freezes and then crashes. And I get the following crash log:

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionMyViewHolder{779d2db position=27 id=-1, oldPos=-1, pLpos:-1 no parent} androidx.recyclerview.widget.RecyclerView{2415051 VFED..... ........ 0,0-1080,2049 #7f09016d app:id/rcvItems}, adapter:ui.adapter.ItemsAdapter@bee10b6, layout:androidx.recyclerview.widget.LinearLayoutManager@38c1b7, context:ui.MainActivity@577a61c
    at androidx.recyclerview.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5972)
    at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6156)
    at androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline(GapWorker.java:288)
    at androidx.recyclerview.widget.GapWorker.flushTaskWithDeadline(GapWorker.java:345)
    at androidx.recyclerview.widget.GapWorker.flushTasksWithDeadline(GapWorker.java:361)
    at androidx.recyclerview.widget.GapWorker.prefetch(GapWorker.java:368)
    at androidx.recyclerview.widget.GapWorker.run(GapWorker.java:399)
    at android.os.Handler.handleCallback(Handler.java:883)
    at android.os.Handler.dispatchMessage(Handler.java:100)
    at android.os.Looper.loop(Looper.java:237)
    at android.app.ActivityThread.main(ActivityThread.java:8167)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:496)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100)

Upvotes: 4

Views: 1832

Answers (3)

Edgar V
Edgar V

Reputation: 77

Based on the tip above (https://stackoverflow.com/a/68815311/2996699) I managed to override the Kotlin non nullable motion events with the Java Nullable annotation:

MyGestureDetector.java

public class MyGestureDetector implements GestureDetector.OnGestureListener {

private final MyGestureDetectorCallBack onScrollDetectAction;

public MyGestureDetector(MyGestureDetectorCallBack onScrollDetectAction) {
    this.onScrollDetectAction = onScrollDetectAction;
}

@Override
public boolean onDown(@NonNull MotionEvent e) { return false; }
@Override
public void onShowPress(@NonNull MotionEvent e) {}
@Override
public boolean onSingleTapUp(@NonNull MotionEvent e) { return false; }

@Override
public boolean onScroll(@Nullable MotionEvent e1, @Nullable MotionEvent e2, float distanceX, float distanceY) {
    if (onScrollDetectAction != null) {
        try {
            onScrollDetectAction.onScrollNullable(e1, e2);
        } catch (Exception ignored) {}
    }
    return false;
}

@Override
public void onLongPress(@NonNull MotionEvent e) {}
@Override
public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) { return false; }}

In Java, we can manipulate the motion events as @Nullable. Overriding onScroll in Kotlin is strict to @NotNull thus a compile error is shown.

MyGestureDetectorCallBack.kt

interface PSINTGestureDetectorCallBack {
    fun onScrollNullable(e1: MotionEvent?, e2: MotionEvent?)
}

And then we just need to define the gesture detector on a SwipeLayout.kt (extending FrameLayout):

private val gestureDetector = GestureDetectorCompat(context, gestureDetectorCallBack)

private val gestureDetectorCallBack = MyGestureDetector(
    object : MyGestureDetectorCallBack {
        override fun onScrollNullable(e1: MotionEvent?, e2: MotionEvent?) {
            //run your code
        }
    }
)

override fun onTouchEvent(event: MotionEvent): Boolean {
    gestureDetector.onTouchEvent(event)
    return false //depending on the requirements
}

Upvotes: 0

Hardik Hirpara
Hardik Hirpara

Reputation: 3026

The main reason is that e1 could be null for the initial movement so put it in try/catch block. And for the try/catch, check for null and return. Check below snippet

if (e1 == null || e2 == null) 
  return false;
try {
...
} catch (Exception e) {}
return false;

Upvotes: 2

mad_lad
mad_lad

Reputation: 702

Make OnSwipeTouchListener.kt :

import android.content.Context
import android.view.GestureDetector
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import kotlin.math.abs
internal open class OnSwipeTouchListener(c: Context?) :
OnTouchListener {
   private val gestureDetector: GestureDetector
   override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
      return gestureDetector.onTouchEvent(motionEvent)
   }
   private inner class GestureListener : SimpleOnGestureListener() {
      private val SWIPE_THRESHOLD: Int = 100
      private val SWIPE_VELOCITY_THRESHOLD: Int = 100
      override fun onDown(e: MotionEvent): Boolean {
         return true
      }
      override fun onSingleTapUp(e: MotionEvent): Boolean {
         onClick()
         return super.onSingleTapUp(e)
      }
      override fun onDoubleTap(e: MotionEvent): Boolean {
         onDoubleClick()
         return super.onDoubleTap(e)
      }
      override fun onLongPress(e: MotionEvent) {
         onLongClick()
         super.onLongPress(e)
      }
      override fun onFling(
         e1: MotionEvent,
         e2: MotionEvent,
         velocityX: Float,
         velocityY: Float
      ): Boolean {
         try {
            val diffY = e2.y - e1.y
            val diffX = e2.x - e1.x
            if (abs(diffX) > abs(diffY)) {
               if (abs(diffX) > SWIPE_THRESHOLD && abs(
                  velocityX
               ) > SWIPE_VELOCITY_THRESHOLD
            ) {
               if (diffX > 0) {
                  onSwipeRight()
               }
               else {
                  onSwipeLeft()
               }
            }
         }
          else {
            if (abs(diffY) > SWIPE_THRESHOLD && abs(
               velocityY
            ) > SWIPE_VELOCITY_THRESHOLD
         ) {
            if (diffY < 0) {
               onSwipeUp()
            }
            else {
                  onSwipeDown()
                  }
               }
               }
         } catch (exception: Exception) {
         exception.printStackTrace()
      }
      return false
      }
   }
   open fun onSwipeRight() {}
   open fun onSwipeLeft() {}
   open fun onSwipeUp() {}
   open fun onSwipeDown() {}
   private fun onClick() {}
   private fun onDoubleClick() {}
   private fun onLongClick() {}
   init {
      gestureDetector = GestureDetector(c, GestureListener())
   }
}

In MainActivity.kt

val recyclerView = findViewById(R.id.recyclerView)
recyclerView.setOnTouchListener(object : OnSwipeTouchListener(this@MainActivity) {
         override fun onSwipeLeft() {
            super.onSwipeLeft()
            Toast.makeText(this@MainActivity, "Swipe Left gesture detected",
            Toast.LENGTH_SHORT)
            .show()
         }
         override fun onSwipeRight() {
            super.onSwipeRight()
            Toast.makeText(
               this@MainActivity,
               "Swipe Right gesture detected",
               Toast.LENGTH_SHORT
            ).show()
         }
         override fun onSwipeUp() {
            super.onSwipeUp()
            Toast.makeText(this@MainActivity, "Swipe up gesture detected", Toast.LENGTH_SHORT)
            .show()
         }
         override fun onSwipeDown() {
            super.onSwipeDown()
            Toast.makeText(this@MainActivity, "Swipe down gesture detected", Toast.LENGTH_SHORT)
            .show()
         }
      })
   }

Upvotes: 2

Related Questions