Spinnerの選択肢取得にSinmpleCursorAdapterを使う

今回から写真メモアプリのレコード編集画面を作っていく。

写真メモアプリでは、メモのカテゴリとサブカテゴリをDBに登録した選択肢から選ぶようにしたい。Androidで選択肢から選ぶ仕組みはSpinnerが用意されているのでこれを使う。

SpinnerはListViewと同様にAdapterViewを親クラスに持っており、表示はほぼ同様の仕組みが使える。そこで以前の記事「SimpleCursorAdapterを用いたDBデータのリスト表示」に倣って、SimpleCursorAdapterを使ってSpinnerの表示を試みた。

スポンサーリンク

レイアウトXML

新しい画面は「Activity追加と画面遷移の実装」と同様の操作で作成。

今回は画面の上半分、日付・カテゴリ・サブカテゴリの配置をした。ConstraintLayoutなのでAndroid StudioのGUI画面エディタでそこそこ画面を作れるのだが、思い通りにならない時もあってゴミのXMLタグが付くことも多く、そういう時はテキストエディタ直接編集でゴミを取り去りながら作成した。

layout/activity_edit.xml

<?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=".EditActivity">

  <TextView
      android:id="@+id/date"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_marginStart="8dp"
      android:layout_marginTop="3dp"
      android:padding="3dp"
      android:textSize="24sp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  <TextView
      android:id="@+id/catLabel"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="16dp"
      android:padding="5dp"
      android:text="カテゴリ:"
      android:textSize="20sp"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/date" />

  <Spinner
      android:id="@+id/category"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_marginStart="8dp"
      android:layout_marginEnd="8dp"
      android:padding="5dp"
      android:spinnerMode="dialog"
      app:layout_constraintBottom_toBottomOf="@+id/catLabel"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@+id/catLabel"
      app:layout_constraintTop_toTopOf="@+id/catLabel" />

  <TextView
      android:id="@+id/subCatLabel"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="16dp"
      android:padding="5dp"
      android:text="サブカテゴリ:"
      android:textSize="20sp"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/catLabel" />

  <Spinner
      android:id="@+id/subCategory"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_marginStart="8dp"
      android:layout_marginEnd="8dp"
      android:padding="5dp"
      android:spinnerMode="dialog"
      app:layout_constraintBottom_toBottomOf="@+id/subCatLabel"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@+id/subCatLabel"
      app:layout_constraintTop_toTopOf="@+id/subCatLabel" />

</android.support.constraint.ConstraintLayout>

Spinner itemのレイアウトXML

Spinnerの中身のレイアウトは、通常表示(選択された状態)と一覧表示(選択肢が全部表示された状態)の2種類を指定する必要がある。それぞれAndroid標準レイアウトがあって、

  • 通常表示: android.R.layout.simple_spinner_item
  • 一覧表示: android.R.layout.simple_spinner_dropdown_item

にて参照可能。ソースコードも取得出来る。例えばsimple_spinner_dropdown_itemは以下。
https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/layout/simple_spinner_dropdown_item.xml

上記2つのソースを確認してみると、通常表示はTextViewだが、一覧表示はCheckedTextViewとなっている。Android標準レイアウトを参考に、以下通常と一覧用の2つのレイアウトを定義した。

layout/spinner_item.xml→通常

<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/spinVal"
    style="?android:attr/spinnerItemStyle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="20sp" />

layout/spinner_dropdown_item.xml→一覧

<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/spinVal"
    style="?android:attr/spinnerDropDownItemStyle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    android:textSize="24sp" />

Activityのjavaソース

EditActivityのソース全体は以下。

EditActivity.java

package com.example.photologger;

import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.SimpleCursorAdapter;
import android.widget.Spinner;
import android.widget.TextView;

public class EditActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener {
    private long mRecID;
    private long mCatID;
    private long mSubCatID;
    private ContentResolver mContentResolver;
    private ThumbnailCursorAdapter mCurAdapter;
    private Spinner mCatSpinner;
    private Spinner mSubCatSpinner;
    private SimpleCursorAdapter mSubCatAdapter;

    static void selectSpinnerByID(Spinner spinner, long id) {
        int selPos = -1;
        for (int idx=0;idx<spinner.getCount();idx++){
            if (spinner.getItemIdAtPosition(idx)==id){
                selPos = idx;
                break;
            }
        }
        if ( selPos >= 0 ) {
            spinner.setSelection(selPos);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_edit);

        mContentResolver = getContentResolver();

        Intent i = getIntent();
        mRecID = i.getLongExtra("id", -1);

        // レコードの取得
        Cursor cursor = mContentResolver.query(MyContentProvider.RECORD_URI, null, "_id = " + mRecID, null, null);
        cursor.moveToFirst();
        ((TextView)findViewById(R.id.date)).setText(cursor.getString(2)); // 日付
        mCatID = cursor.getInt(7);
        mSubCatID = cursor.getInt(8);

        // Category Spinner
        cursor = mContentResolver.query(MyContentProvider.CATEGORY_URI, null, null, null, null);
        cursor.moveToFirst();

        String[] from = {"Name"};
        int[] to = {R.id.spinVal};

        SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.spinner_item, cursor, from, to, 0);
        adapter.setDropDownViewResource(R.layout.spinner_dropdown_item);

        mCatSpinner = (Spinner)findViewById(R.id.category);
        mCatSpinner.setAdapter(adapter);
        selectSpinnerByID(mCatSpinner, mCatID);
        mCatSpinner.setOnItemSelectedListener(this);

