How to implement abbreviation search to SearchView

321 Views Asked by At

I have a Fragment containing a list of strings (company names) which works with SearchView. However, due to many companies having long names, is there a way where I can type in an abbreviation for a company name rather than having to type in the whole company name? 'FTSE 150' and 'FTSE 250' are self-explanatory hence don't need abbereviations.

Abberviations for company names

  • GSK - GlaxoSmithKline plc
  • HSX - Hiscox Ltd
  • IHG - InterContinental Hotels Group plc
  • MKS - Marks & Spencer Group plc

strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="company_names">
        <item>@string/glaxosmithkline_plc</item>
        <item>@string/hiscox_ltd</item>
        <item>@string/intercontinental_hotels_group_plc</item>
        <item>@string/marks_and_spencer_group_plc</item>
        <item>@string/ftse_150</item>
        <item>@string/ftse_250</item>
    </string-array>

    <string name="glaxosmithkline_plc">GlaxoSmithKline plc</string>
    <string name="hiscox_ltd">Hiscox Ltd</string>
    <string name="intercontinental_hotels_group_plc">InterContinental Hotels Group plc</string>
    <string name="marks_and_spencer_group_plc">Marks &amp; Spencer Group plc</string>
    <string name="ftse_150">FTSE 150</string>
    <string name="ftse_250">FTSE 250</string>
</resources>

fragment class

class MyFragment : androidx.fragment.app.Fragment() {

    private var mAdapter: MyListAdapter? = null

    private lateinit var mRecyclerView: androidx.recyclerview.widget.RecyclerView

    private var mTwoPane: Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.layout_recyclerview, container, false)
        mTwoPane = (activity as androidx.fragment.app.FragmentActivity).findViewById<View>(R.id.detail_container) != null

        mRecyclerView = view.findViewById(R.id.recyclerView_list)
        mRecyclerView.setHasFixedSize(true)
        mRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this.activity)
        mRecyclerView.addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(Objects.requireNonNull<Context>(context), LinearLayout.VERTICAL))

        val myList = ArrayList<Companies>()

//        val items = resources.getStringArray(R.array.company_names)
//        for (n in items) {
//            val company = Companies(0, "", "")
//            myList.add(company)
//        }

        val companyA = Companies(1, "GlaxoSmithKline plc", "GSK")
        val companyB = Companies(2, "Hiscox Ltd", "HSX")
        val companyC = Companies(3, "InterContinental Hotels Group plc", "IHG")
        val companyD = Companies(4, "Marks & Spencer Group plc", "MKS")
        val companyE = Companies(5, "FTSE 150", "")
        val companyF = Companies(6, "FTSE 250", "")

        val myList = DatabaseHandler(this.context!!)
        myList.insertData(companyA)
        myList.insertData(companyB)
        myList.insertData(companyC)
        myList.insertData(companyD)
        myList.insertData(companyE)
        myList.insertData(companyF)

        mAdapter = MyListAdapter(activity!!, myList, mTwoPane)

        mRecyclerView.adapter = mAdapter

        return view
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        val mInflater = Objects.requireNonNull<androidx.fragment.app.FragmentActivity>(activity).menuInflater
        mInflater.inflate(R.menu.menu_search, menu)

        val searchView = searchitem.actionView as SearchView

        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(query: String): Boolean {
                return false
            }

            override fun onQueryTextChange(newText: String): Boolean {
                mAdapter!!.filter.filter(newText)
                return false
            }
        })

        super.onCreateOptionsMenu(menu, inflater)
    }
}

MyListAdapter class

class MyListAdapter(private val mCtx: Context, private val myList: MutableList<Companies>,
                          private val
mTwoPane: Boolean) : androidx.recyclerview.widget.RecyclerView.Adapter<MyListAdapter
.CompanyViewHolder>(), Filterable {
    private var myListFull = myList.toMutableList()

    private val companyFilter = object : Filter() {
        override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
            val filteredList = ArrayList<Companies>()

            when {
                constraint == null || constraint.isEmpty() -> filteredList.addAll(myListFull)
                else -> {
                    val filterPattern = constraint.toString().toLowerCase().trim { it <= ' ' }

                    for (item in myListFull) {
                        when {
                            item.companyName!!.toLowerCase().contains(filterPattern) ->
                                filteredList.add(item)
                        }
                    }
                }
            }

            val results = Filter.FilterResults()
            results.values = filteredList
            return results
        }

        override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults?) {
            myList.clear()
            myList.addAll(results!!.values as List<Companies>)
            notifyDataSetChanged()
        }
    }

    inner class CompanyViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView
    .ViewHolder(itemView) {
        var tvTitle: TextView = itemView.findViewById(R.id.tv_RVItem)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CompanyViewHolder {
        val inflater = LayoutInflater.from(mCtx)
        val v = inflater.inflate(R.layout.recyclerview_item_textview, parent, false)
        return CompanyViewHolder(v)
    }

    override fun onBindViewHolder(holder: CompanyViewHolder, position: Int) {
        val product = myList[holder.adapterPosition]

        holder.tvTitle.text = product.companyfuName
    }

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

    override fun getFilter(): Filter {
        return companyFilter
    }
}

enter image description here

enter image description here

