pale bone
pale bone

Reputation: 1836

Programmatically trigger a swipe motion event in Android view

I'm trying to programmatically trigger a swipe in a view. There are other questions with answers but they haven't worked for me. I also saw some comments that said programmatic dragging/scrolling was disabled for security reasons, is that true? I'd love to find a definitive answer on this.

I've tried the following method that the poster showed working via a video, so it should work! Is there something I need in my AndroidManifest? I've also tried the scrollBy() API but that doesn't scroll my content, and instead moves the content offscreen.

What's more, I've registered a OnTouchListener with my view and seen that the below code copies the same format of event firing (ACTION_DOWN,ACTION_MOVE, ACTION_UP)that a click+drag, and mouse wheel do, though the click+drag and a mouse wheel works while the programmatic scroll/swipe does not.

        final Handler handler = new Handler();

        handler.post(new Runnable() {
            @Override
            public void run() {
                final MotionEvent event = MotionEvent.obtain(System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_DOWN, 500, 700, 0);
                dispatchTouchEvent(event);
                event.recycle();
            }
        });

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                final MotionEvent event = MotionEvent.obtain(System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_MOVE, 500, 700 ,0);
                dispatchTouchEvent(event);
                event.recycle();
            }
        }, 50);

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                final MotionEvent event = MotionEvent.obtain(System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_MOVE, 500 ,700 + 400, 0);
                dispatchTouchEvent(event);
                event.recycle();
            }
        }, 100);

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                final MotionEvent event = MotionEvent.obtain(System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_UP, 500, 700 + 400, 0);
                dispatchTouchEvent(event);
                event.recycle();
            }
        }, 1000);

Upvotes: 1

Views: 3294

Answers (2)

Hitesh Sahu
Hitesh Sahu

Reputation: 45042

This is my fling method

 /**
 * Simulate touching a specific location and dragging to a new location.
 *
 * @param fromX X coordinate of the initial touch, in screen coordinates
 * @param toX Xcoordinate of the drag destination, in screen coordinates
 * @param fromY X coordinate of the initial touch, in screen coordinates
 * @param toY Y coordinate of the drag destination, in screen coordinates
 * @param stepCount How many move steps to include in the drag
 */
 fun fling(
        fromX: Float, toX: Float, fromY: Float,
        toY: Float, stepCount: Int
    ) {

        val inst = Instrumentation()

        val downTime = SystemClock.uptimeMillis()
        var eventTime = SystemClock.uptimeMillis()

        var y = fromY
        var x = fromX

        val yStep = (toY - fromY) / stepCount
        val xStep = (toX - fromX) / stepCount

        var event = MotionEvent.obtain(
            downTime, eventTime,
            MotionEvent.ACTION_DOWN, fromX, fromY, 0
        )
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
            event.source = InputDevice.SOURCE_TOUCHSCREEN
        }
        inst.sendPointerSync(event)



        for (i in 0 until stepCount) {
            y += yStep
            x += xStep
            eventTime = SystemClock.uptimeMillis()
            event = MotionEvent.obtain(
                downTime, eventTime + stepCount,
                MotionEvent.ACTION_MOVE, x, y, 0
            )
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
                event.source = InputDevice.SOURCE_TOUCHSCREEN
            }
            inst.sendPointerSync(event)
        }

        eventTime = SystemClock.uptimeMillis() + stepCount.toLong() + 2
        event = MotionEvent.obtain(
            downTime, eventTime,
            MotionEvent.ACTION_UP, toX, toY, 0
        )

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
            event.source = InputDevice.SOURCE_TOUCHSCREEN
        }
        inst.sendPointerSync(event)
    }

And this is how I use it

 fab.setOnClickListener { view ->
            Thread(Runnable {
                try {

                    fling(500f ,900f ,530f ,20f, 5);
                   // emulateMptionEvent()

                } catch (e: Exception) {
                }
            }).start()
        }

This is how it works:

Before

enter image description here

After clicking on Fab Button, Blue dotted line is emulated fling path.

enter image description here

Upvotes: 3

Martin Zeitler
Martin Zeitler

Reputation: 76569

With androidx.test.uiautomator this is entirely possible - and this is a real-world scenario. I've wrote this method to test an endless scrolling RecyclerView ...just because I'm too lazy to swipe:

protected void fling(UiObject2 view, int speed, int pause) {
    Assert.assertThat(view, not(equalTo(null)));
    try {
        view.fling(Direction.DOWN, speed);
    } catch (StaleObjectException e) {
        Assert.fail();
    }
    sleep(pause);
}

event automation with adb shell input swipe is also possible ...

in both cases, the event does not originate from the app, which is tested.

and in both cases, is relies on android.permission.INJECT_EVENTS.

on a rooted device, one does not even need adb - see this answer.

Upvotes: 0

Related Questions