はじめに
AndroidのSpinnerの選択位置は、初期値0ですが、
これを動的に変えたい(前回選択していた位置を復元したい等)ときにちょっとハマったので備忘録として残しておきます。
前提知識
Spinnerは初回起動時に、onItemSelected()イベントが発生します。
初回起動というのは、具体的にSpinnerのインスタンスが生成され、アダプタやリスナーがセットされて画面に表示されるタイミングを指します。
ActivityやFragmentのonCreate()やonViewCreated()などでSpinnerのセットアップを行った直後が該当します。
これにより、アイテム選択が切り替わっていないのに、onItemSelected()内の処理が実行されてしまいます。
private Spinner mSpinner;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mSpinner = (Spinner) view.findViewById(R.id.spinner);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item, arrGroupName);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_item);
mSpinner.setAdapter(adapter);
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
// onItemSelected()は画面表示時にも呼ばれてしまう
Toast.makeText(requireContext(), "選択された項目: " + selectedItem, Toast.LENGTH_SHORT).show();
// 以降は、アイテム選択が切り替わったときの処理(省略)
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
}
一般的な対策として、Focusable()を初回起動判定用フラグとして扱うようにすることで回避できます。
private Spinner mSpinner;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mSpinner = (Spinner) view.findViewById(R.id.spinner);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item, arrGroupName);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_item);
mSpinner.setAdapter(adapter);
// 【New】初回動作の対応
mSpinner.setFocusable(false);
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
// 【New】初回の動作
if (mSpinner.isFocusable() == false) {
// 初回のonItemSelected()時は、何もしない
mSpinner.setFocusable(true);
return;
}
// 初回以降の動作
Toast.makeText(requireContext(), "選択された項目: " + selectedItem, Toast.LENGTH_SHORT).show();
// 以降は、アイテム選択が切り替わったときの処理(省略)
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
}
ハマったところ
onItemSelected()の初回起動対策を行ったコードに対して、
Spinnerが画面表示される際、初期選択状態を動的に変えたいときに問題が起こりました。
(今回やりたかったことは、画面表示時に前回選択していた位置を復元したい というものでした)
以下は、試しに書いてみたコードです。
private Spinner mSpinner;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mSpinner = (Spinner) view.findViewById(R.id.spinner);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item, arrGroupName);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_item);
mSpinner.setAdapter(adapter);
// 【New】前回選択していたアイテムの選択位置を取得する処理(非同期で行われる)
getPreviousSelectedPosition(new PositionCallback() {
@Override
public void onPositionRetrieved(int position) {
if (position > 0) {
mSpinner.setSelection(position);
}
// Spinnerのアイテム選択イベントリスナー設定を行う
setupSpinnerListener();
}
});
}
// 【New】Spinnerのアイテム選択イベントリスナー設定
private void setupSpinnerListener() {
// 初回動作の対応
mSpinner.setFocusable(false);
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
// 初回の選択イベントをスキップするための処理
if (!mSpinner.isFocusable()) {
mSpinner.setFocusable(true);
return;
}
String selectedItem = (String) adapterView.getItemAtPosition(position);
Toast.makeText(requireContext(), "選択された項目: " + selectedItem, Toast.LENGTH_SHORT).show();
// 以降は、アイテム選択が切り替わったときの処理(省略)
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
// 処理なし
}
});
}
Spinnerの選択位置が0のときだけ、起動時のonItemSelected()が呼ばれなくなった
上記のコードで動作確認をすると、Spinnerの選択位置が0のだけ、起動時のonItemSelected()が呼ばれなくなってしまいました。
これにより、画面表示時に呼ばれるonItemSelected()イベントを回避するためのコードが実行されなくなってしまい、
アイテム選択を切り替えたときに必要な処理が走らなくなってしまいました。
// 初回動作の対応
mSpinner.setFocusable(false);
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
// 初回の選択イベントをスキップするための処理
if (!mSpinner.isFocusable()) {
/**
*【問題点】初回の選択イベントが走らなくなってしまい、
* アイテム選択を切り替えた時に、この分岐に入ってしまう。
* そのため、アイテム選択が切り替わったときの処理が走ってくれない。
*/
mSpinner.setFocusable(true);
return;
}
// 以降は、アイテム選択が切り替わったときの処理(省略)
setOnItemSelectedListener()でイベントを設定するタイミングが変わってしまったのが原因
前回選択していた位置を非同期で取得後にsetOnItemSelectedListener()を呼ぶように変更したのが原因でした。
前提知識にも書いていますが、
Spinnerで初回起動時にonItemSelected()イベントが発生するのは、
具体的にSpinnerのインスタンスが生成され、アダプタやリスナーがセットされて画面に表示されるタイミングです。
つまり、onResume()イベント発生後にonItemSelected()イベントが呼ばれる仕組みになっています。
前回選択していた位置の取得は非同期処理で行っているため、
onResume()イベント以降にsetOnItemSelectedListener()を行うようになってしまいました。
本来は、onResume()のタイミングでonItemSelected()が呼ばれるわけですが、
今回のケースでは、まだsetOnItemSelectedListener()を行っていないため、
起動時のonItemSelected()が呼ばれなかった というわけです。
前回選択していた位置が0以外の場合はonItemSelected()が呼ばれた
ちなみに、前回選択していた位置が0以外の場合はonItemSelected()が呼ばれました。
これは推測ですが、
初期選択位置である0から、setSelection()で選択位置が変わったことによるものだと思われます。
※今回のケースでは、初期値である0から変わらないケースだけ、onItemSelected()が呼ばれないため
// 【New】前回選択していたアイテムの選択位置を取得する処理(非同期で行われる)
getPreviousSelectedPosition(new PositionCallback() {
@Override
public void onPositionRetrieved(int position) {
if (position > 0) {
// このタイミングで初期値である0から選択位置が変わるため、
// onItemSelected()が呼ばれたと推測
mSpinner.setSelection(position);
}
// Spinnerのアイテム選択イベントリスナー設定を行う
setupSpinnerListener();
}
});
対策
初回起動時の対策として行っていたsetFocusable()の処理を、
前回選択していたアイテムの選択位置が0以外のときだけ行うように修正しました。
mSpinner.setFocusable(false);
前回選択していたアイテムの位置が、
- 初期値から変更なし:起動時のonItemSelected()は呼ばれない
- 初期値から変更あり:起動時のonItemSelected()は呼ばれる
ので、初回起動判定として使用していたFocusable()のフラグは、
選択位置が初期値から変更があるときだけ設定するようにして対策できました。
対策したコード
// 【New】Spinnerのアイテム選択イベントリスナー設定
private void setupSpinnerListener() {
// ここから 対策コード
if(mSpinner.getSelectedItemPosition() != 0) {
// 初回動作の対応
mSpinner.setFocusable(false);
}
// ここまで 対策コード
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
// 初回の選択イベントをスキップするための処理
if (!mSpinner.isFocusable()) {
mSpinner.setFocusable(true);
return;
}
String selectedItem = (String) adapterView.getItemAtPosition(position);
Toast.makeText(requireContext(), "選択された項目: " + selectedItem, Toast.LENGTH_SHORT).show();
// 以降は、アイテム選択が切り替わったときの処理(省略)
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
// 処理なし
}
});
補足
ここで紹介した現象をまとめると、
onCreateView()イベントの外でsetOnItemSelectedListener()を実行したことで、onItemSelected()イベントを受け取れなくなったことになるのですが、
SharedPreferencesを用いて前回のアイテム選択位置を取得すれば、わざわざ非同期処理にする必要もなく今回の問題も発生しません。
僕の環境上の制約で、SharedPreferencesが使えなかっただけなので、
大抵の方は、SharedPreferencesやDataStoreを採用するのが良いと思います。
Googleは、SharedPreferencesの代替としてDataStoreの利用を強く推奨しています。
ソースコード全体
private Spinner mSpinner;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mSpinner = (Spinner) view.findViewById(R.id.spinner);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_spinner_item, arrGroupName);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_item);
mSpinner.setAdapter(adapter);
// 【New】前回選択していたアイテムの選択位置を取得する処理(非同期で行われる)
getPreviousSelectedPosition(new PositionCallback() {
@Override
public void onPositionRetrieved(int position) {
if (position > 0) {
mSpinner.setSelection(position);
}
// Spinnerのアイテム選択イベントリスナー設定を行う
setupSpinnerListener();
}
});
// 【New】Spinnerのアイテム選択イベントリスナー設定
private void setupSpinnerListener() {
// ここから 対策コード
if(mSpinner.getSelectedItemPosition() != 0) {
// 初回動作の対応
mSpinner.setFocusable(false);
}
// ここまで 対策コード
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
// 初回の選択イベントをスキップするための処理
if (!mSpinner.isFocusable()) {
mSpinner.setFocusable(true);
return;
}
String selectedItem = (String) adapterView.getItemAtPosition(position);
Toast.makeText(requireContext(), "選択された項目: " + selectedItem, Toast.LENGTH_SHORT).show();
// 以降は、アイテム選択が切り替わったときの処理(省略)
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
// 処理なし
}
});