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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值