【Android测试】【随笔】模拟双指点击

时间:2023-12-09 23:04:13

版权声明:本文出自胖喵~的博客,转载必须注明出处。

转载请注明出处:http://www.cnblogs.com/by-dream/p/5258660.html

手势


  看到这个标题,很多人会想一想 “双指点击” 的操作是什么样的,首先解释一下吧,为了能清晰明了一点,请看下面的图:

【Android测试】【随笔】模拟双指点击

  左上角的Tap代表点击操作,也就是我们说的 “单指单击”;右上角是Double Tap顾名思义,使用一个手指完成 “双击” 的动作;左下角的Scroll代表的是用一个手指完成 “滑动”的动作;最后看右下角这张图,这个动作就是我们本节要讲的内容,用两个手指完成 “单击” 的动作,注意两个手指点击的实际要同时,同时按下,同时抬起。

什么需求


  为什么会有这样的需求呢?这个需求可能大部分人都没遇到过,就是目前市面上的三大地图(腾讯、百度、高德)的底图都支持了多种缩放效果,其中“双指点击”的操作就是让底图缩小一个层级,如下所示:

【Android测试】【随笔】模拟双指点击

  当然 “双指点击 ”的操作也可以延伸为 “双指滑动 ”的操作,这个可能会在游戏里面用的多吧,当然这个后续有需求了再介绍。

思考


  于是我得思考用什么方式实现呢?上一节 《【Android测试】【随笔】模拟长按电源键》中介绍了一种getevent/sendevent 的方法,那么是否可以通过getevent录制手势,然后用sendevent回放呢?于是我就去尝试了一下,结果失败了。失败的原因是我那两个不够灵活的手指在同时按下的时候总是微微的移动一下,很难模拟出只有按下和抬起的操作,于是回放的时候总是不成功。试了好多次都不行,最终决定尝试其他的方式。抱着试试看的态度,去看了看Uiautomator的源码,结果就找到了要的答案。

Uiautomator源码


  我的Uiautomator系列的博客中,没有向其他工具一样介绍源码分析的部分,原因是实在太忙,没时间去细看,草草了事分享给大家又怕把大家带入误区,刚好这里我就简单的说说。首先要下载的同学可以点击这里下载。

