安卓实现带搜索框的Spinner

由于我接手现在开发的app的时候用的Spinner是 https://github.com/jaredrummler/MaterialSpinner 这位大神的,所以里面一些解决问题的思路是参考这位大神的,先感谢他.

效果图

实现思路

    1,继承TextView,内置一个PopupWindow用于弹出列表

    2,PopupWindow的跟布局为RelativeLayout 

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">
    <EditText
        android:id="@+id/popup_search_spinner_et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="1dp"
        android:background="@drawable/search_bg"
        android:gravity="center_vertical"
        android:paddingEnd="1dp"
        android:paddingStart="1dp"
        android:singleLine="true"/>

    <TextView
        android:id="@+id/popup_search_spinner_tv"
        android:textColor="@android:color/black"
        android:gravity="center"
        android:ellipsize="end"
        android:singleLine="true"
        android:visibility="gone"
        android:text="@string/no_search_result"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <ListView
        android:id="@+id/popup_search_spinner_lv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/popup_search_spinner_et"
        android:divider="@null"
        android:dividerHeight="0dp"
        android:scrollbars="none"/>
</RelativeLayout>

    3,点击TextView的时候获取View当前在屏幕的位置,计算要View的上下高度从而计算弹出位置

val locations = IntArray(2)
        getLocationOnScreen(locations)
        x = locations[0]//记录x值和y值,在别的地方还需要用到
        y = locations[1]

        //移除RelativeLayout限制的所有规则
        removeRule(searchView)
        removeRule(listView)
        //当EditText有输入内容的时候,并且搜索到的内容列表不为空 或者 EditText没有内容
        //表示这个时候ListView里面绝对有数据
        if ((isSearch && !searchList.isEmpty()) || !isSearch) {
            //当处于搜索状态的时候,使用搜索列表的size计算PopupWindow需要弹出的高度,否则使用总数据
            val popupHeight = calculatePopupWindowHeight(if (isSearch) searchList else list)
            listView.adapter = adapter
            //当上方高度大于下方高度的时候
            if (isTop()) {
                //设置弹出动画
                popupWindow.animationStyle = topPopupAnim

                val searchParam = searchView.layoutParams as RelativeLayout.LayoutParams
                //设置EditText在RelativeLayout的底部
                searchParam.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                searchView.layoutParams = searchParam

                val listParam = listView.layoutParams as RelativeLayout.LayoutParams
                //设置ListView在EditText的上方
                listParam.addRule(RelativeLayout.ABOVE, searchViewResId)
                listView.layoutParams = listParam

                popupWindow.height = popupHeight
                popupWindow.showAtLocation(this, Gravity.START or Gravity.TOP, x, y - popupHeight)
            } else {
                //设置弹出动画
                popupWindow.animationStyle = bottomPopupAnim

                val param = listView.layoutParams as RelativeLayout.LayoutParams
                //设置ListView在EditText的下面
                param.addRule(RelativeLayout.BELOW, searchViewResId)
                listView.layoutParams = param

                popupWindow.height = popupHeight
                popupWindow.showAsDropDown(this)
            }
            //隐藏提示的View,显示ListView
            emptyTipView.visibility = View.GONE
            listView.visibility = View.VISIBLE
            listView.setSelection(listSelectIndex)
        } else {//当没有数据的时候
            val popupHeight = calculatePopupWindowHeight(searchList) + height
            removeRule(emptyTipView)
            val param = emptyTipView.layoutParams as RelativeLayout.LayoutParams
            if (isTop()) {
                popupWindow.animationStyle = topPopupAnim

                val searchParam = searchView.layoutParams as RelativeLayout.LayoutParams
                searchParam.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                searchView.layoutParams = searchParam

                param.addRule(RelativeLayout.ABOVE, searchViewResId)
                emptyTipView.layoutParams = param

                popupWindow.height = popupHeight
                popupWindow.showAtLocation(this, Gravity.START or Gravity.TOP, x, y - popupHeight)
            } else {
                popupWindow.animationStyle = bottomPopupAnim

                param.addRule(RelativeLayout.BELOW, searchViewResId)
                emptyTipView.layoutParams = param

                popupWindow.height = popupHeight
                popupWindow.showAsDropDown(this)
            }
            emptyTipView.visibility = View.VISIBLE
            listView.visibility = View.GONE
        }
 private fun removeRule(view: View) {
        val param = view.layoutParams as RelativeLayout.LayoutParams
        // 如果最低版本在16以上,可以直接调用removeRule(rule)
        param.addRule(RelativeLayout.BELOW, 0)
        param.addRule(RelativeLayout.ABOVE, 0)
        param.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 0)
        view.layoutParams = param
    }

    //屏幕高度-y坐标-控件高度
    private fun getBottomHeight(): Int = screenHeight - y - height

    //当当前控件所处在屏幕的y坐标减去通知栏的高度大于控件下方的高度的时候
    private fun isTop(): Boolean = y - statusBarHeight > getBottomHeight()

    //计算PopupWindow显示的高度
    private fun calculatePopupWindowHeight(list: ArrayList<T>): Int {
        //所有数据*item的高度+EditText的高度
        val listFullHeight = list.size * height + searchView.height
        if (isTop()) {
            var popupHeight = Math.min(listFullHeight, y)
            //当y值为PopupWindow的高度的时候
            if (popupHeight == y) {
                //减去通知栏的高度
                popupHeight = popupHeight - statusBarHeight
            }
            return popupHeight
        } else {
            var popupHeight = Math.min(listFullHeight, getBottomHeight())
            if (popupHeight == getBottomHeight()) {
                //留一部分用于显示阴影
                popupHeight = popupHeight - (height * 0.25).toInt()
            }
            return popupHeight
        }
    }

    4,弹出来后根据搜索的结果动态计算PopupWindow的高度和上次选择的item在本次搜索的list的index,并通过update方法改变高度

