はじめに
AndroidのTabLayout
+ ViewPager
で作られた画面上で、EditText
を使った際、
EditText
の文字終端部分でソフトウェアキーボードの移動キー(left/right arrow)を押すと
タブが切り替わるという現象に出会いました。
(↓見辛いですがこんな感じ)
-
EditText
内は自由にカーソル移動できてほしい - タブは切り替わってほしくない
という2つを満たす方法を今回は探してみました。
動作環境
この記事での動作環境および開発環境は以下となります。
- Android Studio 3.0.1
- Nexus 5X
- Android O (8.1.0)
ベースコード
問題切り分けのために、まずは最低限のコードを用意しました。
3つのタブにそれぞれ同じレイアウト( EditText
のみを設置)の Fragment
を詰めます。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewPager = this.findViewById(R.id.viewPager) as ViewPager
viewPager.adapter = object : FragmentPagerAdapter(this.supportFragmentManager) {
override fun getCount(): Int = 3
override fun getItem(position: Int): Fragment = TabFragment()
override fun getPageTitle(position: Int): CharSequence = "tab ${position + 1}"
}
val tabLayout = this.findViewById(R.id.tabLayout) as TabLayout
tabLayout.setupWithViewPager(viewPager)
}
}
class TabFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater?.inflate(R.layout.fragment_tab, container, false)
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.takefumiota.tablayouttest.MainActivity">
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</android.support.constraint.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/editText"
android:hint="@string/app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="3" />
</LinearLayout>
これを動かすと、冒頭にあったように、ソフトウェアキーボードでカーソルを移動していると、
文字終端(空文字の場合は常に)で意図せずタブが切り替わる場面に遭遇します。
一体何が
最初は何が起こったのか分からず震えるだけでしたが、心を落ち着けて少し調べてみました。
まず、EditText
は TextView
を継承しています。( AppCompatEditText
も TintableBackgroundView
をimplementした EditText
なので同様)
TextView
のKeyDownイベントの挙動として以下のようになっています。
doKeyDown
関数にで KEY_EVENT_NOT_HANDLED
でなければキーイベントが回収されたことになることがわかるかと思います。
http://tools.oesf.biz/android-8.1.0_r1.0/xref/frameworks/base/core/java/android/widget/TextView.java#7093
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
final int which = doKeyDown(keyCode, event, null);
if (which == KEY_EVENT_NOT_HANDLED) {
return super.onKeyDown(keyCode, event);
}
return true;
}
さらに、上記で実行されている doKeyDown
関数は以下となります。(長いので超一部抜粋)
http://tools.oesf.biz/android-8.1.0_r1.0/xref/frameworks/base/core/java/android/widget/TextView.java#7201
private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
...
if (mMovement != null && mLayout != null) {
boolean doDown = true;
if (otherEvent != null) {
try {
boolean handled = mMovement.onKeyOther(this, (Spannable) mText, otherEvent); // ★☆★☆★
doDown = false;
if (handled) {
return KEY_EVENT_HANDLED;
}
} catch (AbstractMethodError e) {
// onKeyOther was added after 1.0, so if it isn't
// implemented we need to try to dispatch as a regular down.
}
}
if (doDown) {
if (mMovement.onKeyDown(this, (Spannable) mText, keyCode, event)) { // ★☆★☆★
if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) {
mPreventDefaultMovement = true;
}
return KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD;
}
}
...
}
return mPreventDefaultMovement && !KeyEvent.isModifierKey(keyCode)
? KEY_EVENT_HANDLED : KEY_EVENT_NOT_HANDLED;
}
★☆★☆★
がついているところに注目すると、 mMovement
の関数(mMovement.onKeyOther
, mMovement.onKeyDown
)がtrueのとき、この関数は KEY_EVENT_NOT_HANDLED
以外を返しています。
また、移動キーでこのイベントが発火し、上記に当てはまらない場合、関数の最後で KEY_EVENT_NOT_HANDLED
が返却され、結果さらに上位のViewにイベントが引き渡されていきます。
要はコレが原因 なわけです。
さて、この mMovement
ですが、 EditText
では ArrowKeyMovementMethod
が設定されます。
http://tools.oesf.biz/android-8.1.0_r1.0/xref/frameworks/base/core/java/android/text/method/ArrowKeyMovementMethod.java
ここから先は細かい話になりますのでソースを読んでいただければと思いますが、端折りますと以下のようになります。
-
EditText
にてカーソル移動をArrowKeyMovementMethod(mMovement)
に要求 -
BaseMovementMethod.onKeyOther(...)
,BaseMovementMethod.onKeyDown(...)
内にて、BaseMovementMethod.handleMovementKey(...)
を実行 - 各移動イベントに応じた実行関数をコール
- 移動に成功すればtrue, 失敗すればfalseを返却
4番の成功/失敗ですが、カーソルがそれ以上要求された方向に移動できないときはfalseを返すようになっているようでした。
e.g.) http://tools.oesf.biz/android-8.1.0_r1.0/xref/frameworks/base/core/java/android/text/Selection.java#213
この結果がずーーーーーっと上まで波及していき、 EditText
を突き抜けて ViewPager
までイベントが到達してしまった結果、
意図しない場面でタブ切り替えが発生します。
( ´ー`)フゥー...
対応版コード
では不要なタブ切り替えイベントを抑制するように修正してみます。(
class TabFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
- return inflater?.inflate(R.layout.fragment_tab, container, false)
+ return inflater?.inflate(R.layout.fragment_tab, container, false)?.apply {
+ (this.findViewById(R.id.editText) as EditText).movementMethod = object : ArrowKeyMovementMethod() {
+ override fun handleMovementKey(widget: TextView?, buffer: Spannable?, keyCode: Int, movementMetaState: Int, event: KeyEvent?): Boolean {
+ val handled = super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event)
+ return when (keyCode) {
+ KeyEvent.KEYCODE_DPAD_LEFT -> true
+ KeyEvent.KEYCODE_DPAD_RIGHT -> true
+ else -> handled
+ }
+ }
+ }
+ }
}
}
以上です。
問題だったのは、キーイベントが右移動または左移動のときに処理したことにならず、
親のViewまでイベントが到達してしまうことでしたので、
それらのキーが入力された際は、今までの処理を行ったあとに常にtrueを返すようにしました。
この結果が以下ですが、文字を打っている最中にタブが不意に切り替わってしまうことを防げていそうです。
(画像見づらくて大変申し訳ありません)
おわりに
データ入力画面を作るときに困っていたのでなんとかしたいと思いなんとか漕ぎ着けましたが、
もっとスマートな方法がある気しかしません。
知見ございましたら優しいツッコミいただければとお思います。