Android 输入法,字符输入和显示过程流程
目录
1 InputConnection对象绑定流程
1.1 InputMethodManager.startInputInner
1.2 IMMS.startInputUncheckedLocked
1.3 IMMS.attachNewInputLocked
1.4 MSG_START_INPUT
1.5 IMS.IInputMethodWrapper.startInput
1.6 IMS.IInputMethodWrapper.DO_START_INPUT
1.7 IMS.IInputMethodWrapper.dispatchStartInputWithToken
1.8 IMS.startInput
1.9 IMS.doStartInput
2 输入法应用传递字符和显示流程
2.1 LatinKeyboardView.onTouchEvent
2.2 LatinKeyboardView.onModifiedTouchEvent
2.3 LatinKeyboardView.detectAndSendKey
2.4 LatinKeyboardView.onKey
2.5 LatinKeyboardView.handleCharacter
2.6 EditableInputConnection.commitText
2.7 BaseInputConnection.commitText
2.8 SpannableStringBuilder.replace
本流程以自定义的输入法应用为例,调试从点击输入法上的字符到输入框显示字符的流程
在讲解view显示字符之前,我们先看下输入法应用端的InputConnection获取过程
InputConnection对象绑定流程
InputConnection创建和绑定.pngInputMethodManager.startInputInner
该过程主要完成InputConnection的创建,并将创建的InputConnection对象,在启动输入法的过程中,传递地到IMMS中
boolean startInputInner(@InputMethodClient.StartInputReason final int startInputReason,
IBinder windowGainingFocus, int controlFlags, int softInputMode,
int windowFlags) {
final View view;
synchronized (mH) {
view = mServedView;//获取输入法焦点的view,可能为null;当窗口获取焦点时,该对象为null+
if (view == null) {
if (DEBUG) Log.v(TAG, "ABORT input: no served view!");
return false;
}
}
Handler vh = view.getHandler();
if (vh == null) {
closeCurrentInput();
return false;
}
if (vh.getLooper() != Looper.myLooper()) {
// The view is running on a different thread than our own, so
// we need to reschedule our work for over there.
if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread");
vh.post(() -> startInputInner(startInputReason, null, 0, 0, 0));
return false;
}
// Okay we are now ready to call into the served view and have it
// do its stuff.
// Life is good: let's hook everything up!
EditorInfo tba = new EditorInfo()//存储编辑框相关的信息,例如包名
// Note: Use Context#getOpPackageName() rather than Context#getPackageName() so that the
// system can verify the consistency between the uid of this process and package name passed
// from here. See comment of Context#getOpPackageName() for details.
tba.packageName = view.getContext().getOpPackageName();
tba.fieldId = view.getId();
InputConnection ic = view.onCreateInputConnection(tba);//获取输入法与该编辑框进行交互的InputConnection对象
if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);
synchronized (mH) {
// Now that we are locked again, validate that our state hasn't
// changed.
if (mServedView != view || !mServedConnecting) {//处理多线程情况,如果要编辑的view发生改变,则直接返回false
return false;
}
// If we already have a text box, then this view is already
// connected so we want to restart it.
if (mCurrentTextBoxAttribute == null) {
controlFlags |= CONTROL_START_INITIAL;
}
// Hook 'em up and let 'er rip.
mCurrentTextBoxAttribute = tba;
mServedConnecting = false;
if (mServedInputConnectionWrapper != null) {//inputconnectiong装饰器
mServedInputConnectionWrapper.deactivate();
mServedInputConnectionWrapper = null;
}
ControlledInputConnectionWrapper servedContext;
final int missingMethodFlags;
if (ic != null) {
mCursorSelStart = tba.initialSelStart;
mCursorSelEnd = tba.initialSelEnd;
mCursorCandStart = -1;
mCursorCandEnd = -1;
mCursorRect.setEmpty();
mCursorAnchorInfo = null;
final Handler icHandler;
missingMethodFlags = InputConnectionInspector.getMissingMethodFlags(ic);
if ((missingMethodFlags & InputConnectionInspector.MissingMethodFlags.GET_HANDLER)
!= 0) {
// InputConnection#getHandler() is not implemented.
icHandler = null;
} else {
icHandler = ic.getHandler();
}
//创建InputConnection装饰器
servedContext = new ControlledInputConnectionWrapper(
icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this);
} else {
servedContext = null;
missingMethodFlags = 0;
}
//保存InputConnection装饰器
mServedInputConnectionWrapper = servedContext;
try {
//启动输入法的时候,将servedContext传递到IMMS中去
final InputBindResult res = mService.startInputOrWindowGainedFocus(
startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode,
windowFlags, tba, servedContext, missingMethodFlags,
view.getContext().getApplicationInfo().targetSdkVersion);
....
}
return true;
}
IMMS.startInputUncheckedLocked
通过以上流程图可知,传递的servedContext最终传递到startInputUncheckedLocked方法,并保存在IMMS的
mCurInputContext变量中
@GuardedBy("mMethodMap")
@NonNull
InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,
/* @InputConnectionInspector.missingMethods */ final int missingMethods,
@NonNull EditorInfo attribute, int controlFlags,
/* @InputMethodClient.StartInputReason */ final int startInputReason) {
// If no method is currently selected, do nothing.
if (mCurMethodId == null) {
return InputBindResult.NO_IME;
}
if (!InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.uid,
attribute.packageName)) {
Slog.e(TAG, "Rejecting this client as it reported an invalid package name."
+ " uid=" + cs.uid + " package=" + attribute.packageName);
return InputBindResult.INVALID_PACKAGE_NAME;
}
//处理启动输入法过程中,应用发生切换到场景
if (mCurClient != cs) {
// Was the keyguard locked when switching over to the new client?
mCurClientInKeyguard = isKeyguardLocked();
// If the client is changing, we need to switch over to the new
// one.
unbindCurrentClientLocked(InputMethodClient.UNBIND_REASON_SWITCH_CLIENT);
if (DEBUG) Slog.v(TAG, "switching to client: client="
+ cs.client.asBinder() + " keyguard=" + mCurClientInKeyguard);
// If the screen is on, inform the new client it is active
if (mIsInteractive) {
executeOrSendMessage(cs.client, mCaller.obtainMessageIO(
MSG_SET_ACTIVE, mIsInteractive ? 1 : 0, cs));
}
}
// Bump up the sequence for this client and attach it.
mCurSeq++;
if (mCurSeq <= 0) mCurSeq = 1;
mCurClient = cs;
//将InputConnection对象,存储到本地mCurInputContext变量中
mCurInputContext = inputContext;
mCurInputContextMissingMethods = missingMethods;
mCurAttribute = attribute;
// Check if the input method is changing.
if (mCurId != null && mCurId.equals(mCurMethodId)) {
if (cs.curSession != null) {
// Fast case: if we are already connected to the input method,
// then just return it.
//每个应用在启动时,都会绑定默认输入法,触发启动后,切换了默认输入法
return attachNewInputLocked(startInputReason,
(controlFlags&InputMethodManager.CONTROL_START_INITIAL) != 0);
}
if (mHaveConnection) {
if (mCurMethod != null) {
// Return to client, and we will get back with it when
// we have had a session made for it.
requestClientSessionLocked(cs);
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_SESSION,
null, null, mCurId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
} else if (SystemClock.uptimeMillis()
< (mLastBindTime+TIME_TO_RECONNECT)) {
// In this case we have connected to the service, but
// don't yet have its interface. If it hasn't been too
// long since we did the connection, we'll return to
// the client and wait to get the service interface so
// we can report back. If it has been too long, we want
// to fall through so we can try a disconnect/reconnect
// to see if we can get back in touch with the service.
return new InputBindResult(
InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
null, null, mCurId, mCurSeq,
mCurUserActionNotificationSequenceNumber);
} else {
EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME,
mCurMethodId, SystemClock.uptimeMillis()-mLastBindTime, 0);
}
}
}
//通过binderserver重新启动输入法应用
return startInputInnerLocked();
}
IMMS.attachNewInputLocked
@GuardedBy("mMethodMap")
@NonNull
InputBindResult attachNewInputLocked(
/* @InputMethodClient.StartInputReason */ final int startInputReason, boolean initial) {
if (!mBoundToMethod) {
executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
MSG_BIND_INPUT, mCurMethod, mCurClient.binding));
mBoundToMethod = true;
}
//创建和保存启动的输入法的相关信息
final Binder startInputToken = new Binder();
final StartInputInfo info = new StartInputInfo(mCurToken, mCurId, startInputReason,
!initial, mCurFocusedWindow, mCurAttribute, mCurFocusedWindowSoftInputMode,
mCurSeq);
mStartInputMap.put(startInputToken, info);
//存储历史输入法相关信息
mStartInputHistory.addEntry(info);
final SessionState session = mCurClient.curSession;
executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(
MSG_START_INPUT, mCurInputContextMissingMethods, initial ? 0 : 1 /* restarting */,
startInputToken, session, mCurInputContext, mCurAttribute));
if (mShowRequested) {
if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
showCurrentInputLocked(getAppShowFlags(), null);
}
return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
session.session, (session.channel != null ? session.channel.dup() : null),
mCurId, mCurSeq, mCurUserActionNotificationSequenceNumber);
}
MSG_START_INPUT
case MSG_START_INPUT: {
final int missingMethods = msg.arg1;
final boolean restarting = msg.arg2 != 0;
args = (SomeArgs) msg.obj;
final IBinder startInputToken = (IBinder) args.arg1;
final SessionState session = (SessionState) args.arg2;
final IInputContext inputContext = (IInputContext) args.arg3;
final EditorInfo editorInfo = (EditorInfo) args.arg4;
try {
setEnabledSessionInMainThread(session);
//session.method,输入法服务启动的时候,从输入法传递过来的IInputMethodWrapper对象
session.method.startInput(startInputToken, inputContext, missingMethods,
editorInfo, restarting);
} catch (RemoteException e) {
}
args.recycle();
return true;
}
该过程,主要是调用输入法应用层IInputMethodWrapper对象的startInput方法
传递的参数:
inputContext:被编辑的输入框的InputConnection对象
editorInfo:编辑框的相关信息
session.method的创建和传递到IMMS的流程如下,具体参看 启动输入法服务2
IMS应用
AbstractInputMethodService.java
@Override
final public IBinder onBind(Intent intent) {
if (mInputMethod == null) {
mInputMethod = onCreateInputMethodInterface();
}
return new IInputMethodWrapper(this, mInputMethod);
}
IMMS.JAVA
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mMethodMap) {
//mCurIntent 上篇文章有讲到,当前默认输入法对应的Intent对象
//校验binder启动的服务对应的ComponentName和该方法传递的name一致性
if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
//获取输入法应用的IInputMethodWrapper对象
mCurMethod = IInputMethod.Stub.asInterface(service);
if (mCurToken == null) {//输入法应用在binderserver的时候,创建的
Slog.w(TAG, "Service connected without a token!");
unbindCurrentMethodLocked(false);//解绑上次绑定的输入法服务
return;
}
if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
//调用输入法应用端的IInputMethodWrapper$InputMethodImpl:attachToken方法
executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
MSG_ATTACH_TOKEN, mCurMethod, mCurToken));
if (mCurClient != null) {//
clearClientSessionLocked(mCurClient);
requestClientSessionLocked(mCurClient);
}
}
}
}
IMS.IInputMethodWrapper.startInput
@BinderThread
@Override
public void startInput(IBinder startInputToken, IInputContext inputContext,
@InputConnectionInspector.MissingMethodFlags final int missingMethods,
EditorInfo attribute, boolean restarting) {
if (mIsUnbindIssued == null) {
Log.e(TAG, "startInput must be called after bindInput.");
mIsUnbindIssued = new AtomicBoolean();
}
//通过message一步处理改流程
mCaller.executeOrSendMessage(mCaller.obtainMessageIIOOOO(DO_START_INPUT,
missingMethods, restarting ? 1 : 0, startInputToken, inputContext, attribute,
mIsUnbindIssued));
}
IMS.IInputMethodWrapper.DO_START_INPUT
case DO_START_INPUT: {
final SomeArgs args = (SomeArgs) msg.obj;
final int missingMethods = msg.arg1;
final boolean restarting = msg.arg2 != 0;
final IBinder startInputToken = (IBinder) args.arg1;
//InputConnection对象
final IInputContext inputContext = (IInputContext) args.arg2;
final EditorInfo info = (EditorInfo) args.arg3;
final AtomicBoolean isUnbindIssued = (AtomicBoolean) args.arg4;
final InputConnection ic = inputContext != null
? new InputConnectionWrapper(
mTarget, inputContext, missingMethods, isUnbindIssued) : null;
info.makeCompatible(mTargetSdkVersion);
inputMethod.dispatchStartInputWithToken(ic, info, restarting /* restarting */,
startInputToken);
args.recycle();
return;
}
IMS.IInputMethodWrapper.dispatchStartInputWithToken
@MainThread
default void dispatchStartInputWithToken(@Nullable InputConnection inputConnection,
@NonNull EditorInfo editorInfo, boolean restarting,
@NonNull IBinder startInputToken) {
if (restarting) {
restartInput(inputConnection, editorInfo);
} else {
startInput(inputConnection, editorInfo);
}
}
IMS.startInput
/**
* {@inheritDoc}
*/
@MainThread
@Override
public void startInput(InputConnection ic, EditorInfo attribute) {
if (DEBUG) Log.v(TAG, "startInput(): editor=" + attribute);
doStartInput(ic, attribute, false);
}
IMS.doStartInput
void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) {
if (!restarting) {
doFinishInput();
}
mInputStarted = true;
//存储TextView的InputConnection对象
mStartedInputConnection = ic;
mInputEditorInfo = attribute;
initialize();
if (DEBUG) Log.v(TAG, "CALL: onStartInput");
onStartInput(attribute, restarting);
if (mWindowVisible) {
if (mShowInputRequested) {
if (DEBUG) Log.v(TAG, "CALL: onStartInputView");
mInputViewStarted = true;
onStartInputView(mInputEditorInfo, restarting);
startExtractingText(true);
} else if (mCandidatesVisibility == View.VISIBLE) {
if (DEBUG) Log.v(TAG, "CALL: onStartCandidatesView");
mCandidatesViewStarted = true;
onStartCandidatesView(mInputEditorInfo, restarting);
}
}
}
通过以上过程,完成InputConnection从TextView到IMS的传递
- 创建TextView对应的InputConnection对象
- 保存TextView的InputConnection对象到IMMS的mCurInputContext对象中
- 传递TextView的InputConnection对象到IMS
- 存储TextView的InputConnection对到mStartedInputConnection中
输入法应用传递字符和显示流程
文字输入和显示.pngLatinKeyboardView.onTouchEvent
if (isPointerCountOne) {//判断是否是单指模式
result = onModifiedTouchEvent(me, false);
mOldPointerX = me.getX();
mOldPointerY = me.getY();
} else {
result = true;
}
LatinKeyboardView.onModifiedTouchEvent
case MotionEvent.ACTION_UP:
...
if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) {
//mCurrentKey表示当前点击的字符在字符数组中对应的下标
detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
}
...
break;
LatinKeyboardView.detectAndSendKey
private void detectAndSendKey(int index, int x, int y, long eventTime) {
if (index != NOT_A_KEY && index < mKeys.length) {
//获取电子的字符对应的key,对应的点击的字符村粗在key.label中
final LatinKeyboard.Key key = mKeys[index];
if (key.text != null) {//不执行
mKeyboardActionListener.onText(key.text);
mKeyboardActionListener.onRelease(NOT_A_KEY);
} else {
int code = key.codes[0];
int[] codes = new int[MAX_NEARBY_KEYS];
Arrays.fill(codes, NOT_A_KEY);
getKeyIndices(x, y, codes);
if (mInMultiTap) {
if (mTapCount != -1) {
mKeyboardActionListener.onKey(LatinKeyboard.KEYCODE_DELETE, KEY_DELETE);
} else {
mTapCount = 0;
}
code = key.codes[mTapCount];
}
//code,codes表示每个字符对应的数字编码
mKeyboardActionListener.onKey(code, codes);
mKeyboardActionListener.onRelease(code);
}
mLastSentIndex = index;
mLastTapTime = eventTime;
}
}
各个字符对应的codes如下表
<Row>
<Key android:codes="113" android:keyLabel="q" android:horizontalGap="@dimen/qwert_r1245_horizontal_border"
android:keyEdgeFlags="left"/>
<Key android:codes="119" android:keyLabel="w"/>
<Key android:codes="101" android:keyLabel="e"/>
<Key android:codes="114" android:keyLabel="r"/>
<Key android:codes="116" android:keyLabel="t"/>
<Key android:codes="121" android:keyLabel="y"/>
<Key android:codes="117" android:keyLabel="u"/>
<Key android:codes="105" android:keyLabel="i"/>
<Key android:codes="111" android:keyLabel="o"/>
<Key android:codes="112" android:keyLabel="p" android:keyEdgeFlags="right"/>
</Row>
LatinKeyboardView.onKey
public void onKey(int primaryCode, int[] keyCodes) {
...
handleCharacter(primaryCode, keyCodes);
...
}
LatinKeyboardView.handleCharacter
private void handleCharacter(int primaryCode, int[] keyCodes) {
if (isInputViewShown()) {//输入法窗口是否显示
if (mInputView.isShifted()) {//是否点击了shift键
primaryCode = Character.toUpperCase(primaryCode);
}
}
if (isAlphabet(primaryCode) && mPredictionOn) {//输入的是字母,并且自动预测功能打开
mComposing.append((char) primaryCode);
getCurrentInputConnection().setComposingText(mComposing, 1);
updateShiftKeyState(getCurrentInputEditorInfo());
} else {
//获取跟要输入的view绑定的InputConnection,通过该对象向绑定的输入框view传递字符
getCurrentInputConnection().commitText(
String.valueOf((char) primaryCode), 1);
}
}
public InputConnection getCurrentInputConnection() {
InputConnection ic = mStartedInputConnection;
if (ic != null) {
return ic;
}
return mInputConnection;
}
通过以上过程,将字符传递到TextVeiw的InputConnection对象中去
看下InputConnection的创建过程
TextView.java
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (onCheckIsTextEditor() && isEnabled()) {
...
if (mText instanceof Editable) {
InputConnection ic = new EditableInputConnection(this);
outAttrs.initialSelStart = getSelectionStart();
outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
return ic;
}
}
return null;
}
onCreateInputConnection创建了EditableInputConnection对象,并持有当前TextView对象
EditableInputConnection.commitText
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (mTextView == null) {
return super.commitText(text, newCursorPosition);
}
if (text instanceof Spanned) {
Spanned spanned = ((Spanned) text);
SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class);
mIMM.registerSuggestionSpansForNotification(spans);
}
mTextView.resetErrorChangedFlag();
//调用父类的文字提交方法
boolean success = super.commitText(text, newCursorPosition);
mTextView.hideErrorIfUnchanged();
return success;
}
BaseInputConnection.commitText
public boolean commitText(CharSequence text, int newCursorPosition) {
if (DEBUG) Log.v(TAG, "commitText " + text);
replaceText(text, newCursorPosition, false);
sendCurrentText();
return true;
}
BaseInputConnection.replaceText
替换输入框的文本内容
主要代码逻辑,根据传入的字符内容和替换位置,将输入的字符插入到要显示的文本字符串中
private void replaceText(CharSequence text, int newCursorPosition,
boolean composing) {
final Editable content = getEditable();
if (content == null) {
return;
}
beginBatchEdit();
// delete composing text set previously.
int a = getComposingSpanStart(content);
int b = getComposingSpanEnd(content);
if (DEBUG) Log.v(TAG, "Composing span: " + a + " to " + b);
if (b < a) {
int tmp = a;
a = b;
b = tmp;
}
if (a != -1 && b != -1) {
removeComposingSpans(content);
} else {
a = Selection.getSelectionStart(content);
b = Selection.getSelectionEnd(content);
if (a < 0) a = 0;
if (b < 0) b = 0;
if (b < a) {
int tmp = a;
a = b;
b = tmp;
}
}
if (composing) {
Spannable sp = null;
if (!(text instanceof Spannable)) {
sp = new SpannableStringBuilder(text);
text = sp;
ensureDefaultComposingSpans();
if (mDefaultComposingSpans != null) {
for (int i = 0; i < mDefaultComposingSpans.length; ++i) {
sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
}
}
} else {
sp = (Spannable)text;
}
setComposingSpans(sp);
}
if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \""
+ text + "\", composing=" + composing
+ ", type=" + text.getClass().getCanonicalName());
if (DEBUG) {
LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
lp.println("Current text:");
TextUtils.dumpSpans(content, lp, " ");
lp.println("Composing text:");
TextUtils.dumpSpans(text, lp, " ");
}
// Position the cursor appropriately, so that after replacing the
// desired range of text it will be located in the correct spot.
// This allows us to deal with filters performing edits on the text
// we are providing here.
if (newCursorPosition > 0) {
newCursorPosition += b - 1;
} else {
newCursorPosition += a;
}
if (newCursorPosition < 0) newCursorPosition = 0;
if (newCursorPosition > content.length())
newCursorPosition = content.length();
Selection.setSelection(content, newCursorPosition);
//替换要显示的内容中的文本内容
content.replace(a, b, text);
if (DEBUG) {
LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
lp.println("Final text:");
TextUtils.dumpSpans(content, lp, " ");
}
endBatchEdit();
}
SpannableStringBuilder.replace
SpannableStringBuilder中存储TextView要显示的文字内容
// Documentation from interface
public SpannableStringBuilder replace(final int start, final int end,
CharSequence tb, int tbstart, int tbend) {
checkRange("replace", start, end);
...
//回调EditChangedListener的beforeTextChanged方法
sendBeforeTextChanged(textWatchers, start, origLen, newLen);
//替换要显示的内容
change(start, end, tb, tbstart, tbend);
...
//回调EditChangedListener的onTextChanged方法
sendTextChanged(textWatchers, start, origLen, newLen);
//回调EditChangedListener的afterTextChanged方法
sendAfterTextChanged(textWatchers);
// Span watchers need to be called after text watchers, which may update the layout
sendToSpanWatchers(start, end, newLen - origLen);
return this;
}
EditChangedListener设置方法
mEditText = (EditText) findViewById(R.id.edit_text);
mEditText.addTextChangedListener(new EditChangedListener(mEditText));
public class EditChangedListener implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Log.d("edit", "beforeTextChanged temp:" + temp.toString());
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Log.d("edit", "onTextChanged s:" + s + " start:" + start + " before:" + before + " count:" + count);
}
@Override
public void afterTextChanged(Editable s) {
/** 得到光标开始和结束位置 ,超过最大数后记录刚超出的数字索引进行控制 */
Log.d("edit", "afterTextChanged s:" + s.toString());
}
};
经过此过程后,输入法输入的内容显示到输入框中