val searchText = searchView.text.toString()
                //当搜索的内容不为空的时候
                if (searchText.isNotEmpty()) {
                    searchList.clear()
                    //将条件符合的内容过滤出来
                    list.filter { it.toString().contains(searchText, isIgnoreCase) }.forEach { searchList.add(it) }
                    adapter.list = searchList
                    adapter.notifyDataSetChanged()
                    isSearch = true
                    //当列表不为空的时候
                    if (!searchList.isEmpty()) {
                        //计算PopupWindow实际弹出的高度
                        val popupHeight = calculatePopupWindowHeight(searchList)
                        if (isTop()) {
                            popupWindow.update(x, y - popupHeight, popupWindow.width, popupHeight)
                        } else {
                            popupWindow.update(x, y + height, popupWindow.width, popupHeight)
                        }
                        listView.visibility = View.VISIBLE
                        emptyTipView.visibility = View.GONE
                        //计算上个选择的item在这个list所在的index.这个计算公式有点复杂,留在下面
                        val index = searchSelectIndex
                        //-1的时候,表示上次选择的item不在这个列表里面
                        if (index != -1) {
                            adapter.setSelect(searchSelectIndex)
                            listView.setSelection(searchSelectIndex)
                        } else {
                            adapter.cancelSelect()
                        }
                    } else {
                        //列表为空,显示用于提示的TextView
                        val popupHeight = calculatePopupWindowHeight(searchList) + height
                        removeRule(emptyTipView)
                        val param = emptyTipView.layoutParams as RelativeLayout.LayoutParams
                        if (isTop()) {
                            param.addRule(RelativeLayout.ABOVE, searchViewResId)
                            emptyTipView.layoutParams = param
                            popupWindow.update(x, y - popupHeight, popupWindow.width, popupHeight)
                        } else {
                            param.addRule(RelativeLayout.BELOW, searchViewResId)
                            emptyTipView.layoutParams = param
                            popupWindow.update(x, y + height, popupWindow.width, popupHeight)
                        }
                        emptyTipView.visibility = View.VISIBLE
                        listView.visibility = View.GONE
                    }
                } else {
                    //当搜索内容为空的时候,显示全部数据
                    val popupHeight = calculatePopupWindowHeight(list)
                    if (isTop()) {
                        popupWindow.update(x, y - popupHeight, popupWindow.width, popupHeight)
                    } else {
                        popupWindow.update(x, y + height, popupWindow.width, popupHeight)
                    }
                    adapter.list = list
                    adapter.notifyDataSetChanged()
                    isSearch = false
                    adapter.setSelect(selectIndex)
                    listView.setSelection(selectIndex)
                    listView.visibility = View.VISIBLE
                    emptyTipView.visibility = View.GONE
                }

