1

I have an activity with recycler view inside a linearLayout and I'm trying to detect swipe up and swipe down gestures on this activity. The problem with my current implementation is that if I add swipe listeners to linearLayout they are not triggered and if I add them to recyclerView it scrolls left and right (as it is a horizontal recyclerView) instead of detecting swipes

HERE IS MY CODE:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/linear">
 <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
     tools:context="com.cronlogy.hopstopandroid.activity.PlaceActivity"
        tools:showIn="@layout/activity_place">
    </android.support.v7.widget.RecyclerView>
</LinearLayout>

Swipe listener class:

public class OnSwipeTouchListener implements View.OnTouchListener {

private GestureDetector gestureDetector;

public OnSwipeTouchListener(Context c) {
    gestureDetector = new GestureDetector(c, new GestureListener());
}

public boolean onTouch(final View view, final MotionEvent motionEvent) {
    return gestureDetector.onTouchEvent(motionEvent);
}

private final class GestureListener extends GestureDetector.SimpleOnGestureListener {

    private static final int SWIPE_THRESHOLD = 100;
    private static final int SWIPE_VELOCITY_THRESHOLD = 100;

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

    // Determines the fling velocity and then fires the appropriate swipe event accordingly
    @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();
                    }
                }
            } else {
                if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
                    if (diffY > 0) {
                        onSwipeDown();
                    } else {
                        onSwipeUp();
                    }
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return result;
    }
}

public void onSwipeRight() {
}

public void onSwipeLeft() {
}

public void onSwipeUp() {
}

public void onSwipeDown() {
}

}

This is how I'm calling it in activty

 recyclerView.setOnTouchListener(new OnSwipeTouchListener(this) {
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onSwipeDown() {
            Toast.makeText(PlaceActivity.this, "Down", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onSwipeLeft() {
        }

        @Override
        public void onSwipeUp() {
            Toast.makeText(PlaceActivity.this, "Up", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onSwipeRight() {
        }
    });

So can someone help me, How can I detect swipes without scrolling the item in recyclerView or how to attach scroll listeners to its parent view.

  • Old question, but the only thing you seem to be missing is returning `true` when you consume the event in `onFling`. So, any time you call one of your swipe functions from `onFling`, return true. This tells the OS that you have consumed the user input and it should not try to act on it (e.g. by scrolling the recycler view). At the moment you are returning false every time. – Adi B Dec 20 '19 at 18:19

1 Answers1

2

For you do swipe up on horizontall recycler view, you need a custom RecyclerView like:

class CustomRecyclerView : RecyclerView {

    private lateinit var mDetector: GestureDetectorCompat

    // =================================================================================================================
    // CONSTRUCTORS

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(
        context,
        attrs,
        defStyleAttr
    )

    override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
        if (!mDetector.onTouchEvent(e)) {
            return super.onInterceptTouchEvent(e)
        }
        return onInterceptTouchEvent(e)
    }

    fun initDetector(swippeListener: SwipeListener) {
        mDetector = GestureDetectorCompat(context, object : OnSwipeListener() {
            override fun onSwipe(direction: Direction): Boolean {
                if (direction == Direction.up) {
                    //Do something here (in my case I try to open a new submenu)
                    swippeListener.openSubmenu()
                    return true
                } else if (direction == Direction.down) {
                    //Do something here (in my case I try to close a new submenu)
                    swippeListener.closeSubmenu()
                    return true
                }
                return false
            }
        })
    }
}

SwipeListener (for return the action to my class):

interface SwipeListener {
    fun openSubmenu()
    fun closeSubmenu()
}

And later you need implementation your own OnSwipeListener:

I obtain this OnSwipeListener from:

How to detect swipe direction between left/right and up/down

    open class OnSwipeListener : GestureDetector.SimpleOnGestureListener() {
    override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {

        // Grab two events located on the plane at e1=(x1, y1) and e2=(x2, y2)
        // Let e1 be the initial event
        // e2 can be located at 4 different positions, consider the following diagram
        // (Assume that lines are separated by 90 degrees.)
        //
        //
        //         \ A  /
        //          \  /
        //       D   e1   B
        //          /  \
        //         / C  \
        //
        // So if (x2,y2) falls in region:
        //  A => it's an UP swipe
        //  B => it's a RIGHT swipe
        //  C => it's a DOWN swipe
        //  D => it's a LEFT swipe
        //

        val x1 = e1.x
        val y1 = e1.y

        val x2 = e2.x
        val y2 = e2.y

        val direction = getDirection(x1, y1, x2, y2)
        return onSwipe(direction)
    }

    /** Override this method. The Direction enum will tell you how the user swiped.  */
    open fun onSwipe(direction: Direction): Boolean {
        return false
    }

    /**
     * Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method
     * returns the direction that an arrow pointing from p1 to p2 would have.
     * @param x1 the x position of the first point
     * @param y1 the y position of the first point
     * @param x2 the x position of the second point
     * @param y2 the y position of the second point
     * @return the direction
     */
    fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): Direction {
        val angle = getAngle(x1, y1, x2, y2)
        return Direction.fromAngle(angle)
    }

    /**
     *
     * Finds the angle between two points in the plane (x1,y1) and (x2, y2)
     * The angle is measured with 0/360 being the X-axis to the right, angles
     * increase counter clockwise.
     *
     * @param x1 the x position of the first point
     * @param y1 the y position of the first point
     * @param x2 the x position of the second point
     * @param y2 the y position of the second point
     * @return the angle between two points
     */
    fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double {

        val rad = Math.atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI
        return (rad * 180 / Math.PI + 180) % 360
    }


    enum class Direction {
        up,
        down,
        left,
        right;


        companion object {

            /**
             * Returns a direction given an angle.
             * Directions are defined as follows:
             *
             * Up: [45, 135]
             * Right: [0,45] and [315, 360]
             * Down: [225, 315]
             * Left: [135, 225]
             *
             * @param angle an angle from 0 to 360 - e
             * @return the direction of an angle
             */
            fun fromAngle(angle: Double): Direction {
                return if (inRange(angle, 45f, 135f)) {
                    Direction.up
                } else if (inRange(angle, 0f, 45f) || inRange(angle, 315f, 360f)) {
                    Direction.right
                } else if (inRange(angle, 225f, 315f)) {
                    Direction.down
                } else {
                    Direction.left
                }

            }

            /**
             * @param angle an angle
             * @param init the initial bound
             * @param end the final bound
             * @return returns true if the given angle is in the interval [init, end).
             */
            private fun inRange(angle: Double, init: Float, end: Float): Boolean {
                return angle >= init && angle < end
            }
        }
    }
}
jordiz
  • 279
  • 3
  • 8