📜 ⬆️ ⬇️

How to make friends Custom View and keyboard

Introduction


MoiOffice works on most modern platforms: these are the Web client, desktop versions of the application for Windows, MacOS and Linux, as well as mobile applications for iOS, Android, Tizen. And if in the development of computer applications for a long time there are basic rules for the approach to interface design, then when creating applications for mobile devices, a separate study of many features is required.



When developing a text editor, we abandoned the standard EditText and made our implementation of a component for entering and formatting text based on the TextureView component. The standard mechanism did not give us the opportunity to add tables, images, apply styles, colors, work with lists, indents and much more. Using your component gives us the flexibility to add new functionality and allows you to optimize the performance of the component.

One of the tasks of the component is to give the user the opportunity to enter data from the keyboard, edit the text, use the autochange feature and adjust the text. Further it will be described how to implement a custom element interacting with the keyboard, get the entered text and send the changes to the keyboard. The creation of a custom keyboard is beyond the scope of this article (you can read here , here or here ).
')

CustomView and keyboard


The whole process of interaction of View-elements with the keyboard takes place via the InputConnection interface. The Android SDK already has a base implementation, BaseInputConnection , which allows you to receive keyboard events, process them, and interact with the Editable interface, which is the result of the data received for the component.

But let's start in order. To communicate with the keyboard, first of all it is necessary for the component being developed to determine the implementation of the interface for interaction - to subscribe to keyboard events. In addition, the keyboard itself can transfer a number of settings that affect the type of keyboard and its behavior. As a result, you need to override the component's method - onCreateInputConnection (...) , which returns the implementation. As an attribute, there are keyboard parameters that can be modified.

@Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { //    outAttrs.inputType = InputType.TYPE_CLASS_TEXT; outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; outAttrs.initialSelEnd = outAttrs.initialSelStart = 0; //      return new BaseInputConnection(this, true); } 

In this example, the method returns the base implementation of the interface. And in EditorInfo parameters for the keyboard are specified. About what exactly, you can see in the documentation here . For example, using inputType you can specify numeric or text input for the keyboard.

It should be noted that if you pass the value TYPE_NULL in inputType, then the software keyboard disables autocomplete and all events will come to View. onKeyDown (as well as when working with physical).

If it becomes necessary to change the keyboard configuration when it is already shown (for example, the input type has changed), you should call the restartInput method. In this case, onCreateInputConnection will be recalled and new values ​​can be passed to EditorInfo. But it should be borne in mind that this will result in re-creation of the InputConnection itself.

The next step is to call the setFocusableInTouchMode (true) method (for example, from the onFinishInflate () method or using an attribute in the markup ). With it, the component indicates that it can intercept the focus. And interaction with the keyboard can be only for that element, which is now in focus. If this is not done, the onCreateInputConnection method will not be called.

 @Override protected void onFinishInflate() { super.onFinishInflate(); setFocusableInTouchMode(true); ... } 

Additionally, it is worth noting that when tapping into a component, you need to initiate the opening of the keyboard. This will not happen automatically, so you need to take care of it. One of the options how to do it:

  setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(v, 0); } }); 

The last thing to do is to override the onCheckIsTextEditor method and return TRUE (false by default). According to the documentation - is a hint for the system to automatically show the keyboard on the screen.

 public class CustomView extends View { ... @Override public boolean onCheckIsTextEditor() { return true; } … } 

As a result, in order to establish interaction with the keyboard, it is necessary:

1. Override the onCreateInputConnection method, specifying the interaction interface implementation, as well as the parameters for the keyboard.
2. Call setFocusableInTouchMode (true) when the component is initialized.
3. Call imm.showSoftInput (...) to display the keyboard when tach a component.
4. Return TRUE in the onCheckIsTextEditor method.

Now the mechanism was described how to start receiving events from the keyboard. Further it will be told how to handle these events.

Input connection


It was already noted earlier that in the Android SDK there is a basic implementation of the InputConnection interface - BaseInputConnection . It adds the main logic that allows you to interact with the keyboard, and delegates the received events to the Editable object, which the custom component should work with in the future. For development, it is recommended to follow it and override the getEditable () method, passing its implementation to Editable.

  public class TestInputConnection extends BaseInputConnection { ... @Override public Editable getEditable() { return mCustomView.getEditable(); } ... } 