关于searchSelectIndex和selectIndex

    1,selectIndex

    想要计算searchSelectInde,需要先计算selectIndex

var selectIndex = 0
        get() {
            //当点击搜索list的时候,就会记录搜索list的数据
            //当点击全部数据的list的时候,就会清除该list的数据
            //它的作用是,当搜索后有选择某个item,然后清除EditText里面的内容后
            //计算出上次在搜索的list选择的item在全部数据的list的index
            //因为存搜索数据的list会根据EditText输入的内容变化而变化,所以才用另一个list才保存搜索的数据
            if (!tmpSearchList.isEmpty()) {
                //listSelectIndex的作用很简单,监听ListView的setOnItemSelect事件,记录position
                val selectField = tmpSearchList[listSelectIndex].toString()
                val countList = ArrayList<Int>()
                for (i in 0 until tmpSearchList.size) {
                    //记录相同数据的个数
                    if (tmpSearchList[i].toString() == selectField) {
                        countList.add(i)
                    }
                    if (i == listSelectIndex) {
                        break
                    }
                }
                //如果只有一个toString后相同的,代表这个在列表是唯一的
                if (countList.size == 1) {
                    var index = -1
                    for (i in 0 until list.size) {
                        if (list[i].toString() == selectField) {
                            //返回全部数据的list所在的index
                            index = i
                            break
                        }
                    }
                    return index
                } else {//如果这个数据toString后出现相同的数据
                    var num = countList.size
                    var index = -1
                    for (i in 0 until list.size) {
                        if (list[i].toString() == selectField) {
                            num--
                        }
                        if (num == 0) {
                            index = i
                            break
                        }
                    }
                    return index
                }
            } else {//当空的时候,表示这个listSelectIndex来源于全部数据的index
                return listSelectIndex
            }
        }
        //调用set方法的时候,直接设置全部数据的index,并清除其他所有list
        set(selectIndex) {
            if (selectIndex >= list.size) {
                throw IndexOutOfBoundsException("index:$selectIndex")
            }
            field = selectIndex
            listSelectIndex = selectIndex
            searchView.setText("")
            searchList.clear()
            tmpSearchList.clear()
            adapter.list = list
            adapter.notifyDataSetChanged()
            adapter.setSelect(field)
            listView.setSelection(field)
            text = list[field].toString()
        }

    2,searchSelectIndex

protected var searchSelectIndex: Int = 0
        get() {
            //获取上次选择item的值
            val selectField = list[selectIndex].toString()
            var count = 0
            for (i in 0..selectIndex) {
                //可能会有相同的值,所以记录一下
                if (list[i].toString() == selectField) {
                    count++
                }
            }
            if (count == 0) {
                return -1
            }
            var index = -1
            for (i in 0 until searchList.size) {
                if (searchList[i].toString() == selectField) {
                    count--
                }
                if (count == 0) {
                    index = i
                    break
                }
            }
            return index
        }

其他细节

    泛型

        因为必须保证几个list用的泛型的一样的,所以在findViewById的时候必须指定泛型

    final SearchSpinner<String> search_spinner_ss = (SearchSpinner<String>) findViewById(R.id.search_spinner_ss);

       这是非常不爽的,所以提供了一个StringSearchSpinner.其实非常简单,就是继承SearchSpiner,设置泛型为String

    class StringSearchSpinner : SearchSpinner<String>{
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
        constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    }

     至于为什么要用泛型,看过android自带的Spinner和ArrayAdapter的源码的人都知道,android就是这样设计的,所以 跟着官方一样设计

    重写该View做自己的初始化操作

    该View采用模板方法设计模式,所以很多初始化操作子类都可以重写指定方法做自己的初始化操作

    例如想更改PopupWindow弹出的布局,可以重写 initLayoutResId和initViewResId这2个方法,分别设置布局和3个控件对应的 id

源码下载地址

关于Spinner实现item选中有背景的实现,可以看这里.上传后才发现代码有问题,如果需要继承Adapter的话需要给Adapter加个open关键字,并且需要在Adapter的初始化方法将objects赋给list

猜你喜欢

转载自blog.csdn.net/android_upl/article/details/79761047
今日推荐