效果图:
之前在这篇文章介绍了如何基于TextView实现带搜索框的Spinner
直到拿到项目中使用,才发现了各式各样的问题,想着解决这些问题太麻烦了,所以决定重写
现在看来,很庆幸当时决定重写,因为重写后很多地方的代码看起来不像之前那么绕,之前一个onClick方法写了一堆代码,现在的onClick方法也简化了很多
先初始化3个常用变量
val screenHeight = context.resources.displayMetrics.heightPixels
val statusBarHeight = getStatusBarHeight()
val elevationSize = 16f
private fun getStatusBarHeight():Int{
val resourceId = Resources.getSystem().getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
return Resources.getSystem().getDimensionPixelSize(resourceId)
}
return 0
}
这次的实现是基于LinearLayout,实现的思路和上次差不多,只是很多细节不一样
根View
1,设置LinearLayout的orientation为Horizontal
2,添加TextIView和ImageView分别用于显示文本和箭头
3,设置onClick
这里之所不使用TextView是因为要在TextView里面控制箭头旋转太麻烦了
private val textView : TextView
private val imageView : ImageView
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
textView = TextView(context)
textView.gravity = Gravity.CENTER_VERTICAL
//最大行数必须只能为1行
textView.maxLines = 1
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
textView.textColor = 0xff000000.toInt()
//设置结尾 ...
textView.ellipsize = TextUtils.TruncateAt.END
val param1 = generateDefaultLayoutParams() as LinearLayout.LayoutParams
param1.gravity = Gravity.CENTER_VERTICAL
//ImageView用剩下的都给TextView用
param1.weight = 1f
textView.layoutParams = param1
super.addView(textView)
imageView = ImageView(context)
imageView.setImageResource(R.drawable.search_down)
val param2 = LinearLayout.LayoutParams(dip(10), dip(10))
param2.gravity = Gravity.CENTER_VERTICAL
//设置左右margin
param2.marginStart = dip(2.5f)
param2.marginEnd = dip(2.5f)
imageView.layoutParams = param2
imageView.adjustViewBounds = true
super.addView(imageView)
setOnClickListener(this)
}
//旋转图片,true为重置,false为旋转
private fun animateArrow(isRelease: Boolean) {
if (isRelease) {
imageView.animate().rotation(0f).start()
} else {
imageView.animate().rotation(180f).start()
}
}
PopupWindow的根View依然是RelativeLayout,里面存放
1,EditText:用于搜素(下面使用popupEditText表示)
2,ListView:用于显示数据(下面用popupListView表示)
3,TextView:用于没有搜索结果的提示(下面用popupTextView表示)
private val popupWindow: PopupWindow
private val popupEditText : EditText
private val popupTextView : TextView
private val popupListView : ListView
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
popupWindow = PopupWindow(context)
popupWindow.isFocusable = true
popupWindow.isOutsideTouchable = true
popupWindow.setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.popup_search_spinner))
try {
//禁止输入法影响屏幕的高度,否则会导致PopupWindow显示的位置不准确
popupWindow.inputMethodMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
(context as Activity).window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
} catch (e: Exception) {
e.printStackTrace()
}
val popupRootView = LayoutInflater.from(context).inflate(R.layout.popup_search_spinner, null)
popupEditView = popupRootView.findViewById(R.id.popup_search_spinner_et)
popupListView = popupRootView.findViewById(R.id.popup_search_spinner_lv)
popupTextView = popupRootView.findViewById(R.id.popup_search_spinner_tv)
popupWindow.contentView = popupRootView
popupWindow.setOnDismissListener {
//当PopupWindow关闭的时候,让三角形旋转回来
animateArrow(true)
}
//需要21及以上才可以为popupWindow设置阴影
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
popupWindow.elevation = elevationSize
}
}
popup_search_spinner
<?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">
<EditText
android:id="@+id/popup_search_spinner_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:background="@drawable/search_bg"
android:layout_margin="1dp"
android:paddingEnd="1dp"
android:maxLines="1"
android:singleLine="true"
android:paddingStart="1dp"/>
<ListView
android:id="@+id/popup_search_spinner_lv"
android:layout_width="match_parent"
android:scrollbars="none"
android:layout_height="wrap_content"
android:divider="@null"
android:layout_below="@+id/popup_search_spinner_et"/>
<TextView
android:id="@+id/popup_search_spinner_tv"
android:layout_width="match_parent"
android:textColor="@android:color/black"
android:text="暂无搜索结果"
android:gravity="center"
android:visibility="gone"
android:layout_height="wrap_content"/>
</RelativeLayout>
首先来个泛型为String的list,再在里面设置数据,并将其覆给Adapter
private list = ArrayList<String>()
private adapter = MyAdapter()
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
(0..10).mapTo(list){it.toString()}
(0..10).mapTo(list){it.toString()}
(0..10).mapTo(list){it.toString()}
(0..10).mapTo(list){it.toString()}
adapter.list = list
popupListView.adapter = adapter
}
override fun onSizeChanged(w:Int,h:Int,oldW:Int,oldH:Int){
adapter.itemHeight = h
adapter.notifyDataSetChanged()
}
private class MyAdapter : BaseAdapter(){
var list = ArrayList<String>()
var itemHeight = 0
override fun getItem(position: Int): String = list[position]
override fun getItemId(position: Int): Long = position.toLong()
override fun getCount(): Int = list.size
override fun getView(position: Int,converView: View?,parent: ViewGroup): View{
val view: View
if(converView == null){
view = TextView(parent.context)
}else{
view = converView
}
(view as TextView).also{
it.text = getItem(position)
it.height = itemHeight
}
return view
}
}
显示方面,由于弹出的时候需要知道ListView的高度,而在显示ListView之前没办法知道ListView的高度,所以限制ListView的Item的高度(尝试过看Spinner的源码是怎么做的,但看不懂, 以后再说吧),然后通过Item的高度和Item的count计算ListView的高度
onClick方法
获取当前的位置
private var y = 0
override fun onClick(view: View){
val point = IntArray(2)
getLocationOnScreen(point)
y = point[1]
}
再移除RelativeLayout所有显示规则,因为如果在上面弹出的话,popupEditText就必须显示在下面,popupListView和popupTextView显示在popupEditText的上面
在下面弹出的话,popupEditText显示在上面,popupListView和popupTextView显示在popupEditText的下面
removeRule(popupEditText)
removeRule(popupListView)
removeRule(popupTextView)
private fun removeRule(view: View) {
val param = view.layoutParams as RelativeLayout.LayoutParams
param.addRule(RelativeLayout.BELOW, 0)
param.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 0)
param.addRule(RelativeLayout.ABOVE, 0)
}
显示的高度和显示的位置用一个对象来存储,再通过getPositionInfo方法来获取
private isTop = false
orverride fun onClick(view: View){
val positionInfo = getPositionInfo(list)
//获取后记录当前显示的位置,搜索的时候要用到
isTop = positionInfo.isTop
}
private fun getPositionInfo(list: MutableList<String>): PositionInfo {
val popupMaxHeight = getPopupMaxHeight(list)
val bottomHeight = screenHeight - y - height
//如果下面够显示,显示在下面
if (bottomHeight > popupMaxHeight + elevationSize) {
return PositionInfo(false, popupMaxHeight)
}
//如果上面够显示,显示在上面
val topHeight = y - statusBarHeight
if (topHeight > popupMaxHeight + elevationSize) {
return PositionInfo(true, popupMaxHeight)
}
//如果都不够
val isTop = topHeight > bottomHeight
val popupHeight: Float
//如果上面大,上面的高度-阴影高度
if (isTop) {
popupHeight = topHeight - elevationSize
} else {
//如果下面大,下面的高度-阴影高度
popupHeight = bottomHeight - elevationSize
}
return PositionInfo(isTop, popupHeight)
}
private fun getPopupMaxHeight(list: MutableList<String>): Float {
return adapter.itemHeight + list.size * adapter.itemHeight + dip(2f)
}
class PositionInfo(val isTop: Boolean, val height: Float)
如果在上面弹出,就给popupListView和popupTextView添加显示在popupEditText上面的规则
如果当前版本大于等于19,就用showAsDropDown,因为这个有动画.也可以使用PopupWindow的setAnimationStyle设置动画
但试过很多动画,看起来都不好看,试过动态改变PopupWindow的高度,但掉帧严重
如果当前版本小于19就使用showAtLocation方法,只是现在的手机小于19的已经比较少了,所以就懒得专门为小于19设置动画
当在下面弹出的时候,就个popupListView和popupTextView添加显示在popupEditText下面的规则
整个onClick方法的大概实现
override fun onClick(v: View?) {
val point = IntArray(2)
getLocationOnScreen(point)
y = point[1]
animateArrow(false)
removeRule(popupDataView)
removeRule(popupSearchView)
removeRule(popupTipView)
val positionInfo = getPositionInfo(list)
popupWindow.height = positionInfo.height.toInt()
//在弹出的时候就记录,弹出的位置
isTop = positionInfo.isTop
if (positionInfo.isTop) {
if (topPopupAnim != -1) {
popupWindow.animationStyle = topPopupAnim
}
val param1 = popupSearchView.layoutParams as RelativeLayout.LayoutParams
param1.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
val param2 = popupDataView.layoutParams as RelativeLayout.LayoutParams
param2.addRule(RelativeLayout.ABOVE, popupSearchId)
val param3 = popupTipView.layoutParams as RelativeLayout.LayoutParams
param3.addRule(RelativeLayout.ABOVE, popupSearchId)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
popupWindow.showAsDropDown(this, 0, 0, Gravity.BOTTOM or Gravity.START)
} else {
popupWindow.showAtLocation(this, Gravity.TOP or Gravity.START, 0, y - popupWindow.height)
}
} else {
if (bottomPopupAnim != -1) {
popupWindow.animationStyle = bottomPopupAnim
}
val param1 = popupDataView.layoutParams as RelativeLayout.LayoutParams
param1.addRule(RelativeLayout.BELOW, popupSearchId)
val param2 = popupTipView.layoutParams as RelativeLayout.LayoutParams
param2.addRule(RelativeLayout.BELOW, popupSearchId)
popupWindow.showAsDropDown(this)
}
}
监听popupEditText的输入事件
1,用searchList来存储符合条件的数据.用searchContent的变量记录搜索的内容
2,当输入框没有内容的时候,显示全部数据.当输入框有内容的时候,根据输入的内容过滤出符合条件的数据并放到searchList,然后将searchList设置到adapter里面.
3,计算ListVIew的高度,根据当前位置,显示最大可以显示的高度
4,如果没有符合条件的数据的时候,就在popupEditText下面显示一句提示
private val searchList = ArrayList<String>()
//记录当前是否为搜索状态,下面popupListView的setOnItemClickListener要用到
priavte var isSearch =false
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
popupEditText.addTextChangedListener(object : TextWatcher{
override fun afterTextChanged(s: Editable) {
isSearch = s.length == 0
searchContent = s.toString()
//PopupWindow最终要显示的高度
val popupWindowHeight :Int
//如果搜索框里面有数据
if(isSearch){
searchList.clear()
//将符合条件的数据添加到searchList
searchList.addAll(list.filter{it.contains(s)})
//当没有符合条件的数据的时候
if(searchList.isEmpty()){
popupTextView.visibility = View.VISIBLE
popupListView.visibility = View.GONE
popupWindowHeight = popupEditText.height + popupTextView.height
}else{
//不管怎么样,直接隐藏popupTextView并显示popupListView,没必要做多余的判断
popupTextView.visibility = View.GONE
popupListView.visibility = View.VISIBLE
adapter.list = searchList
adapter.notifyDataSetChanged()
//获取在search状态下popupWindow可以显示的高度
popupWindowHeight = getPopupSearchHeight(searchList).toInt()
}
}else{//如果没有数据
popupTextView.visibility = View.GONE
popupListView.visibility = View.VISIBLE
adapter.list = list
adapter.notifyDataSetChanged()
popupWindowHeight = getPopupSearchHeight(list).toInt()
}
if(isTop){
//如果显示在上面,需要正确计算显示的坐标
//left即获取当前View左上角的x位置
//当宽度小于0时,表示不改变宽度
popupWindow.update(left,y - popupWindowHeight,-1,popupWindowHeight)
}else{
//如果显示在下面的话,直接更新高度即可
popupWindow.update(-1,popupWindowHeight)
}
}
//另外2个方法在这里用不上,所以就不贴出来了
})
}
/**
* 获取PopupWindow在search状态下的高度,不是使用searchList这个变量计算高度
*/
private fun getPopupSearchHeight(list: MutableList<T>): Float {
val height: Float
val maxHeight = getPopupMaxHeight(list)
if (isTop) {
if (maxHeight < y - statusBarHeight) {
height = maxHeight
} else {
height = y - statusBarHeight - elevationSize
}
} else {
if (screenHeight - y > maxHeight) {
height = maxHeight
} else {
height = screenHeight - y - this.height - elevationSize
}
}
return height
}
在isTop那里的update的y参数可能有人看起来不太懂,用图说明一下
监听popupListView的setOnItemClickListener
1,用searchSelectIndex记录在searchList的index,用selectIndex记录在list的index
2,当不是search状态的时候,非常简单,直接将position的值给selectIndex
3,search状态的时候,将position的值给searcSelectIndex,并通过position计算出在list的index
计算方式
1):声明count
2):用list遍历,当遍历到的变量符合要求,count++
3):判断count是否等于position+1,如果是,就将当前的index给selectIndex
private var searchSelectIndex = 0
private var selectIndex = 0
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
popupListView.setOnItemClickListener { _, _, position, _ ->
if(isSearch){
//记得给textView设置文本
textView.text = searchList[position]
searchSelectIndex = position
//直接给-1,下面判断的时候postion就不用+1
var count = -1
for(i in 0 until list.size){
if(list[i].contains(searchContent)){
count++
}
if(count == position){
selectIndex = i
break
}
}
}else{
searchSelectIndex = -1
textView.text = list[position]
selectIndex = position
}
//PopupWindow记得dissmiss
popupWindow.dismiss()
}
}
主要的实现思路就这样,其他方面直接看源码吧,大部分都有写注释