In more detail about the Editable interface will be described below. In the meantime, I want to consider some InputConnection methods that may be useful to someone. Full documentation on all methods can be found here . It should be noted that the sequence of method calls, the value of the parameters with which they are called, depends on the implementation of the keyboard used at the time of input on the device, and may differ.

beginBatchEdit () and endBatchEdit ()


Informs about the start and end of a set of actions from the keyboard. For example, using the keyboard in T9 mode, a space is entered after the text. In this case, the call finishComposingText () and commitText () will be called successively within one batch event. That is, the sequence will be something like this:

beginBatchEdit
finishComposingText
beginBatchEdit
endBatchEdit
commitText
beginBatchEdit
endBatchEdit
endBatchEdit

Please note that batch nesting is allowed. That is, it is necessary to count the number of started calls and the number of completed ones in order to determine whether the process has ended or not. For example, you can look at the implementation of EditableInputConnection - the implementation for TextView, where increment occurs just at each begin and decrement at end.

Important! Until the batch is finished, it is not recommended to send events from the editor to the keyboard (for example, changing the cursor position).

setComposingText ()


The method is called in cases where the so-called composite text is entered from the keyboard. For example, voice input, text input in autochange mode, etc. That is, the text that can be corrected / replaced from the keyboard.

Example of entering the word test:

setComposingText t 1
setComposingText te 1
setComposingText tes 1
setComposingText test 1

As the parameters of the method comes the new value of the composite text. Next, this value is passed to Editable and marked with special spans (start and end mark composing text). With each new composite text, the previous one is deleted according to the marked spans. This is how text auto-replacement occurs.

finishComposingText ()


It's all quite simple, the method will be called at the moment when the keyboard decides that the text will not be further adjusted and the user will enter the final version. Editing removes all the information about composing Text.

commitText ()


The method is called with the CharSequence text parameters , int newCursorPosition , when the added text is approved, that is, it is not planned to be adjusted. For example, select from the keyboard suggest. In this case, comes the value of the text, which should be added to the place of the current cursor or instead of the compose-text, if it was. As well as information on the new position of the cursor for the editor. A value of> 0 (for example, 1) will indicate the position of the cursor at the end of the new text. Any other value is at the beginning.


deleteSurroundingText ()


The method is called with 2 parameters - int beforeLength, int afterLength and informs that it is necessary to delete part of the text before the current position of the cursor and after. Moreover, if there is a selection of text, then these characters are ignored.

For example, this method is called when the user in T9 mode clicks on the text in the editor and replaces the selected word from the list of hints.

Editable implementation


BaseInputConnection works closely with the Editable interface, the implementation of which needs to be passed in the getEditable () method. All interface methods can be divided into 3 types:

* modification and receipt of text;
* work with spans;
* applying filters.

If you look at the TextView implementation, you can see that the getText () method returns Editable. Or rather, the implementation of SpannableStringBuilder , which is basic and ready for storing and modifying text, working with filters and with spans.

If for some reason the standard implementation does not fit, you can implement your own. The main method of working with text changes is replace (...) . All insert, append, delete and so on. cause the replacement of the text of a particular site with a new one. But you should not forget that before replacing it is necessary to apply a set of filters for text. Further, it is important to correctly implement work with spans, which allow you to hang tags: cursor position, text selection, composing region (the beginning and end of the region for autochange), etc.

Textwatcher


Let's say that we are satisfied with the standard implementation of the interaction with the keyboard and the standard Editable. Now back to the developed component and subscribe to the changes in Editable. This is done quite simply by adding a special spans with a TextWatcher object.

 mEditable.setSpan(mMyTextWatcher, 0, mEditable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); 

After that, for any editable change, notifications will come. It is important to specify the flag - SPAN_INCLUSIVE_INCLUSIVE, which allows you not to delete the listener when you call the clearSpans () method (for example, it is called when finishComposingText occurs).

When receiving notifications, you can get the following information:

* mEditable. toString () will return all text. It can be displayed on the UI - this is what the user entered.
* The methods of the Selection class are needed to obtain information about the cursor and selection.

setText ()


Suppose the component has a setText () method. You need to update the Editable value of the component being developed and notify the keyboard that the previous text in the keyboard buffer was not valid. This is done by creating a new Editable object and calling the restartInput method.

  public void setText(@NonNull String newText) { mEditable = Editable.Factory.getInstance().newEditable(newText); mImm.restartInput(this); } 