【Android测试】【随笔】模拟双指点击

  下载后我们可以在 ..\uiautomator\core\com\android\uiautomator\core 的路径下看到这些代码。

  通过仔细的寻找,在 InteractionController.java 文件中找到了一个看起来很好用的一个方法 performMultiPointerGesture :

    /**
* Performs a multi-touch gesture
*
* Takes a series of touch coordinates for at least 2 pointers. Each pointer must have
* all of its touch steps defined in an array of {@link PointerCoords}. By having the ability
* to specify the touch points along the path of a pointer, the caller is able to specify
* complex gestures like circles, irregular shapes etc, where each pointer may take a
* different path.
*
* To create a single point on a pointer's touch path
* <code>
* PointerCoords p = new PointerCoords();
* p.x = stepX;
* p.y = stepY;
* p.pressure = 1;
* p.size = 1;
* </code>
* @param touches each array of {@link PointerCoords} constitute a single pointer's touch path.
* Multiple {@link PointerCoords} arrays constitute multiple pointers, each with its own
* path. Each {@link PointerCoords} in an array constitute a point on a pointer's path.
* @return <code>true</code> if all points on all paths are injected successfully, <code>false
* </code>otherwise
* @since API Level 18
*/
public boolean performMultiPointerGesture(PointerCoords[] ... touches) {
boolean ret = true;
if (touches.length < 2) {
throw new IllegalArgumentException("Must provide coordinates for at least 2 pointers");
} // Get the pointer with the max steps to inject.
int maxSteps = 0;
for (int x = 0; x < touches.length; x++)
maxSteps = (maxSteps < touches[x].length) ? touches[x].length : maxSteps; // specify the properties for each pointer as finger touch
PointerProperties[] properties = new PointerProperties[touches.length];
PointerCoords[] pointerCoords = new PointerCoords[touches.length];
for (int x = 0; x < touches.length; x++) {
PointerProperties prop = new PointerProperties();
prop.id = x;
prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
properties[x] = prop; // for each pointer set the first coordinates for touch down
pointerCoords[x] = touches[x][0];
} // Touch down all pointers
long downTime = SystemClock.uptimeMillis();
MotionEvent event;
event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 1,
properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
ret &= injectEventSync(event); for (int x = 1; x < touches.length; x++) {
event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
getPointerAction(MotionEvent.ACTION_POINTER_DOWN, x), x + 1, properties,
pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
ret &= injectEventSync(event);
} // Move all pointers
for (int i = 1; i < maxSteps - 1; i++) {
// for each pointer
for (int x = 0; x < touches.length; x++) {
// check if it has coordinates to move
if (touches[x].length > i)
pointerCoords[x] = touches[x][i];
else
pointerCoords[x] = touches[x][touches[x].length - 1];
} event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
MotionEvent.ACTION_MOVE, touches.length, properties, pointerCoords, 0, 0, 1, 1,
0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); ret &= injectEventSync(event);
SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
} // For each pointer get the last coordinates
for (int x = 0; x < touches.length; x++)
pointerCoords[x] = touches[x][touches[x].length - 1]; // touch up
for (int x = 1; x < touches.length; x++) {
event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
getPointerAction(MotionEvent.ACTION_POINTER_UP, x), x + 1, properties,
pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
ret &= injectEventSync(event);
} Log.i(LOG_TAG, "x " + pointerCoords[0].x);
// first to touch down is last up
event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 1,
properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
ret &= injectEventSync(event);
return ret;
}

  对这个方法解释一下吧:模拟一个多触摸的手势。至少两个点进行一系列的触摸操作,每一个滚动轨迹的点集都需要使用在 PointerCoords 数组中实现它所有的步骤,顺着轨迹指针的路径进行模拟触摸,调用者是能够指定进行完成复杂的手势,例如画圈,不规则形状,其中每个点都可以有不同的路径。(解释的我自己都看不下去了)

  废话不说了,直接上我的实战代码吧:

 private void zoomin() {
UiDevice mdevice = getUiDevice();
int h = mdevice.getDisplayHeight();
int w = mdevice.getDisplayWidth();
System.out.println("h: " + h + " w: " + w); MultiPointerGesture(w / 2 - 100, h / 2 - 100, w / 2 + 100, h / 2 + 100);
} private void MultiPointerGesture(int x1, int y1, int x2, int y2) {
PointerProperties[] properties = new PointerProperties[2];
PointerProperties pp1 = new PointerProperties();
pp1.id = 0;
pp1.toolType = MotionEvent.TOOL_TYPE_FINGER;
PointerProperties pp2 = new PointerProperties();
pp2.id = 1;
pp2.toolType = MotionEvent.TOOL_TYPE_FINGER;
properties[0] = pp1;
properties[1] = pp2; PointerCoords[] pointerCoords = new PointerCoords[2];
PointerCoords pc1 = new PointerCoords();
pc1.pressure = 1;
pc1.size = 1;
pc1.x = x1;
pc1.y = y1;
PointerCoords pc2 = new PointerCoords();
pc2.pressure = 1;
pc2.size = 1;
pc2.x = x1;
pc2.y = y1;
pointerCoords[0] = pc1;
pointerCoords[1] = pc2; PointerProperties[] properties2 = new PointerProperties[2];
PointerProperties pp12 = new PointerProperties();
pp12.id = 0;
pp12.toolType = MotionEvent.TOOL_TYPE_FINGER;
PointerProperties pp22 = new PointerProperties();
pp22.id = 1;
pp22.toolType = MotionEvent.TOOL_TYPE_FINGER;
properties2[0] = pp12;
properties2[1] = pp22; PointerCoords[] pointerCoords2 = new PointerCoords[2];
PointerCoords pc12 = new PointerCoords();
pc12.pressure = 1;
pc12.size = 1;
pc12.x = x2;
pc12.y = y2;
PointerCoords pc22 = new PointerCoords();
pc22.pressure = 1;
pc22.size = 1;
pc22.x = x2;
pc22.y = y2;
pointerCoords2[0] = pc12;
pointerCoords2[1] = pc22; PointerCoords[][] ppCoords = new PointerCoords[2][];
ppCoords[0] = pointerCoords;
ppCoords[1] = pointerCoords2; UiDevice device = UiDevice.getInstance(); Class UiDevice_class = UiDevice.class;
Field field_UiD;
try
{
field_UiD = UiDevice_class.getDeclaredField("mUiAutomationBridge");
field_UiD.setAccessible(true);
Object uiAutomatorBridge; uiAutomatorBridge = field_UiD.get(device); Class tmp = Class.forName("com.android.uiautomator.core.UiAutomatorBridge");
Field field = tmp.getDeclaredField("mInteractionController");
field.setAccessible(true);
Object interactionController = field.get(uiAutomatorBridge); Class ijClass = interactionController.getClass();
Method method = null;
try
{
method = ijClass.getDeclaredMethod("performMultiPointerGesture", new Class[] { PointerCoords[][].class });
} catch (NoSuchMethodException e)
{
// method =
// ijClass.getDeclaredMethod("performMultiPointerGesture", new
// Class[]{PointerCoords[].class);
}
method.setAccessible(true);
method.invoke(interactionController, new Object[] { ppCoords }); } catch (NoSuchFieldException e)
{
e.printStackTrace();
} catch (SecurityException e)
{
e.printStackTrace();
} catch (IllegalArgumentException e)
{
e.printStackTrace();
} catch (IllegalAccessException e)
{
e.printStackTrace();
} catch (InvocationTargetException e)
{
e.printStackTrace();
} catch (ClassNotFoundException e1)
{
e1.printStackTrace();
}
}

  可以看到当时花了很大的代价,都用到了反射机制,最终发现Uiautomator对外暴露了这个接口,真是让人哭笑不得啊。。