用recylcerview 自定义一个地址选择器

242 阅读3分钟

先上效果图

GIF 2022-5-26 17-26-02.gif 很常见的地址选择器 之所以自己写主要是闲的

首先分析一下

  • 当选择器滑动到顶部和底部时

微信截图_20220526174124.png

image.png

1.顶部时有一个空白item 而底部有2个空白item 所以itemcount需要+3 重写adapter

override fun getItemCount(): Int {
    return if(data.isEmpty()) 0 else data.size+3
}


override fun getItem(position: Int): AddressModel {
    if(data.isEmpty()){
        return AddressModel()
    }
    if(position>0&&position<=data.size){
        return data[position-1]
    }
    return data[0]
}
//设置数据
override fun setData(data: AddressModel) {
    root.visibility=if(position==0||position>=adapter.itemCount-2) View.INVISIBLE else View.VISIBLE
    address.text=data.name
}

2.可以看到选中的item和未选中的item明显有区别 这一块选择了改变item的alpha来实现这个效果 重写recyclerview的drawChild方法

override fun drawChild(canvas: Canvas?, child: View, drawingTime: Long): Boolean {
    //获取第一个item的position
    val first = layoutManager!!.findFirstVisibleItemPosition()
    //设置选中的item position 自由更改
    val center = 1+ first
    val position = getChildAdapterPosition(child)
    if (position == center) {
        child.alpha = 1.0f
    } else {
        //没选中的改为0.4F
        child.alpha = 0.4f
    }
    return super.drawChild(canvas, child, drawingTime)
}

3.选中item 上下方的线 我这里选择重写recyclerview直接画

override fun draw(canvas: Canvas) {
    super.draw(canvas)
    if (mItemH != 0) {
        val size = 1
        canvas.drawLine(
            0f, (mItemH * size).toFloat(), itemWith.toFloat(), (mItemH
                    * size).toFloat(), mHoloPaint!!
        )
        canvas.drawLine(
            0f, (mItemH * (size + 1)).toFloat(), itemWith.toFloat(), (mItemH
                    * (size + 1)).toFloat(), mHoloPaint!!
        )
    }
}

代码很简单 然后我们还要提供一个获取选中的位置的方法 还有recyclerview高度需要重新设置 recyclerview完整代码如下

class SelectRv: RecyclerView {

    private var layoutManager: LinearLayoutManager? = null
    private var mItemH = 0
    private var mHoloPaint: Paint? = null
    private val mItemSize = 4
    private var itemWith = 0



    constructor(context: Context) : this(context,null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs,0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ){
            init()
    }

    private fun init(){
        mHoloPaint = Paint()
        mHoloPaint?.strokeWidth = 1f
        mHoloPaint?.color = Color.parseColor("#E1E1E1")

        viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                if (childCount > 0 && mItemH == 0) {
                    itemWith = width
                    viewTreeObserver.removeOnGlobalLayoutListener(this)
                    mItemH = getChildAt(0).height
                    if (mItemH != 0) {
                        val params = layoutParams
                        params.height = mItemH * mItemSize
                        requestLayout()
                    }
                }
            }
        })
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        layoutManager = getLayoutManager() as? LinearLayoutManager
    }


    override fun drawChild(canvas: Canvas?, child: View, drawingTime: Long): Boolean {
        //获取第一个item的position
        val first = layoutManager!!.findFirstVisibleItemPosition()
        //设置选中的item position 自由更改
        val center = 1+ first
        val position = getChildAdapterPosition(child)
        if (position == center) {
            child.alpha = 1.0f
        } else {
            //没选中的改为0.4F
            child.alpha = 0.4f
        }
        return super.drawChild(canvas, child, drawingTime)
    }

    override fun draw(canvas: Canvas) {
        super.draw(canvas)
        if (mItemH != 0) {
            val size = 1
            canvas.drawLine(
                0f, (mItemH * size).toFloat(), itemWith.toFloat(), (mItemH
                        * size).toFloat(), mHoloPaint!!
            )
            canvas.drawLine(
                0f, (mItemH * (size + 1)).toFloat(), itemWith.toFloat(), (mItemH
                        * (size + 1)).toFloat(), mHoloPaint!!
            )
        }
    }

    fun getSelect(): Int {
        //因为adapter会添加一个头 所以不用+1
        return layoutManager?.findFirstVisibleItemPosition()?:-1
    }

}

