6

I want my app to simulate a swipe touch event (to up/down/left/right) when I click a button, then a TextView will scroll down/up.

I have tried to use Motion Event, but nothing happen after I dispatch 3 Motion Event of ACTION_DOWN, ACTION_MOVE and ACTION_UP respectively.

Is that possible to simulate a swipe event?

public void simulation(View view){
    swipe(Direction.Bot);
}
public enum Direction {
    Top, Bot, Left, Right;
}
protected void swipe(Direction direction) {
    Point size = new Point();
    this.getWindowManager().getDefaultDisplay().getSize(size);
    int height = size.y; // height will be at top of the screen
    int width = size.x; // width will be rightmost location of the screen
    float xStart = size.x-50;
    float xEnd = size.x-50;
    float yStart = size.y-50;
    float yEnd = size.y-50;
    long downTime = SystemClock.uptimeMillis();

    if(direction == Direction.Top || direction == Direction.Bot){
        yStart = ((direction == Direction.Top) ? 50 : (height - 50));
        yEnd = ((direction == Direction.Top) ? (height - 50) : 50);
    }else {
        xStart = ((direction == Direction.Left) ? (width - 50) : 50); // true: xStart = w-10; else: = 10
        xEnd = ((direction == Direction.Left) ? 50 : (width - 50)); // true: xEnd = 10; else: = w-10
    }
    findViewById(R.id.my_id).dispatchTouchEvent(MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),MotionEvent.ACTION_DOWN, xStart/2, yStart/2, 0));
    System.out.println("ACTION_DOWN");
    findViewById(R.id.my_id).dispatchTouchEvent(MotionEvent.obtain(downTime, SystemClock.uptimeMillis() + 500, MotionEvent.ACTION_MOVE, xEnd / 2, yEnd / 2, 0));
    System.out.println("ACTION_MOVE");
    findViewById(R.id.my_id).dispatchTouchEvent(MotionEvent.obtain(downTime, SystemClock.uptimeMillis() + 1000, MotionEvent.ACTION_UP, xEnd / 2, yEnd / 2, 0));
    System.out.println("ACTION_UP");

}
vc25
  • 97
  • 1
  • 7
  • It might be hard to simulate touch events in Android. I would recommend wrapping your TextView within a ScrollView and then use smoothScrollBy when the button is pressed. – Pol Nov 13 '15 at 17:51
  • 1
    Ya I think so, I just think simulating touch will give more "freedom" to how I use it. I have looked through several posts, most of them are talking about simulating swipe in test cases, and other posts say it is impossible to mimic touch in the non-testing scenario, due to security reason. – vc25 Nov 13 '15 at 17:57

3 Answers3

6

I was able to programmatically emulate fling event in scrolling activity demo.

enter image description here

This is an example of an emulating fling event I was trying and it worked.

Blue dotted line is the fling event I have emulated:

enter image description here

 class ScrollingActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_scrolling)
    setSupportActionBar(toolbar)
    fab.setOnClickListener { view ->
        Thread(Runnable {
            try {

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

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

/** * 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)
}
}

Hope it help somebody

Hitesh Sahu
  • 41,955
  • 17
  • 205
  • 154
0

I have wrote extension functions that doesn't require Instrumentation, so they could be used not only in androidTest but also in robolectric tests and even in release build:

fun ViewGroup.performSwipeToLeft(target: View) {
    this.performSwipe(target, distanceX = -this.width * .5f, distanceY = 0f)
}
fun ViewGroup.performSwipeToRight(target: View) {
    this.performSwipe(target, distanceX = +this.width * .5f, distanceY = 0f)
}
fun ViewGroup.performSwipeToTop(target: View) {
    this.performSwipe(target, distanceX = 0f, distanceY = -this.height * .5f)
}
fun ViewGroup.performSwipeToBottom(target: View) {
    this.performSwipe(target, distanceX = 0f, distanceY = +this.width * .5f)
}

fun ViewGroup.performSwipe(target: View, distanceX: Float, distanceY: Float) {
    val parentCoords = intArrayOf(0, 0)
    this.getLocationInWindow(parentCoords)

    val childCoords = intArrayOf(0, 0)
    target.getLocationInWindow(childCoords)

    val initGlobalX = childCoords[0].toFloat() + 1f
    val initGlobalY = childCoords[1].toFloat() + 1f

    val initLocalX = (childCoords[0] - parentCoords[0]).toFloat() + 1f
    val initLocalY = (childCoords[1] - parentCoords[1]).toFloat() + 1f

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

    this.dispatchTouchEvent(
        MotionEvent.obtain(
            downTime,
            eventTime,
            MotionEvent.ACTION_DOWN,
            initGlobalX,
            initGlobalY,
            0
        ).apply {
            setLocation(initLocalX, initLocalY)
            source = InputDevice.SOURCE_TOUCHSCREEN
        }
    )

    val steps = 20
    var i = 0
    while (i in 0..steps) {
        val globalX = initGlobalX + i * distanceX / steps
        val globalY = initGlobalY + i * distanceY / steps
        val localX = initLocalX + i * distanceX / steps
        val localY = initLocalY + i * distanceY / steps
        if (globalX <= 10f || globalY <= 10f) {
            break
        }
        this.dispatchTouchEvent(
            MotionEvent.obtain(
                downTime,
                ++eventTime,
                MotionEvent.ACTION_MOVE,
                globalX,
                globalY,
                0
            ).apply {
                setLocation(localX, localY)
                source = InputDevice.SOURCE_TOUCHSCREEN
            }
        )
        i++
    }

    this.dispatchTouchEvent(
        MotionEvent.obtain(
            downTime,
            ++eventTime,
            MotionEvent.ACTION_UP,
            initGlobalX + i * distanceX,
            initGlobalY + i * distanceY,
            0
        ).apply {
            setLocation(initLocalX + i * distanceX, initLocalY + i * distanceY)
            source = InputDevice.SOURCE_TOUCHSCREEN
        }
    )
}

To use it you need pass your textView as an argument, and parent of textView should be receiver of this functions:

val textView = ...
(textView.parent as ViewGroup).performSwipeToTop(textView)
Oleksandr Albul
  • 1,611
  • 1
  • 23
  • 31
0

With RecyclerView you also wanna take a look at the smoothScollBy function where you can pass an Interpolator like the following.

recyclerView.smoothScrollBy(0, 500, AccelerateDecelerateInterpolator())

Comes close enough to a "swipe scoll"

longi
  • 11,104
  • 10
  • 55
  • 89