2021年5月1日土曜日

AndroidのSpinnerをちょっと変えてみる。

AndroidのUIのひとつにSpinnerがあります。

いくつかの候補から対象を選ぶときに最適なドロップダウン(プルダウン)リストが主な使い道です。見た目は違いますが、iOSならUIPickerViewがそれに相当します。

こんなの。



ところがこのSpinner、表示内容に対してwidthが十分取れないと残念な見た目になってしまいます。

ドロップダウンのリスト部分は画面に被って表示されますから、画面幅を超えない限り長くても大丈夫ですが、選択されたアイテムが表示されるSpinner自体はレイアウトに左右されます。

そこでどうしたいのかというと、ドロップダウンのリスト部分と選択されたアイテムの短縮形を表示したいのです。「ジミ・ヘンドリックス」を選ぶと「ジミヘン」になるようにね。


♪♪♪


標準的なSpinnerがセットアップされている前提です。


♪♪♪


まずは、ドロップダウンのリスト部分と選択されたアイテムのペアを格納するデータクラスを作成します。
package bar.foo.spinnersample
data class CustomSpinnerHolder(val name: String, val shortName: String) {
}
値を入れて、取り出すだけなので処理は空っぽです。もちろん、凝った表示が必要なら処理を書きます。

Spinnerとリスト部分のレイアウトは基本的に変わりませんが、それぞれ名前(id)をつけます。

<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/custom_spinner_short_name"
style="?attr/spinnerDropDownItemStyle"
android:singleLine="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee" />
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/custom_spinner_name"
style="?attr/spinnerDropDownItemStyle"
android:singleLine="true"
android:layout_width="match_parent"
android:layout_height="?attr/dropdownListPreferredItemHeight"
android:ellipsize="marquee" />


専用のカスタムアダプターを作成します。
package bar.foo.spinnersample
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.TextView
class CustomSpinnerAdapter(val context: Context, var dataSource: List<CustomSpinnerHolder>) : BaseAdapter() {
private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
//region SpinnerAdapter
/**
* Gets a View that displays in the drop down popup the data at the specified position in the data set.
*/
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View
val vh: ItemHolder
if (convertView == null) {
view = inflater.inflate(R.layout.spinner_artist_dropdown, parent, false)
vh = ItemHolder(view)
view?.tag = vh
} else {
view = convertView
vh = view.tag as ItemHolder
}
vh.label.text = dataSource.get(position).name
return view
}
//endregion
//region Adapter
/**
* How many items are in the data set represented by this Adapter.
*/
override fun getCount(): Int {
return dataSource.size;
}
/**
* Get the data item associated with the specified position in the data set.
*/
override fun getItem(position: Int): Any? {
return dataSource[position];
}
/**
* Get the row id associated with the specified position in the list.
*/
override fun getItemId(position: Int): Long {
return position.toLong();
}
/**
* Get a View that displays the data at the specified position in the data set.
*/
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View
val vh: SelectedItemHolder
if (convertView == null) {
view = inflater.inflate(R.layout.spinner_artist, parent, false)
vh = SelectedItemHolder(view)
view?.tag = vh
} else {
view = convertView
vh = view.tag as SelectedItemHolder
}
vh.label.text = dataSource.get(position).shortName
return view
}
//endregion
//region PRIVATE METHODS
private class SelectedItemHolder(row: View?) {
val label: TextView
init {
label = row?.findViewById(R.id.custom_spinner_short_name) as TextView
}
}
private class ItemHolder(row: View?) {
val label: TextView
init {
label = row?.findViewById(R.id.custom_spinner_name) as TextView
}
}
//endregion
}


あとは、データの型をStringからドロップダウンのリスト部分と選択されたアイテムのペアを格納するデータクラスに変更して、ArrayAdapterをカスタムアダプターに変更すればOKです。
package bar.foo.spinnersample
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.Toast
class MainActivity : AppCompatActivity() {
//region ITEMS ON SCREEN
private lateinit var spArtist: Spinner
//endregion
//region ITEMS ON SPINNER
val spinnerItems = listOf<CustomSpinnerHolder>(
CustomSpinnerHolder("ジミ・ヘンドリックス", "ジミヘン"),
CustomSpinnerHolder("エリック・クラプトン", "クラプトン"),
CustomSpinnerHolder("スティーヴィー・レイ・ヴォーン", "SRV"),
CustomSpinnerHolder("フランク・ザッパ", "ザッパ"),
CustomSpinnerHolder("ピート・タウンゼント", "ピート"),
CustomSpinnerHolder("スティーヴ・クロッパー", "クロッパー"),
)
//endregion
//region LIFECYCLE METHODS OF FRAGMENT
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
spArtist = findViewById<Spinner>(R.id.spnner_artist)
val adapter = CustomSpinnerAdapter(this, spinnerItems)
spArtist.adapter = adapter
spArtist.onItemSelectedListener = spinnerItemSelectedListener()
}
//endregion
//region ACTION EVENT
/**
* Spinnerが操作されたとき
*/
private inner class spinnerItemSelectedListener : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
when(parent?.id) {
R.id.spnner_artist -> {
Toast.makeText(this@MainActivity, spinnerItems[position].name, Toast.LENGTH_SHORT).show()
}
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
//region ACTION EVENT
}
view raw MainActivity.kt hosted with ❤ by GitHub


これでこんな感じでいけます。

GitHubにこのサンプルを置いておきました。

やっぱりこれだよ、これ。