实际开发中发现该View写得有问题,所有重写了一个,链接
由于我接手现在开发的app的时候用的Spinner是 https://github.com/jaredrummler/MaterialSpinner 这位大神的,所以里面一些解决问题的思路是参考这位大神的,先感谢他.
效果图
实现思路
1,继承TextView,内置一个PopupWindow用于弹出列表
2,PopupWindow的跟布局为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): 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()
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 search_spinner_ss = (SearchSpinner) findViewById(R.id.search_spinner_ss);
这是非常不爽的,所以提供了一个StringSearchSpinner.其实非常简单,就是继承SearchSpiner,设置泛型为String
class StringSearchSpinner : SearchSpinner{
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