        // SubCategory
        cursor = mContentResolver.query(MyContentProvider.SUBCATEGORY_URI, null, "CatID = " + mCatID, null, null);        
        mSubCatAdapter = new SimpleCursorAdapter(this, R.layout.spinner_item, cursor, from, to, 0);
        mSubCatAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
        mSubCatSpinner = (Spinner)findViewById(R.id.subCategory);
        mSubCatSpinner.setAdapter(mSubCatAdapter);
        selectSpinnerByID(mSubCatSpinner, mSubCatID);

        mSubCatSpinner.setOnItemSelectedListener(this);
    }

    /* Spinnerのクリックハンドラ */
    @Override
    public void onItemSelected(AdapterView<?> parent, View v, int position, long id) {
        Spinner spinner = (Spinner)parent;
        if ( spinner == mCatSpinner ) {
            mCatID = id;
            Cursor cur = mContentResolver.query(MyContentProvider.SUBCATEGORY_URI, null, "CatID = " + mCatID, null, null);
            mSubCatAdapter.swapCursor(cur);
        }
        if ( spinner == mSubCatSpinner ) {
            mSubCatID = id;
        }
        //        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
    }

    @Override
    public void onNothingSelected(AdapterView<?> adapterView) {
        Spinner spinner = (Spinner)adapterView;
        if ( spinner == mCatSpinner ) {
            selectSpinnerByID(mCatSpinner, mCatID);
        }
        if ( spinner == mSubCatSpinner ) {
            selectSpinnerByID(mSubCatSpinner, mSubCatID);
        }
    }
}

以下、コードの解説をパートごとに。

CursorAdapter用のCursorをContentProviderから取得する

ContentProviderの仕組みが提供するContentResolver#queryメソッドを用いて、画面のViewの表示内容を取得したりSpinnerの表示を担うCursorの取得も行うこととした。この仕組みはListViewなどがバックグラウンドでquery動作を行う場合のみ必須で、今回の場合はLogDBAdapterクラスの変数を直接持ってもいいのだが、ソースの読みやすさの観点からインタフェースが共通の方が良いと考えてこの仕様にした。

SpinnerをDBのIDで選択する – selectSpinnerByIDメソッド

Spinnerクラスで特定の選択肢を選択させる手段は、setSelectionメソッドしか用意されていない。このsetSelectionメソッドは先頭を0番目として何番目かを指定するという代物で、DBのIDを指定すると該当する場所を選択してくれるような便利な物ではない。CursorAdapterではCursorに_idカラムが必須とされているのだから逆引きも標準であって欲しいものだが、それは自分でやらないと駄目のようだ。

というわけで、_idカラムの値でSpinnerの項目選択を行うselectSpinnerByIDメソッドを作成した。実現方法はSpinnerの一番上からgetItemIdAtPositionメソッドを使って順番に紐付いている_idカラムの値を取得し、一致する位置のインデックスを取得するという原始的なもの。

onCreateメソッドでは、このselectSpinnerByIDメソッドでDBレコードに該当する項目の選択を行っている。

Spinnerのクリックハンドラ

Spinnerの値が変更された時にはEditActivityクラスのメンバ変数も変更されるようにしたい。そのためにはMainActivityにListViewのクリックハンドラを追加した時にOnItemClickListenerを追加したように、Activityクラス自身にハンドラを追加する必要がある。やり方はListViewだろうがSpinnerだろうが同じだろうと予想したが、Spinnerの場合はOnItemClickListenerではなくOnItemSelectedListenerを追加する必要がある。

OnItemSelectedListenerの追加にあたっては、onItemSelectedとonNothingSelectedの2つのメソッドのオーバーライドを行った。

onItemSelectedメソッドのパラメータparentには選択されたSpinnerオブジェクトが渡されるため、OnCreateで2つのSpinnerをメンバ変数に保存しておき、どちらが選ばれたかを判定している。特にカテゴリが選択された場合はサブカテゴリの選択肢の中身を入れ替える必要があって、その動きも実装してある。

onNothingSelectedメソッドはOnCreateメソッドでSpinnerの選択をしないと呼ばれるのかと当初思い、だったらonNothingSelectedが呼ばれたタイミングでレコード設定値に基づいてSpinnerの選択をしてもいいのかなと思ったのだが、予想に反してonNothingSelectedは呼ばれず勝手にSpinnerの先頭が選択されるという結果になった。ので、onNothingSelectedメソッドの中身は適当。

問題点:Spinnerを未選択に出来ない

カテゴリを選択するとサブカテゴリの選択肢を更新するコードをは上記で追加出来たが、サブカテゴリの選択状態は未選択にしたい。もっと言うと、レコードを新規作成した時はカテゴリも未選択の状態が正しい。しかし、Spinnerクラスに未選択にするメソッドは存在しない…。ということは「未選択」という選択肢を用意して、それが選ばれている時を未選択と見做すしかないだろう。デフォルトで一番先頭が選ばれるというSpinnerの特性に鑑みると、一番先頭に未選択のレコードを入れなくてはならないという事になる。

実行結果

上記ソースでの実行結果。まずは、初期状態。2つのSpinnerが表示されている。

「カテゴリ」のSpinnerをクリックした状態。ん?CheckedTextViewだから選択肢にラジオボタンが表示される筈だが…

「サブカテゴリ」のSpinnerをクリックした状態。やっぱりラジオボタンは表示されない。

う〜ん、なんだか色々思い通りにならないが、長くなったので今回はここまで。次回へ続く。

コメント