enter image description here

UPDATES

Custom model class

data class Companies (val id: String, val fullName: String, val abbreviation: String)

updated Adapter class

class MyListAdapter(private val mCtx: Context,
                    private val mCompanies: MutableList<Companies>,
                    private val mTwoPane: Boolean) : androidx.recyclerview.widget.RecyclerView.Adapter<MyListAdapter
.CompanyViewHolder>(), Filterable {
    private val mCompaniesFull = mCompanies.toMutableList()

    private val companyFilter = object : Filter() {
        override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
            val filteredList = if (constraint == null || constraint.isEmpty()) {
                mCompanies
            } else {
                val filterText = constraint.toString()
                mCompanies.filter { it.companyName.matchesIgnoreCase(filterText) || it.companyAbbreviation.matchesIgnoreCase(filterText) }
            }

            val results = Filter.FilterResults()
            results.values = filteredList
            return results
        }

        override fun publishResults(constraint: CharSequence?, results: Filter.FilterResults?) {
            mCompanies.clear()
            mCompanies.addAll(results!!.values as List<Companies>)
            notifyDataSetChanged()
        }
    }

    private fun String.matchesIgnoreCase(otherString: String): Boolean {
        return this.toLowerCase().contains(otherString.trim().toLowerCase())
    }

    inner class CompanyViewHolder(itemView: View) : androidx.recyclerview.widget.RecyclerView
    .ViewHolder(itemView) {
        var tvTitle: TextView = itemView.findViewById(R.id.tv_RVItem)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CompanyViewHolder {
        val inflater = LayoutInflater.from(mCtx)
        val v = inflater.inflate(R.layout.recyclerview_item_textview, parent, false)
        return CompanyViewHolder(v)
    }

    override fun onBindViewHolder(holder: CompanyViewHolder, position: Int) {
        val product = mCompanies[holder.adapterPosition]
        holder.tvTitle.text = product.companyName
    }

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

    override fun getFilter(): Filter {
        return companyFilter
    }
}

updated Fragment class

class MonFragment : androidx.fragment.app.Fragment() {

    private var mAdapter: MyListAdapter? = null

    private lateinit var mRecyclerView: androidx.recyclerview.widget.RecyclerView

    private var mTwoPane: Boolean = false

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setHasOptionsMenu(true)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.layout_recyclerview, container, false)
        mTwoPane = (activity as androidx.fragment.app.FragmentActivity).findViewById<View>(R.id.detail_container) != null

        mRecyclerView = view.findViewById<RecyclerView>(R.id.recyclerView_list)
        mRecyclerView.setHasFixedSize(true)
        mRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this.activity)
        mRecyclerView.addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(Objects.requireNonNull<Context>(context), LinearLayout.VERTICAL))

        mCompanies.add(Companies("GlaxoSmithKline plc", "GSK"))
        mCompanies.add(Companies("Hiscox Ltd", "HSX"))
        mCompanies.add(Companies("InterContinental Hotels Group plc", "IHG"))
        mCompanies.add(Companies("Marks & Spencer Group plc", "MKS"))
        mCompanies.add(Companies("FTSE 150", ""))
        mCompanies.add(Companies("FTSE 250", ""))

        mAdapter = MyListAdapter(activity!!, mCompanies, mTwoPane)

        mRecyclerView.adapter = mAdapter

        return view
    }

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        val mInflater = Objects.requireNonNull<androidx.fragment.app.FragmentActivity>(activity).menuInflater
        mInflater.inflate(R.menu.menu_search, menu)

        val searchitem = menu.findItem(R.id.action_search)
        val searchView = searchitem.actionView as SearchView

        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(query: String): Boolean {
                return false
            }

            override fun onQueryTextChange(newText: String): Boolean {
                mAdapter!!.filter.filter(newText)
                mAdapter!!.notifyDataSetChanged()

                return false
            }
        })

        super.onCreateOptionsMenu(menu, inflater)
    }
}
1

There are 1 best solutions below

22
Jakub Licznerski On

Following the discussion in comments I'd suggest two approaches for storing the data:

  1. Device storage either with SharedPreferences or databases: SQLite, Room (they are lightweight local storage databases, Internet connection is not needed).
  2. Device memory (not recommended) as a List of hardcoded objects initialized either in MainActivity or Fragment's onCreate method (dependent on the usage).

As @CoolMind suggested you should implement a custom model class CompanyName(id, fullName, abbreviation) and then filter by the alternative of fullName and abbreviation.

EDIT: In MyListAdapter get rid of myListFull as it does nothing and put:

private val stockFilter = object : Filter() {
        override fun performFiltering(constraint: CharSequence?): Filter.FilterResults {
            val filteredList = if (constraint == null || constraint.isEmpty()) myList
                else {
                   val filterText = constraint.toString()
                   myList.filter { it.fullName.matchesIgnoreCase(filterText ) || it.abbreviation.matchesIgnoreCase(filterText) }
                }
            val results = Filter.FilterResults()
            results.values = filteredList
            return results
        }
}

Also add in the MyListAdapter class this extension function:

private fun String.matchesIgnoreCase(otherString: String): Boolean {
    return this.toLowerCase().contains(otherString.trim().toLowerCase())
}