安卓实现带搜索框的Spinner(2)

效果图:

源码

之前在这篇文章介绍了如何基于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()
    }
}

主要的实现思路就这样,其他方面直接看源码吧,大部分都有写注释

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值