Change cursor position


For full interaction with the keyboard, you must add support for cursor positioning. If you enter text in the setComposingText () and commitText () methods, the value comes in the cursorPosition parameter, which determines whether the cursor will be placed at the beginning or at the end of the text to be added. In the case of implementation through BaseInputConnection there is no need to worry about the position of the cursor, the logic is already implemented inside. It is enough to use the Selection method. getSelectionStart and getSelectionEnd to find out the position.

It is quite important to add backward support for changing the cursor. For example, if the developed component is able to display the cursor and it has the ability to change its position, then with each change, you should notify the keyboard. If you do not do this, subsequent keyboard input will ignore the changed position. Also, the replacement of words in T9 mode will work incorrectly.

For notification, use the updateSelection method, where information about the new position of the pins is transferred. It is worth remembering that until the batch in InputConnection ends, the notification should not be sent.

Word replacement with T9


Now a small example, when the user uses the keyboard with T9. Suppose that several words are entered in the editor and one of them is clicked. When working with the standard EditText in the keyboard, a hint will be shown, when clicked, the word will be completely replaced.


With EditText, the keyboard receives information about the new cursor position, sends back to the editor information about the currently selected composing text via the setComposingRegion method, thereby marking the word with which it will continue to work, namely the word under the cursor. Now, if you select one of the prompts, the method will be called: delete the current word and insert a new one.

But research shows that just calling the updateSelection method is not enough. The word is not replaced, but is added to the current position on the cursor, since setComposingRegion is not called.

To find a solution, it is worth looking at the sequence of InputMethodManager called methods when working with EditText, which turns out like this:

 inputMethodManager.viewClicked(view); inputMethodManager.updateSelection(view, cursorPosition, cursorPosition, cursorPosition, cursorPosition); //    -1  composing region  . inputMethodManager.updateSelection(view, cursorPosition, cursorPosition, -1, -1); 

Now, if we add the specified lines to the processing of clicking on a component, the keyboard will start calling setComposingRegion and the text will be replaced correctly.

Code example


 public class CustomView extends View { @NonNull private Editable mEditable; @NonNull private final InputMethodManager mImm; @NonNull private final MyTextWatcher mMyTextWatcher = new MyTextWatcher(); /** *   ,     .  > 0 -  . */ private int mBatch; public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); //  editable. mEditable = Editable.Factory.getInstance().newEditable(""); Selection.setSelection(mEditable, 0); //    setFocusableInTouchMode(true); //     setOnClickListener(v -> mImm.showSoftInput(v, 0)); //       Editable mEditable.setSpan(mMyTextWatcher, 0, mEditable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { outAttrs.inputType = InputType.TYPE_CLASS_TEXT; outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; outAttrs.initialSelEnd = outAttrs.initialSelStart = 0; //    . return new BaseInputConnection(this, true) { @Override public Editable getEditable() { return mEditable; } @Override public boolean endBatchEdit() { mBatch++; return super.endBatchEdit(); } @Override public boolean beginBatchEdit() { mBatch--; return super.beginBatchEdit(); } }; } @Override public boolean onCheckIsTextEditor() { return true; } /** *      . */ public void setText(@NonNull String newText) { mEditable = Editable.Factory.getInstance().newEditable(newText); mImm.restartInput(this); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP && mBatch == 0) { int cursorPosition = 0; //    (   ) // notify keyboard that cursor position has changed. mImm.viewClicked(this); mImm.updateSelection(this, cursorPosition, cursorPosition, cursorPosition, cursorPosition); mImm.updateSelection(this, cursorPosition, cursorPosition, -1, -1); } return super.onTouchEvent(event); } private class MyTextWatcher implements TextWatcher { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { Log.d("CustomView", "Current text: " + mEditable); } @Override public void afterTextChanged(Editable editable) {} } } 

Total


Creating your own editor component that is not inherited from EditText is a rather rare task. But if you come across it, you have to engage in keyboard support. As it was already written in the article, the easiest way is to use a ready-made implementation, which is in the SDK. But if for some reason it does not fit, then it is necessary first of all to rely on the methods described in the article - this is the basis. You can then read more documentation. And the most productive way is to look into the source code of TextView.

Source: https://habr.com/ru/post/320788/


All Articles