项目需求:
我们有一个输入框,为了提高用户体验,会自动格式化输入的字符串,其实这些东西在各种电商APP上很常见,举个例子,
例如输入手机号:13323450000
输入过程中,APP会自动做判断,根据输入的长度做判断进而格式化例如变为:133 2345 0000
这期间的实现其实很简单,就是每当我们输入字符时,都会触发一个Event事件,如果当我们输入3个字符后,在输入第四个字符时,程序就会自动在其前面增加一个空格。
如果你们试验一下的话就是会发现,如果我们用sendkeys来输入的话,基本很难成功,输入的字符不是多几位就是少几位,反正就是各种错误,后来我想到是否可以分开输入,3,4,4节奏的输入,但是还是会失败,为什么呢?
下面贴一段代码,大家一看便知:
package io.appium.android.bootstrap.handler;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;
import java.util.Hashtable;
/**
* This handler is used to set text in elements that support it.
*
*/
public class SetText extends CommandHandler {
/*
* @param command The {@link AndroidCommand} used for this handler.
*
* @return {@link AndroidCommandResult}
*
* @throws JSONException
*
* @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
* bootstrap.AndroidCommand)
*/
@Override
public AndroidCommandResult execute(final AndroidCommand command)
throws JSONException {
if (command.isElementCommand()) {
// Only makes sense on an element
try {
final Hashtable<String, Object> params = command.params();
final AndroidElement el = command.getElement();
boolean replace = Boolean.parseBoolean(params.get("replace").toString());
String text = params.get("text").toString();
boolean pressEnter = false;
if (text.endsWith("\\n")) {
pressEnter = true;
text = text.replace("\\n", "");
Logger.debug("Will press enter after setting text");
}
boolean unicodeKeyboard = false;
if (params.get("unicodeKeyboard") != null) {
unicodeKeyboard = Boolean.parseBoolean(params.get("unicodeKeyboard").toString());
}
String currText = el.getText();
new Clear().execute(command);
if (!el.getText().isEmpty()) {
// clear could have failed, or we could have a hint in the field
// we'll assume it is the latter
Logger.debug("Text not cleared. Assuming remainder is hint text.");
currText = "";
}
if (!replace) {
text = currText + text;
}
final boolean result = el.setText(text, unicodeKeyboard);
if (pressEnter) {
final UiDevice d = UiDevice.getInstance();
d.pressEnter();
}
return getSuccessResult(result);
} catch (final UiObjectNotFoundException e) {
return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
e.getMessage());
} catch (final Exception e) { // handle NullPointerException
return getErrorResult("Unknown error");
}
} else {
return getErrorResult("Unable to set text without an element.");
}
}
}
这就是bootstrap中的setText类,中间有一段代码“new Clear().execute(command);” 清除命令
Appium的实现都是通过bootstrap来和uiautomator做交互的,So 刚刚那个想法有点儿障碍,但是我没有屈服,我们是开源的所以我修改了这个类,但是实际操作后我发现貌似没有什么作用,在赋值时依然会先clear,这是为什么呢,没办法,再次向下追踪,刚刚已经说过,bootstrap是跟uiautomator做交互,实际上也就是说是最终通过uiautomator来操作我们的手机,所以我找到了uiautomator的源码,发现了一个我不愿意相信的事实,大家看下面的源码,是UiObject类的内容,截图了:
这个里面也会clear,so,我彻底死心了这个办法是无解了。
不过我们不会屈服的,对吧,哈哈,换个角度,如果我能够控制输入的速度,是不是就可以了呢,可是,Appium暴露给我们的只有一个sendKeys的API,怎么办呢?
继续解读bootstrap的源码,通过UiObject类中的那段代码我们可以看到,最终调用的是一个sendText的API,继续往下挖,我们会发现这个API无法再往下点了(通过ctrl + 单击),说明是隐藏的类,从Android sdk的source包中,我找到了这个方法的源码:
public boolean sendText(String text) {
if (DEBUG) {
Log.d(LOG_TAG, "sendText (" + text + ")");
}
KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
if (events != null) {
long keyDelay = Configurator.getInstance().getKeyInjectionDelay();
for (KeyEvent event2 : events) {
// We have to change the time of an event before injecting it because
// all KeyEvents returned by KeyCharacterMap.getEvents() have the same
// time stamp and the system rejects too old events. Hence, it is
// possible for an event to become stale before it is injected if it
// takes too long to inject the preceding ones.
KeyEvent event = KeyEvent.changeTimeRepeat(event2,
SystemClock.uptimeMillis(), 0);
if (!injectEventSync(event)) {
return false;
}
SystemClock.sleep(keyDelay);
}
}
return true;
}
这里面我们发现了什么?对,有sleep,哈哈,好像看到希望了,先简单说一下它的工作原理,我们把一个string给这个API,它先把String分割为单个的KeyEvent,然后有一个for循环来依次执行,每次执行都会判断一下是否成功,并且会sleep一下。所以,我们控制一下这个keyDelay就能实现我们的愿望了,好激动,不过这个keyDelay是传人的,而是
“long keyDelay = Configurator.getInstance().getKeyInjectionDelay();”
那么我们需要继续深挖了,Configurator类源码不少,我贴几个重要的如下:
getKeyInjectionDelay API:
/**
* Gets the current delay between key presses when injecting text input.
* See {@link UiObject#setText(String)}
*
* @return current delay in milliseconds
* @since API Level 18
*/
public long getKeyInjectionDelay() {
return mKeyInjectionDelay;
}
getKeyInjectionDelay API:
/**
* Sets a delay between key presses when injecting text input.
* See {@link UiObject#setText(String)}
*
* @param delay Delay value in milliseconds
* @return self
* @since API Level 18
*/
public Configurator setKeyInjectionDelay(long delay) {
mKeyInjectionDelay = delay;
return this;
}
mKeyInjectionDelay 成员变量
// Default is inject as fast as we can
private long mKeyInjectionDelay = 0; // ms
getInstance()
/**
* Retrieves a singleton instance of Configurator.
*
* @return Configurator instance
* @since API Level 18
*/
public static Configurator getInstance() {
if (sConfigurator == null) {
sConfigurator = new Configurator();
}
return sConfigurator;
}
行了,这样大家应该可以看出来了,getInstance()可以获取一个实例,所以,我们可以在bootstrap做一些修改,找到我最上面贴的那段代码中加一行这样的代码:
Configurator.getInstance().setKeyInjectionDelay(500);
位置是在这行代码前面:
final boolean result = el.setText(text, unicodeKeyboard);
新增代码的意思就是将mKeyInjectionDelay设置为500毫秒,即:在每次执行KeyEvent时先等待500毫秒。
结果:我发现这个做法成功的几率也很低,因为它会拖慢整个Event执行的速度,只有在机器变的很慢时才会有效,当时我用的是红米手机,刷了4.4的原生Android系统的,so,这个办法也行不通,不过确让我了解了uiautomator的运行原理及部分源码的运行逻辑,虽然时间浪费了,不过还是值得的。
但是,问题没有解决,怎么办?
后来我再次去研究Appium提供的API,发现了一个sendKeyEvent的API,这次好像就没有那么大的激情了,不过只能死马当活马医了,这个方法是直接发送key值,关于key值,key值的Map网上有很多,在这里我给大家直接发一个源码文件吧,就不贴了,而且咱们论坛里已经有人发出来了,大家也可以去参考一下。
实现方法我大体说一下,其实很简单,先把String分割为单个的char,然后通过key的Map映射关系一一转换为key值,然后使用一个for循环,一一直接调用sendKeyEvent方法把key值传进去即可,中间设置一个sleep,我设置的是500毫秒,失败的几率很小,目前我的公司项目中是这么用的,没什么大问题,我把它封装成了一个方法,在某种意义上可以代替sendKeys方法了。
最后,关于这个问题,我首先是从github上咨询的官方,但是官方一直没有什么具体的解决办法!
https://github.com/appium/appium/issues/3812#issuecomment-60433195
关于keyEvent 映射,贴一个本站的帖子:
http://www.testerhome.com/topics/1386
当然如果有AndroidSDK源码的童鞋也可以去找一下这个类:KeyEvent.java(这是最权威的!)