Android
Kotlin
AndroidStudio

Kotlinでインクリメンタルサーチをやってみる

インクリメンタルサーチとは

そもそもインクリメンタルサーチとはなんぞや:yum:

インクリメンタルサーチ(Wikipedia)

アプリケーションにおける検索方法のひとつ。 検索したい単語をすべて入力した上で検索するのではなく、入力のたびごとに即座に候補を表示させる。 逐語検索、逐次検索とも。

アプリでよく使われている検索方法ですね。
早速やってみます。

準備

記事ではRecyclerViewを使用して、ツールバーに検索ボックス(SearchView)を実装してみます。

app/build.gradle
dependencies {
    compile 'com.android.support:recyclerview-v7:26.1.0'
}
styles.xml
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar">
    /// 省略
</style>

SearchView

検索用のメニューを作成します。

search.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/menu_search"
        android:title="検索"
        android:actionViewClass="android.widget.SearchView"
        android:showAsAction="always"/>
</menu>



SearchView.OnQueryTextListenerから検索文字を取得します。

val searchView = menu.findItem(R.id.menu_search).actionView as SearchView
    searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        .
        .
        .

  
今回はインクリメンタルで検索したいので、onQueryTextChangeから
検索文字が変更されるたびに文字を受け取るようにします。

ItemListActivity.kt
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.search, menu)
    if (menu != null) {
        val searchView = menu.findItem(R.id.menu_search).actionView as SearchView
        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(text: String?): Boolean {
                // 検索キーが押下された
                Log.d(TAG, "submit text: $text")
                return false
            }

            override fun onQueryTextChange(text: String?): Boolean {
                // テキストが変更された
                Log.d(TAG, "change text: $text")
                val itemListFragment = fragmentManager.findFragmentById(R.id.container)
                if (itemListFragment is ItemListFragment && text != null) {
                    itemListFragment.searchRequest(text)
                }
                return false
            }

        })
    }
    return super.onCreateOptionsMenu(menu)
}


検索文字を受け取ったら検索対象にフィルターをかけて更新します。

ItemListFragment.kt
fun searchRequest(text: String) {
    val adapter = adapter
    if (adapter != null) {
        adapter.data = items.filter { it.contains(text) }
        adapter.notifyDataSetChanged()
    }
}


実行結果

search_ss.gif


テキストの入力の度に検索結果を表示させることができました:smile:

コード

以下、最終的なソースコードです。
レイアウトは各々で作成してみてください。

ItemListAdapter.kt
class ItemListAdapter(private val context: Context,
                      var data: List<String>) : RecyclerView.Adapter<ItemListAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
        val inflater = LayoutInflater.from(context)
        return ViewHolder(inflater.inflate(R.layout.item, parent, false))
    }

    override fun getItemCount(): Int {
        return data.size
    }

    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
        holder?.title = data[position]
    }

    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private var titleTextView: TextView = itemView.findViewById(R.id.title_text)

        var title: String
            get() = titleTextView.text.toString()
            set(value) {
                titleTextView.text = value
            }
    }

}


ItemListFragment.kt
class ItemListFragment : Fragment() {

    private var mView: View? = null
    private var recyclerView: RecyclerView? = null
    private val items: MutableList<String> = mutableListOf()
    private var adapter: ItemListAdapter? = null

    init {
        for (i in 1..100) {
            items.add("アイテム$i")
        }
    }

    override fun onCreateView(inflater: LayoutInflater?,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View {
        super.onCreateView(inflater, container, savedInstanceState)

        mView = inflater!!.inflate(R.layout.fragment_item_list, container, false)
        val linerLayoutManager = LinearLayoutManager(mView!!.context)
        recyclerView = mView!!.findViewById(R.id.recycler_view)
        recyclerView!!.layoutManager = linerLayoutManager

        val dividerDecoration = DividerItemDecoration(recyclerView!!.context, linerLayoutManager.orientation)
        recyclerView!!.addItemDecoration(dividerDecoration)

        return mView!!
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        adapter = ItemListAdapter(mView!!.context, items)
        recyclerView!!.adapter = adapter
    }

    fun searchRequest(text: String) {
        val adapter = adapter
        if (adapter != null) {
            adapter.data = items.filter { it.contains(text) }
            adapter.notifyDataSetChanged()
        }
    }
}


ItemListActivity.kt
class ItemListActivity : Activity() {

    companion object {
        private val TAG = ItemListActivity::class.java.simpleName
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_item_list)

        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        toolbar.title = "検索"
        setActionBar(toolbar)

        if (savedInstanceState == null) {
            val itemListFragment = ItemListFragment()
            val transaction = fragmentManager.beginTransaction()
            transaction.replace(R.id.container, itemListFragment)
            transaction.commit()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.search, menu)
        if (menu != null) {
            val searchView = menu.findItem(R.id.menu_search).actionView as SearchView
            searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(text: String?): Boolean {
                    Log.d(TAG, "submit text: $text")
                    return false
                }

                override fun onQueryTextChange(text: String?): Boolean {
                    Log.d(TAG, "change text: $text")
                    val itemListFragment = fragmentManager.findFragmentById(R.id.container)
                    if (itemListFragment is ItemListFragment && text != null) {
                        itemListFragment.searchRequest(text)
                    }
                    return false
                }
            })
        }
        return super.onCreateOptionsMenu(menu)
    }
}

参考

Toolbar に 13行で SearchView を実装する