flavorによっては自由入力だったり択一だったりあるいはそもそも固定値なのでViewがなかったりということが弊社のギョームアプリでは多々あり、そういったView側の実装を考慮することなくActivity/Fragmentの側からViewIdを一切知ることなくrequestFocus(requestFocusFromTouch)するのが目標です。
コード
BindingAdapter
前回の発展です。今回もBindingAdapterに頑張ってもらいます。と言っても加えるコードは極々短いものです。
requestFocusFromTouchしたいViewに"focusId"と"focusTarget"という2つのattributeを定義します。
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です。
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だけです。
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 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の中で必須入力のチェックを行い、空であればその入力欄に相当する識別子へ通知します。
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 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画面に収まっているのでわかりづらいですね…
実際は必須入力が全て入力されるまで確定ボタン押せないとか、そういう方が親切だと思います。