はじめに
やりたいこと
EditTextで、特定の内容しか入力を受け付けたくないようなケース。
ソフトキーボードを非表示にして、アプリ画面でボタンを用意、ボタンからの入力しか受け付けないような画面(具体的には電卓アプリのような画面)を考えています。
素直に「TextViewを弄ればいいじゃん」という話でもあるのですが、せっかく、EditTextのフォーカス状態や編集状態が一目瞭然なUIがあるのに、それを一から自前で実装するというのも勿体無い。
そう、ここで大事なのは、キーボードは非表示にしつつ「カーソルは表示させたままにしたい」ということ。
単にキーボードを非表示にするだけならば簡単なのだけれども、これがまた一筋縄ではいかない話でした。
確認環境
Compile SDK API レベル: 24
シミュレータ:
Android 5.1(Lollipop)
Android 6.0 (Marshmallow)
カーソルが出なくてもいいなら
カーソルが出なくてよく、単にキーボード表示を抑止したいだけなら、下記のように簡単に抑止できます。
EditText editText = (EditText)findViewById(R.id.editText1);
editText.setKeyListener(null);
EditText editText = (EditText)findViewById(R.id.editText1);
editText.setRawInputType(InputType.TYPE_NULL);
EditText editText = (EditText)findViewById(R.id.editText1);
editText.setInputType(InputType.TYPE_NULL);
ちなみに、Android 2.x の時代は、この辺のやり方でカーソルも表示されていたみたいです。
その時のままだったなら、こんなに苦労しなくても済んだのに。。。
カーソルを表示しつつつキーボード表示を抑止する方法
ということで本題。
参考にしたのはこの辺り。
- Disable input method of EditText but keep cursor blinking
- Android development: Custom keyboard
- Disable keyboard on EditText
- Issue 27609: EditText cursor missing when inputtype null
方法その1
EditText editText = (EditText)findViewById(R.id.editText1);
if (Build.VERSION.SDK_INT >= 11) {
editText.setRawInputType(InputType.TYPE_CLASS_TEXT);
editText.setTextIsSelectable(true);
} else {
editText.setRawInputType(InputType.TYPE_NULL);
editText.setFocusable(true);
}
今となってはAPI10以下をサポートすることもないだろうし、if (Build.VERSION.SDK_INT >= 11)
はなくてもいいかな?
if文の中の方も、setRawInputTypeの方は、InputType.TYPE_NULL でないことが大事で(TYPE_NULLだとカーソルがでない)、それ以外であればなんでも良いっぽいようなコメントも見かけ、実質的には、setTextIsSelectable(true)
が肝みたいです。
(逆に「一律TYPE_CLASS_TEXTで大丈夫なのか?」と思ったけれども、setInputTypeでなくsetRawInputTypeだからなのか、特に問題ないみたい。パスワード表示も、複数行テキストもちゃんと引き継がれていました)
方法その2
EditText editText = (EditText)findViewById(R.id.editText1);
editText.setOnTouchListener(new OnTouchListener() {
@Override public boolean onTouch(View v, MotionEvent event) {
EditText edittext = (EditText) v;
int inType = edittext.getInputType(); // Backup the input type
edittext.setInputType(InputType.TYPE_NULL); // Disable standard keyboard
edittext.onTouchEvent(event); // Call native handler
edittext.setInputType(inType); // Restore input type
return true; // Consume touch event
}
});
こちらの方法は、onTouchイベントをキーボードのでないTYPE_NULLのInputTypeで呼んでおいて、後でしれっと元のInputTypeに戻しておくってことですね。でもって、以降の余計な処理を走らせないように、戻り値はtrueで返しておく、と。
方法その2の問題点
私が実際にアプリで採用しているのは「その2」の方法なのですが、今回の記事をまとめるに当たって改めて色々と実験してみたところ、幾つか問題があることがわかりました。
(逆に、昔のメモでは「その1」はうまくいかなかったとあったのだけれど、今回、改めて試してみたところ、何の問題もなく動作してしまった。。。何か見落としがあるのか、昔の手順が何かミスっていたのか、、、)。
問題その1(カーソルが移動しない・範囲選択できない・コピペできない)
テキストエリアをタップ(ダブルタップ・ロングタップを含む)しても、カーソル移動ができませんし、範囲選択もできません。InputType.TYPE_NULL の状態で onTouchEventを読んだ時点で、カーソル位置がクリアされてれしまうためだと思われます。
が、これが本当に問題なのかと言われると、入力内容を制限するためにキーボードを非表示にしているという観点からすると、想定外の入力がなされる可能性が減るという意味で、デメリットではなくむしろメリットになるケースもあるのではないかと。ケースバイケースですね(実際、私のアプリでは、むしろカーソルが勝手に動かされると困るので、問題にはなっていません)。
ただ、このままだとカーソル位置は先頭で固定されてしまいますので、末尾に移動しておきましょう。
edittext.setOnTouchListener(new OnTouchListener() {
@Override public boolean onTouch(View v, MotionEvent event) {
EditText edittext = (EditText) v;
int inType = edittext.getInputType(); // Backup the input type
edittext.setInputType(InputType.TYPE_NULL); // Disable standard keyboard
edittext.onTouchEvent(event); // Call native handler
edittext.setInputType(inType); // Restore input type
// カーソルを常に末尾にする
edittext.setSelection(edittext.getText().length());
return true; // Consume touch event
}
});
ソフトキーボードやタッチ以外の方法でカーソル移動を許可するなどで、末尾以外にカーソルがある可能性がある場合は、InputType同様バックアップ変数に退避しておき、後で戻す方法で。
(この場合も、あくまでも画面に「←」「→」ボタンなどを用意して移動させるなどのケースを想定しており、いずれにせよ、タッチ位置によるカーソル移動はできませんのでご注意を)
// カーソルの初期位置は末尾に
edittext.setSelection(edittext.getText().length());
edittext.setOnTouchListener(new OnTouchListener() {
@Override public boolean onTouch(View v, MotionEvent event) {
EditText edittext = (EditText) v;
// カーソル位置を退避
int inStartSel = edittext.getSelectionStart();
int inEndSel = edittext.getSelectionEnd();
int inType = edittext.getInputType(); // Backup the input type
edittext.setInputType(InputType.TYPE_NULL); // Disable standard keyboard
edittext.onTouchEvent(event); // Call native handler
edittext.setInputType(inType); // Restore input type
// カーソル位置を戻す
edittext.setSelection(inStartSel,inEndSel);
return true; // Consume touch event
}
});
問題その2(SingleLineモードだと、文字選択がされた時にキーボードが出てしまう)
問題その1で書いた、範囲選択ができないとか、コピペメニューが出てこないとかは、android:inputTypeやsetInputTypeを特に指定していなかった時や、android:inputType="textMultiLine"を指定していた時の話で、android:inputType="text" など、SingleLineのInputTypeを指定している時は、範囲選択ができてしまいます。だけでなく、選択画面に遷移した際にキーボードが表示されてしまいます。
左が「方法その2」のコードそのままだった場合、右が「問題その1」で書いたカーソル位置を固定する処理を入れている場合の画面。
何故、シングルラインとマルチラインで、こんなところの挙動が変わってしまうのかは不明。
passwordタイプの時はまた挙動が違う
ちなみに、android:inputType="passwordText"などのパスワードタイプを指定していた時はまた挙動が違っていて、範囲選択(というか全選択?)はできてしまいますがキーボードは表示されません。また、カーソルの移動もできません。
この場合は、ロングタップを無効にしてしまえば、マルチラインの時とほぼ同じ挙動(カーソル動かない、範囲選択できない、コピペできない)で動かすことができます。
edittext.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return true;
}
});
何故、InputTypeのモードによってこんなにも挙動が変わってしまうのか、、、。
どっちの方法がいいのか?
結局どっちの方法がいいのか?
上述の通り、「方法その2」には幾つか問題があるので、「方法その1」、、、と言いたいところなのですが、実はそうとも言えず。
結論から言うと、「(タップでの)カーソルの移動やコピペがしたい」という場合は、「方法その1」がいいですが(というかそれしか選択肢がないですが)、「カーソル移動ができなくていい、コピペもできなくていい(ていうか、むしろ出来ない方がいい!)」という場合には、「方法その2」の方がいいです。
というのも、後述しますが、「方法その1」でコピペのアクションモードを完全に抑止することが極めて困難だからです(というか、結局、私が調べた狭い範囲では完全解決させることができませんでした、、、)。
ちなみに、では、カーソル移動やコピペは出来ない方がいいけど、マルチラインにはしたくないという場合は?
android:inputType="textMultiLine" にしておいた上で、android:maxLines="1",android:scrollHorizontally:"true" などの設定にして見た目だけ一行っぽくする。あとは、入力監視して、改行が入らないようにする(入力手段が限られるはずなので、割り切れば監視そのものを省略してしまうというのも手?)とかですかね。
もう一工夫
最初からキーボードが表示されているケースの対処
画面上の入力欄がこれしかない、または全てキーボードを表示させないパターンであれば問題はないけれども、通常のEditTextと共存する場合など、既にキーボードが出ている状態で該当のEditTextにフォーカス遷移してくると、(当然ながら)どちらの方法でもキーボードが表示されたままで入力もできてしまいまう。
したがって、このような場合は、フォーカスが当たった時にキーボードを閉じる処理を追加する必要があります。
edittext.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
EditText edittext = (EditText) v;
// EditTextのフォーカスが当たった場合
if (hasFocus == true) {
// ソフトキーボードを非表示にする
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
});
※ ちなみに、ふと「方法その1、方法その2のような面倒くさいことしなくても、この処理だけでもいいんじゃね?」と思って試してみたけれども、これだけだとバッチリキーボードが表示されてしまいました。
(キーボードを出す処理が動くのは、onFocusChangeイベントよりも後みたいです)
コピペ画面の抑制
「方法その1」の場合は、カーソル移動もできるし、コピペも出来るし、範囲選択もできます。
が、わざわざキーボードからの入力を抑止して独自の入力方法を用意するということは、コピペで予期せぬ内容を入力されるのも避けたいというケースは少なくないはずです。
けれども「方法その2」では(タップでの)カーソル移動もできなくなってしまうので、「方法その1」を採用した上で、コピペだけを抑止したい。
ということで、散々調べたのですが、完全解決には至らず。
ですが、ここまでは調べました、という履歴のために、無駄かもしれませんが記録を残しておきます。
この辺りを参考にしましたが、結局解決策には至っていない模様。各回答のコメントまで追っていくと、「not working」の英語がいたるところに出てきています(^^;)
- How to disable copy/paste from/to EditText
- EditText: Disable Paste/Replace menu pop-up on Text Selection Handler click event
ActionModeCallBack をオーバーライドする
Actionメニューを自前で編集するためのCallBackクラスをセットして、そこでアクションメニューを無効にしてしまう方法。
editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// TODO Auto-generated method stub
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// TODO Auto-generated method stub
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// TODO Auto-generated method stub
return false;
}
});
if (android.os.Build.VERSION.SDK_INT >= 23) {
editText.setCustomInsertionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// TODO Auto-generated method stub
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// TODO Auto-generated method stub
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// TODO Auto-generated method stub
return false;
}
});
}
これで、コピー&ペーストのメニューを出さなくすることはできるのですが、「if (android.os.Build.VERSION.SDK_INT >= 23)
」とあります通り、「setCustomInsertionActionModeCallback
」は、Android6.0以降でないと使えないんですよね。
でもって、この部分がなくて「setCustomSelectionActionModeCallback
」だけだとどうなるか?
(こっちは、API11から使えます)。
EditText内のクリックではアクションメニューが表示されなくなるのですが、カーソルの丸部分(Text Selection Handle と呼ぶらしい)をクリックするとやっぱりメニューが表示されてしまいます。
Text Selection Handle を非表示にする
それならば、と、Text Selection Handle を非表示にしてやるとどうなるか、と試してみましたが、、、。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<size
android:height="0dp"
android:width="0dp" />
</shape>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="text"
android:text="text""
android:ems="10"
android:textSelectHandle="@drawable/empty_cursor"
android:textSelectHandleLeft="@drawable/empty_cursor"
android:textSelectHandleRight="@drawable/empty_cursor"
android:id="@+id/editText" />
見た目的には丸いハンドルは見えなくなりましたが、ハンドルがあると思わしき部分をクリックすると、やはりペーストメニューが出てきてしまいました。
EditText のサブクラスを継承して、canPaste/isSuggestionsEnabled をオーバライドする
stackOverflow にもこの回答があったのですが、残念ながらcanPasteはプライベートメソッド。サブクラスで再定義してもオーバーライドは出来ないというのが結論のようです(stackOverflowでのやり取りを見ると、何故か、4.4ではオーバーライドできていたらしいですが、少なくとも5.1以降ではできなくなっているみたいです)
boolean canPaste()
{
return false;
}
@Override
public boolean isSuggestionsEnabled()
{
return false;
}
結論
諦めました(笑)!
Android6.0以上がデフォルトになるまで時代を待つか、それ未満のバージョンは対象外としてしまうか、コピペは許可しておいてInputFilterなどで入力内容を制限するか、カーソル移動はできないものと割り切って「方法その2」で対処するか。
実際問題、私が採用しているのは「方法その2」なわけですし、当面の間はその辺りで使い分けていくしかなさそうですね。
蛇足
android:inputType="none" と setInputType(InputType.TYPE_NULL)
今回の目的とは直接的には関係ないのですが、いろいろと試していく中で気がついたことというか、気になったこと。
どうも XMLで指定する android:inputType="none" は、setInputType(InputType.TYPE_NULL)と等価ではないみたいですね。
というか、android:inputType="none"
を指定して getInputTypeの値をデバッグで追っていったら、何も指定しなかった時と同じ、すなわちandroid:inputType="textMultiLine"
のときと同じ値がセットされていたですよ(Hex:0x20001 or Decimal: 131073 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE)。
これは仕様なのかバグなのか、、、。なんともすっきりしない話です。
このあたりでも散々議論されているけど。。。