解决格式化字符输入的困扰--Android

来源:互联网 发布:微服务架构 数据库 编辑:程序博客网 时间:2024/05/15 23:52

项目需求:
我们有一个输入框,为了提高用户体验,会自动格式化输入的字符串,其实这些东西在各种电商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(这是最权威的!)

0 0