接下来我们要解决另外一个问题 每次滑动结束需要对齐头部item

如果是奇数的item展示 比如一次展示3个或者5个item 不需要重写LinearSnapHelper
偶数的话则需要重写LinearSnapHelper

class TopLinearSnapHelper: LinearSnapHelper() {
    private var mVerticalHelper: OrientationHelper? = null

    private var mHorizontalHelper: OrientationHelper? = null
    override fun calculateDistanceToFinalSnap(
        layoutManager: RecyclerView.LayoutManager,
        targetView: View
    ): IntArray? {
        val out = IntArray(2)
        if (layoutManager.canScrollHorizontally()) {
            out[0] = distanceToTop(
                targetView,
                getHorizontalHelper(layoutManager)
            )
        } else {
            out[0] = 0
        }

        if (layoutManager.canScrollVertically()) {
            out[1] = distanceToTop(
                targetView,
                getVerticalHelper(layoutManager)
            )
        } else {
            out[1] = 0
        }
        return out
    }
    private fun distanceToTop( targetView:View, helper: OrientationHelper):Int{//获取top位置距离顶部差距
        return helper.getDecoratedStart(targetView)
    }

    private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
        if (mVerticalHelper == null) {
            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
        }
        return mVerticalHelper!!
    }

    private fun getHorizontalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
        if (mHorizontalHelper == null) {
            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
        }
        return mHorizontalHelper!!
    }

    override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
        val helper=if(layoutManager.canScrollHorizontally())  getHorizontalHelper(layoutManager) else getVerticalHelper(layoutManager)
        return find(layoutManager,helper)
    }
    private fun find(layoutManager: RecyclerView.LayoutManager,helper:OrientationHelper):View?{
        val childCount: Int = layoutManager.childCount
        if (childCount == 0) {
            return null
        }

        val first=layoutManager.getChildAt(0)
        var closestChild: View? = first
        val itemHalf=helper.getDecoratedMeasurement(first)/2  //获取item一半的位置
        val itemTop=helper.getDecoratedStart(first)
       if(abs(itemTop)>=itemHalf){ //如果超过一半 滚动到下一个 否则滚动到上一个
            if(childCount>1){
                closestChild=layoutManager.getChildAt(1)
            }
        }
        return closestChild
    }
}

顺便附上snap的使用 设置完adapter后attach就好了

bind.addressRv1.layoutManager=LinearLayoutManager(context)
bind.addressRv2.layoutManager=LinearLayoutManager(context)
bind.addressRv3.layoutManager=LinearLayoutManager(context)
bind.addressRv1.adapter=adp1
bind.addressRv2.adapter=adp2
bind.addressRv3.adapter=adp3

val mSnap1 = TopLinearSnapHelper()
val mSnap2 = TopLinearSnapHelper()
val mSnap3 = TopLinearSnapHelper()

mSnap1.attachToRecyclerView(bind.addressRv1)
mSnap2.attachToRecyclerView(bind.addressRv2)
mSnap3.attachToRecyclerView(bind.addressRv3)

到这里基本上已经完成一大半了,剩下的就是监听rv的滑动。

类似这样

bind.addressRv1.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
       if(newState==RecyclerView.SCROLL_STATE_IDLE){//滚动结束需要更新数据
           setData(false)
       }
    }
})

还有就是dialog加上动画 ,传递选择的数据就好了。 优点嘛也有,不用引入第三方库, 可以任意的自定义。

然后直接调用即可

AddressDialog().setGravity(DialogGravity.BOTTOM).show(fragment!!.childFragmentManager,"addressDialog")

完整的demo见lujing5873/FreeDialog: 基于DialogFragment封装。 可以实现spinner, dialog ,popwindow等功能。 可自由扩展 ,使用方便。 (github.com)

感谢观看