DataBindingを使っていてもrequestFocusしたい


flavorによっては自由入力だったり択一だったりあるいはそもそも固定値なのでViewがなかったりということが弊社のギョームアプリでは多々あり、そういったView側の実装を考慮することなくActivity/Fragmentの側からViewIdを一切知ることなくrequestFocus（requestFocusFromTouch）するのが目標です。

# コード

## BindingAdapter

[前回](http://qiita.com/teracy/items/82ff174d7616092f4318)の発展です。今回もBindingAdapterに頑張ってもらいます。と言っても加えるコードは極々短いものです。
requestFocusFromTouchしたいViewに"focusId"と"focusTarget"という2つのattributeを定義します。

```java
public class DataBindingAdapter {
    // region Spinner等択一のViewで現在選択されている位置を取るためのBindingAdapterと双方向バインドのためのInverseBindingAdapter
    @BindingAdapter(value = {"selectedPosition", "selectedPositionAttrChanged"}, requireAll = false)
    public static void bindSelectedValue(Spinner spinner, int position, final InverseBindingListener textAttrChanged) {
        spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                textAttrChanged.onChange();
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {

            }
        });
        if (spinner.getAdapter().getCount() > position) {
            spinner.setSelection(position, false);
        }
    }

    @InverseBindingAdapter(attribute = "selectedPosition", event = "selectedPositionAttrChanged")
    public static int captureSelectedPosition(Spinner spinner) {
        return spinner.getSelectedItemPosition();
    }
    // endregion

    @BindingAdapter("android:text")
    public static void setTextViewText(TextView textView, String text) {
        if (text != null) {
            textView.setText(text);
        } else {
            textView.setText("");
        }
    }

    /**
     * requestFocusFromTouchのBindingAdapter
     *
     * @param view   対象のView
     * @param id     View自身のrequestFocusFromTouch用識別子（View.Idとは別物）
     * @param target ViewModel側から指定する、requestFocusFromTouchしたいViewの識別子（やはりView.Idとは別物）
     */
    @BindingAdapter(value = {"focusId", "focusTarget"})
    public static void requestFocusFromTouch(View view, int id, int target) {
        if (target == 0 || id != target) {
            return;
        }
        view.requestFocusFromTouch();
    }
}
```

## TextWatcher

EditTextに双方向バインドする際に無限ループにならないようにするためのTextWatcherです。

```java
public abstract class SimpleTextWatcher implements TextWatcher {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

    @Override
    public void afterTextChanged(Editable s) {
        onTextChanged(s.toString());
    }

    public abstract void onTextChanged(String value);
}
```

## ViewModel

今回も手抜きでViewModelだけです。

```java
public class MainViewModel extends BaseObservable {
    public static final int TARGET_ANSWER1 = 1;
    public static final int TARGET_ANSWER2 = 2;
    public static final int TARGET_ANSWER3 = 3;

    private final Context context;

    private int focusTarget;
    private String answer1;
    private String answer2;
    private String answer3;
    private int selectedPosition;
    private String message;
    private boolean isEditMode = false;

    public MainViewModel(Context context) {
        this.context = context;
    }

    public SimpleTextWatcher answer2Watcher = new SimpleTextWatcher() {
        @Override
        public void onTextChanged(String value) {
            isEditMode = true;
            setAnswer2(value);
            isEditMode = false;
        }
    };

    public SimpleTextWatcher answer3Watcher = new SimpleTextWatcher() {
        @Override
        public void onTextChanged(String value) {
            isEditMode = true;
            setAnswer3(value);
            isEditMode = false;
        }
    };

    @Bindable
    public int getFocusTarget() {
        return focusTarget;
    }

    @Bindable
    public String getAnswer1() {
        return answer1;
    }

    @Bindable
    public String getAnswer2() {
        return answer2;
    }

    @Bindable
    public String getAnswer3() {
        return answer3;
    }

    @Bindable
    public int getSelectedPosition() {
        return selectedPosition;
    }

    @Bindable
    public String getMessage() {
        return message;
    }

    public void setFocusTarget(int focusTarget) {
        this.focusTarget = focusTarget;
        notifyPropertyChanged(BR.focusTarget);
    }

    public void setAnswer1(String answer1) {
        this.answer1 = answer1;
        notifyPropertyChanged(BR.answer1);
    }

    public void setAnswer2(String answer2) {
        this.answer2 = answer2;
        if (!isEditMode) {
            notifyPropertyChanged(BR.answer2);
        }
    }

    public void setAnswer3(String answer3) {
        this.answer3 = answer3;
        if (!isEditMode) {
            notifyPropertyChanged(BR.answer3);
        }
    }

    public void setSelectedPosition(int selectedPosition) {
        this.selectedPosition = selectedPosition;
        notifyPropertyChanged(BR.selectedPosition);
        setAnswer1(this.context.getResources().getStringArray(R.array.spinner_value)[selectedPosition]);
    }

    public void setMessage(String message) {
        this.message = message;
        notifyPropertyChanged(BR.message);
    }
}
```

## レイアウト

必須入力のSpinnerとEditTextを1つずつ、加えて任意入力のEditText、入力を確定させるためのButtonを定義します。先ほどBindigAdapterで定義したattributeを利用してそのViewが「どの項目の入力欄か」を定義しておきます。

```xml
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >
    <data>
        <import type="com.example.trs.requestfocussample.MainViewModel"/>
        <variable
            name="viewModel"
            type="com.example.trs.requestfocussample.MainViewModel"/>
        <variable
            name="onClickListener"
            type="android.view.View.OnClickListener"/>
    </data>
    <LinearLayout
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:windowSoftInputMode="stateHidden|adjustPan"
        android:focusable="true"
        android:focusableInTouchMode="true"
        android:orientation="vertical"
        tools:context="com.example.trs.requestfocussample.MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="設問1：性別（必須）"
            />
        <Spinner
            android:id="@+id/answer1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:entries="@array/spinner_code"
            app:selectedPosition="@={viewModel.selectedPosition}"
            app:focusId="@{MainViewModel.TARGET_ANSWER1}"
            app:focusTarget="@{viewModel.focusTarget}"
            />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:text="設問2：推しアイドル（必須）"
            />
        <EditText
            android:id="@+id/answer2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="text"
            android:maxLines="1"
            app:addTextChangedListener="@{viewModel.answer2Watcher}"
            android:text="@{viewModel.answer2}"
            app:focusId="@{MainViewModel.TARGET_ANSWER2}"
            app:focusTarget="@{viewModel.focusTarget}"
            />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:text="設問3：月間課金額（任意）"
            />
        <EditText
            android:id="@+id/answer3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="numberDecimal"
            android:maxLines="1"
            app:addTextChangedListener="@{viewModel.answer3Watcher}"
            android:text="@{viewModel.answer3}"
            app:focusId="@{MainViewModel.TARGET_ANSWER3}"
            app:focusTarget="@{viewModel.focusTarget}"
            />

        <Button
            android:id="@+id/button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:text="回答する"
            android:onClick="@{onClickListener}"
            />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:text="@{viewModel.message}"
            />
    </LinearLayout>
</layout>
```

MainActivity.javaはDataBindingの設定とOnClickListenerを定義しておきます。
OnClickListenerの中で必須入力のチェックを行い、空であればその入力欄に相当する識別子へ通知します。

```java
public class MainActivity extends AppCompatActivity {
    private MainViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        this.viewModel = new MainViewModel(this);
        this.viewModel.setFocusTarget(MainViewModel.TARGET_ANSWER1);
        binding.setViewModel(this.viewModel);
        binding.setOnClickListener(this.onClickListener);
    }

    public View.OnClickListener onClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // EditText以外も対象なのでnullチェック忘れずに
            if (getCurrentFocus() != null && getCurrentFocus().getWindowToken() != null) {
                InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
                inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
            }
            viewModel.setMessage("");
            // viewModel.getSelectedPosition() < 1で判断してもいいけど今回は「Viewの実装を一切知らずに処理する」がコンセプトなので
            if (TextUtils.isEmpty(viewModel.getAnswer1())) {
                viewModel.setMessage("性別を選択してください");
                viewModel.setFocusTarget(MainViewModel.TARGET_ANSWER1);
                return;
            }
            if (TextUtils.isEmpty(viewModel.getAnswer2())) {
                viewModel.setMessage("推しアイドルを入力してください");
                viewModel.setFocusTarget(MainViewModel.TARGET_ANSWER2);
                return;
            }
            // 本来ならここで送信処理とか
            viewModel.setMessage("ご回答ありがとうございました");
            viewModel.setFocusTarget(0);
        }
    };
}
```

## リソース

今回もSpinnerは表示用と入力値用の2つのstring-arrayを定義しておきます。

```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="spinner_code">
        <item>--未回答--</item>
        <item>男性</item>
        <item>女性</item>
        <item>その他</item>
    </string-array>
    <string-array name="spinner_value">
        <item />
        <item>男性</item>
        <item>女性</item>
        <item>その他</item>
    </string-array>
</resources>
```

## 実行

入力確定したときに必須入力の項目の入力内容が空白だった場合、その箇所をfocusするようになっています。
1画面からはみ出るようなUIだとわかりやすいのですが、今回全てが1画面に収まっているのでわかりづらいですね…

実際は必須入力が全て入力されるまで確定ボタン押せないとか、そういう方が親切だと思います。

![sample.gif](https://qiita-image-store.s3.amazonaws.com/0/21354/b7e36eb1-7fda-c57e-6508-a7da9b4595af.gif)