可拖动的pop弹框(Kotlin)

  • 要实现view跟随手指拖拽,可以获取viewlayoutParams,然后重新设置它的位置

先看一下效果图吧(图一):

图一

  • 我们在mMenuView.rlMySettingsUserPopAll(最父级别的view下的RelactiveLayout布局)的onTouch里进行逻辑处理(这里的mPointPositionPoint对象,存放相应坐标。record数组我分别存放了坐标和时间,时间用来判断松开的时候是否要弹上去或关闭的)

代码

//顶部手势监听
mMenuView.rlMySettingsUserPop.setOnTouchListener { v, event ->
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            //每次按下时记录坐标
            mPointPosition.y = event.rawY.toInt()
            record[0] = event.rawY.toInt()
            record[1] = System.currentTimeMillis().toInt()
        }
        MotionEvent.ACTION_MOVE -> {
            //每次重绘都会根据上一次最后触屏的mPointPosition.y坐标算出新移动的值
            val dy = event.rawY.toInt() - mPointPosition.y
            //变化中的顶部距离
            val top = v.top + dy
            //获取到layoutParams后改变属性 在设置回去
            val layoutParams = v.layoutParams as RelativeLayout.LayoutParams
            layoutParams.topMargin = top
            v.layoutParams = layoutParams
            //记录最后一次移动的位置
            mPointPosition.y = event.rawY.toInt()
        }
        MotionEvent.ACTION_UP -> {
            //先根据时间算 如果在0.5秒内的话且向下拉的话 就直接销毁弹框
            if (System.currentTimeMillis().toInt() - record[1] < 500 && event.rawY.toInt() > record[0]) {
                dismiss()
            } else {    //然后再根据移动距离判断是否销毁弹框
                //下移超过200就销毁 否则弹回去
                if (event.rawY.toInt() - record[0] > 300) {
                    dismiss()
                } else {
                    //获取到layoutParams后改变属性 在设置回去
                    val layoutParams = v.layoutParams as RelativeLayout.LayoutParams
                    layoutParams.topMargin = defaultTop
                    v.layoutParams = layoutParams
                }
            }
        }
    }
    //刷新界面
    mMenuView.rlMySettingsUserPopAll.invalidate()
    true
}
  • 好了 这里已经实现了一个空白的pop的下拉拖拽功能了,但一般pop弹框里都会嵌套一个RecyclerView,这样的话我们底部mMenuView.rlMySettingsUserPopAll的手势就被拦截了,就达不到在RecyclerView上也能拖拽的效果了

  • RecyclerView实现的代码我就先不贴出来,Adapter用的是宏洋大神的BaseAdapter 地址

看图,用户体验略差(图二):

图二

  • 然后我们思考一下。。。
  • 现在RecyclerView控制不了下面的mMenuView.rlMySettingsUserPopAll了,是因为覆盖了底部mMenuView.rlMySettingsUserPopAll,然后底部mMenuView.rlMySettingsUserPopAll监听不到onTouch事件了,那么这样我们能不能在RecyclerView上也添加onTouch事件,然后控制最底部的view进行移动呢。。试一试咯

代码

//RecyclerView监听
mMenuView.rvMySettingsUserPop.setOnTouchListener { v, event ->
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            //每次按下时记录坐标
            mPointPosition.y = event.rawY.toInt()
            record[0] = event.rawY.toInt()
            record[1] = System.currentTimeMillis().toInt()
        }
        MotionEvent.ACTION_MOVE -> {
            //每次重绘都会根据上一次最后触屏的mPointPosition.y坐标算出新移动的值
            val dy = event.rawY.toInt() - mPointPosition.y
            //变化中的顶部距离
            val top = mMenuView.rlMySettingsUserPop.top + dy
            //获取到layoutParams后改变属性 在设置回去
            val layoutParams = mMenuView.rlMySettingsUserPop.layoutParams as RelativeLayout.LayoutParams
            layoutParams.topMargin = top
            mMenuView.rlMySettingsUserPop.layoutParams = layoutParams
            //记录最后一次移动的位置
            mPointPosition.y = event.rawY.toInt()
        }
        MotionEvent.ACTION_UP -> {
            //先根据时间算 如果在0.5秒内的话且向下拉的话 就直接销毁弹框
            if (System.currentTimeMillis().toInt() - record[1] < 500 && event.rawY.toInt() > record[0]) {
                dismiss()
            } else {    //然后再根据移动距离判断是否销毁弹框
                //下移超过200就销毁 否则弹回去
                if (event.rawY.toInt() - record[0] > 300) {
                    dismiss()
                } else {
                    //获取到layoutParams后改变属性 在设置回去
                    val layoutParams = mMenuView.rlMySettingsUserPop.layoutParams as RelativeLayout.LayoutParams
                    layoutParams.topMargin = defaultTop
                    mMenuView.rlMySettingsUserPop.layoutParams = layoutParams
                }
            }
        }
    }
    //刷新界面
    mMenuView.rlMySettingsUserPopAll.invalidate()
    true
}
  • 答案显然是可以的,就是效果嘛。。。嘿嘿嘿,有点尴尬!

效果(图三):

图三

  • 继续分析一波咯,为啥会这样呢,我找了好久。。。最后终于功夫不负有心人,我发现RecyclerViewonTouch事件的MotionEvent.ACTION_DOWN事件根本不会走,被它的item拦截了,所以我们最开始按下屏幕的坐标没有设置上去。我就在适配器里的item上也添加了一个onTouch方法,用来记录坐标用:
  • 这里的onTouch事件肯定要返回false了,不得别的onTouch就监听不到了

效果(图四):

图四

  • 现在还有什么问题呢,我们的RecyclerView滑动效果呢???(问号三联)+ (滑稽三联),不急,请容我再思考一稍稍。我们可以继承apiRelactiveLayout布局,然后手动设置它是否拦截事件
  • 修改后的RelactiveLayout代码:
/**
 *  重写RelativeLayout的事件传递 设置动态拦截
 */
class MyRelativeLayout : RelativeLayout {
    private var mIsIntercept = false    //是否拦截

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    //动态设置是否拦截
    fun setIntercept(isIntercept: Boolean) {
        this.mIsIntercept = isIntercept
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        var intercepted = false
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                intercepted = intercepted
            }
            MotionEvent.ACTION_MOVE -> {
                //方法传进来 如果为true就拦截事件
                intercepted = mIsIntercept
            }
            MotionEvent.ACTION_UP -> {
                //松开手再赋值为false
                mIsIntercept = false
                intercepted = false
            }
            else -> {
            }
        }
        return intercepted
    }
}
  • 然后修改RecyclerViewonTouch代码:
  • 思路:只要记录坐标就行了(虽然这里的ACTION_DOWN不会触发,但当RecyclerViewitem为空时就会触发了哦),然后移动的时候判断RecyclerView是否滑到顶部了,滑到顶部在下滑就把事件交给它的父级view
//RecyclerView监听
mMenuView.rvMySettingsUserPop.setOnTouchListener { v, event ->
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            //每次按下时记录坐标
            mPointPosition.y = event.rawY.toInt()
            record[0] = event.rawY.toInt()
            record[1] = System.currentTimeMillis().toInt()
        }
        MotionEvent.ACTION_MOVE -> {
            //下拉
            if (event.rawY <= record[0]) {
                mMenuView.rlMySettingsUserPop.setIntercept(false)
            } else {
                //滑到顶部了
                if (!v.canScrollVertically(-1)) {
                    mMenuView.rlMySettingsUserPop.setIntercept(true)
                }
            }
        }
    }
    false
}
  • 还有mMenuView.rlMySettingsUserPop.setOnTouch里的MotionEvent.ACTION_UP里添加mMenuView.rlMySettingsUserPop.setIntercept(false) //松开后不拦截事件传递:

最终效果(图五):

图五

全部代码:

  • PopCitySelect:(pop弹框,里面有的代码用不到估计,都加了注释了,小伙伴们可自行选择)
  • 获取缓存getCacheData()方法可以去掉,打开直接请求网络数据,还有CommonAdapter是宏洋大神的BaseAdapterRecyclerView里的item布局我也不贴了,可以自己写哦
/**
 *  Description :个人资料城市选择

 *  Author:yang

 *  Email:[email protected]

 *  Date: 2019/01/05
 */
@SuppressLint("CheckResult")
class PopCitySelect : PopupWindow {
    private var mActivity: Activity
    private lateinit var mLayoutManager: LinearLayoutManager
    private lateinit var mAdapter: CommonAdapter
    private var mAllCityBean = AllCityBean()                         //接口数据
    private var mList = ArrayList()                         //目前列表用到的数据
    //    private var mOneCityList = ArrayList()                  //一级城市(省)
    private var mTwoCityList = ArrayList()                  //二级城市(市)
    private var mThreeCityList = ArrayList()                //三级城市(区/县)
    private lateinit var mSelectOneCity: AllCity                      //当前选择的省
    private lateinit var mSelectTwoCity: AllCity                      //当前选择的市
    private var mMenuView: View
    private var mPointPosition = Point()                             //手指按下坐标
    private var record = arrayOf(0, 0)                               //存放手指按下坐标和时间戳
    private var defaultTop = 0                                       //弹框原始距离顶部位置
    private var mCitySelectCallBack: CitySelectCallBack? = null      //回调

    //构造方法
    constructor(activity: Activity) : super(activity) {
        this.mActivity = activity
        mMenuView = LayoutInflater.from(activity).inflate(R.layout.activity_my_settings_user_citypop, null)

        initListener()
        initAdapter()

        //获取缓存数据
        getCacheData()

//        //如果页面的临时数据不为空 就不用请求数据
//        if (MySettingsUserActivity.mAllCityList.isNotEmpty()) {
//            //执行数据分类方法
//            setData(MySettingsUserActivity.mAllCityList)
//        } else {
//            httpLoad()
//        }

        //取消按钮
        mMenuView.ivMySettingsUserPopClose.setOnClickListener {
            //销毁弹出框
            dismiss()
        }

        //顶部手势监听
        mMenuView.rlMySettingsUserPop.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    //每次按下时记录坐标
                    mPointPosition.y = event.rawY.toInt()
                    record[0] = event.rawY.toInt()
                    record[1] = System.currentTimeMillis().toInt()
                }
                MotionEvent.ACTION_MOVE -> {
                    //每次重绘都会根据上一次最后触屏的mPointPosition.y坐标算出新移动的值
                    val dy = event.rawY.toInt() - mPointPosition.y
                    //变化中的顶部距离
                    val top = v.top + dy
                    //获取到layoutParams后改变属性 在设置回去
                    val layoutParams = v.layoutParams as RelativeLayout.LayoutParams
                    layoutParams.topMargin = top
                    v.layoutParams = layoutParams
                    //记录最后一次移动的位置
                    mPointPosition.y = event.rawY.toInt()
                }
                MotionEvent.ACTION_UP -> {
                    //先根据时间算 如果在0.5秒内的话且向下拉的话 就直接销毁弹框
                    if (System.currentTimeMillis().toInt() - record[1] < 500 && event.rawY.toInt() > record[0]) {
                        dismiss()
                    } else {    //然后再根据移动距离判断是否销毁弹框
                        //下移超过200就销毁 否则弹回去
                        if (event.rawY.toInt() - record[0] > 300) {
                            dismiss()
                        } else {
                            //获取到layoutParams后改变属性 在设置回去
                            val layoutParams = v.layoutParams as RelativeLayout.LayoutParams
                            layoutParams.topMargin = defaultTop
                            v.layoutParams = layoutParams
                        }
                    }
                    mMenuView.rlMySettingsUserPop.setIntercept(false)
                }
            }
            //刷新界面
            mMenuView.rlMySettingsUserPopAll.invalidate()
            true
        }

        //RecyclerView监听
        mMenuView.rvMySettingsUserPop.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    //每次按下时记录坐标
                    mPointPosition.y = event.rawY.toInt()
                    record[0] = event.rawY.toInt()
                    record[1] = System.currentTimeMillis().toInt()
                }
                MotionEvent.ACTION_MOVE -> {
                    //下拉
                    if (event.rawY <= record[0]) {
                        mMenuView.rlMySettingsUserPop.setIntercept(false)
                    } else {
                        //滑到顶部了
                        if (!v.canScrollVertically(-1)) {
                            mMenuView.rlMySettingsUserPop.setIntercept(true)
                        }
                    }
                }
            }
            false
        }

        //设置SelectPicPopupWindow的View
        this.contentView = mMenuView
        //设置SelectPicPopupWindow弹出窗体的宽
        this.width = ViewGroup.LayoutParams.MATCH_PARENT
        //设置SelectPicPopupWindow弹出窗体的高
        this.height = ViewGroup.LayoutParams.WRAP_CONTENT
        //设置SelectPicPopupWindow弹出窗体可点击
        this.isFocusable = true
        //设置SelectPicPopupWindow弹出窗体动画效果
        this.animationStyle = R.style.bottomDialog_animStyle
        //设置SelectPicPopupWindow弹出窗体的背景
        this.setBackgroundDrawable(ColorDrawable(-0x00000000))

        //mMenuView添加OnTouchListener监听判断获取触屏位置如果在选择框外面则销毁弹出框
        mMenuView.setOnTouchListener { _, event ->
            val height = mMenuView.rlMySettingsUserPop.top
            val y = event.y
            if (event.action == MotionEvent.ACTION_UP) {
                if (y < height) {
                    dismiss()
                }
            }
            true
        }
    }

    private fun initListener() {

    }

    private fun initAdapter() {
        mLayoutManager = LinearLayoutManager(mActivity)
        mMenuView.rvMySettingsUserPop.layoutManager = mLayoutManager
        mAdapter = CommonAdapter(mActivity, R.layout.activity_my_settings_user_citypop_item, mList, holderConvert = { holder, t, _, _ ->
            holder.apply {
                setText(R.id.tvMySettingsUserPopRvCity, t.name)
                getView(R.id.rlSelectDialogRvMenuL).setOnTouchListener { _, event ->
                    when (event.action) {
                        MotionEvent.ACTION_DOWN -> {
                            //每次按下时记录坐标
                            mPointPosition.y = event.rawY.toInt()
                            record[0] = event.rawY.toInt()
                            record[1] = System.currentTimeMillis().toInt()
                        }
                    }
                    false
                }
            }
        }, onItemClick = { _, _, position ->
            //第一次点击
            if (mMenuView.rlMySettingsUserPopTopBottom.visibility == View.GONE) {
                mMenuView.rlMySettingsUserPopTopBottom.visibility = View.VISIBLE
                mMenuView.tvMySettingsUserPopOneTips.text = mList[position].name
                mMenuView.tvMySettingsUserPopTips.text = "选择城市"
                //记录选择的第一个城市
                mSelectOneCity = mList[position]
                mList.clear()
                //遍历二级城市列表 把所有父id为当前城市id的市拿出来加到mList里去
                mTwoCityList.forEach {
                    if (it.fatherId.toString() == mSelectOneCity.id) {
                        mList.add(it)
                    }
                }
                //如果mList是空的 那么就直接关闭pop 调回调方法
                if (mList.isEmpty()) {
                    mCitySelectCallBack?.onCitySelectCallBack(mSelectOneCity)
                    dismiss()
                } else {
                    //滚到第一个
                    mMenuView.rvMySettingsUserPop.scrollToPosition(0)
                    mAdapter.notifyDataSetChanged()
                }
            } else if (mMenuView.tvMySettingsUserPopTwoDot.visibility == View.GONE) {  //选择第二个城市了
                mMenuView.tvMySettingsUserPopTwoDot.visibility = View.VISIBLE
                mMenuView.tvMySettingsUserPopTwoTips.visibility = View.VISIBLE
                mMenuView.tvMySettingsUserPopTwoTips.text = mList[position].name
                mMenuView.tvMySettingsUserPopThreeTips.text = "请选择县"
                mMenuView.tvMySettingsUserPopTips.text = "选择区/县"
                //记录选择的第二个城市
                mSelectTwoCity = mList[position]
                mList.clear()
                mThreeCityList.forEach {
                    if (it.fatherId.toString() == mSelectTwoCity.id) {
                        mList.add(it)
                    }
                }
                if (mList.isEmpty()) {
                    mCitySelectCallBack?.onCitySelectCallBack(mSelectOneCity, mSelectTwoCity)
                    dismiss()
                } else {
                    //滚到第一个
                    mMenuView.rvMySettingsUserPop.scrollToPosition(0)
                    mAdapter.notifyDataSetChanged()
                }
            } else {  //选择第三个县
                //调用回调
                mCitySelectCallBack?.onCitySelectCallBack(mSelectOneCity, mSelectTwoCity, mList[position])
                dismiss()
            }
        })
        mMenuView.rvMySettingsUserPop.adapter = mAdapter
    }

    //城市列表请求
    private fun httpLoad(version: String? = null) {
        ApiUtils.getApi().let {
            if (version == null || version == "") {
                it.getCityStatic()
            } else {
                it.getCityStatic(version)
            }
        }
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe({
                    it.apply {
                        if (code == 12000) {
                            //数据库先删除bean
                            BoxUtils.removeAllCity()
                            mAllCityBean = data!!
                            //数据库保存bean
                            BoxUtils.saveAllCity(mAllCityBean)
                            //执行数据分类方法
                            setData(mAllCityBean)
                        } else if (code == 20000) {

                        }
                    }
                }, {

                }, {}, {})
    }

    //获取缓存数据
    private fun getCacheData() {
        Observable.create {
            mAllCityBean = BoxUtils.getAllCity()!!
//            mAllCityBean.version = mAllCityBean.city[0].version
            it.onNext(mAllCityBean)
        }.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    //页面赋值
                    setData(it)
                    //请求下接口
                    httpLoad(it.version)
                }, {
                    httpLoad()
                })
    }

    //数据分类
    private fun setData(bean: AllCityBean) {
//        mOneCityList.clear()
        mTwoCityList.clear()
        mThreeCityList.clear()
        //遍历请求到的数组 然后一个个分类存放
        bean.city.forEach {
            // || it.level == 0
            if (it.level == 1) {
//                mOneCityList.add(it)
                //先从省级别选择
                mList.add(it)
            } else if (it.level == 2) {
                mTwoCityList.add(it)
            } else if (it.level == 3) {
                mThreeCityList.add(it)
            }
        }
        mMenuView.pbYingyingRecommend.visibility = View.GONE
        mMenuView.tvMySettingsUserPopTips.visibility = View.VISIBLE
        mAdapter.notifyDataSetChanged()
    }

    override fun showAtLocation(parent: View?, gravity: Int, x: Int, y: Int) {
        super.showAtLocation(parent, gravity, x, y)
        backgroundAlphaExt(0.5f)
    }

    override fun dismiss() {
        super.dismiss()
        backgroundAlphaExt(1f)
    }

    //改变背景亮度
    private fun backgroundAlphaExt(bgAlpha: Float) {
        val lp = mActivity.window.attributes
        //0.0-1.0
        lp?.alpha = bgAlpha
        mActivity.window.attributes = lp
    }

    //回调方法
    fun setOnCitySelectListener(citySelectListener: CitySelectCallBack) {
        mCitySelectCallBack = citySelectListener
    }

    interface CitySelectCallBack {
        fun onCitySelectCallBack(oneCity: AllCity, twoCity: AllCity? = null, threeCity: AllCity? = null)
    }
}
  • value里的styles里的:R.style.bottomDialog_animStyle
    
    

    
    
    
    
    
        
    

    
    
    
    
        
    
  • bean
/**
 *  Description :所有城市bean

 *  Author:yang

 *  Email:[email protected]

 *  Date: 2019/1/15
 */

@Entity
class AllCityBean {
    @Id
    var cacheId: Long = 0
    var version: String = ""
    var city: List = ArrayList()
    var cityString: String = ""
}

@Entity
data class AllCity(
        @Id
        var cacheId: Long,
        val id: String,
        val name: String,
        val fatherId: Int,
        val level: Int
)
  • xml代码:



    

        

            

                

                
            

            

                

                

                

                

                

                

                

                    

                    
                
            
        

        

        

            

            

            
        

        
    

  • drawable里的:ripple_bg_white_top_radius15


    
    

  • drawable-v21里的:ripple_bg_white_top_radius15


    

  • drawable里的:color_gray_click


    

  • drawable里的:ripple_bg_drawable_white_top_radius15


    
    
    
    

  • MyRelativeLayout上面贴出来了

用法:

private lateinit var mPopCity: PopCitySelect
//方法里调用。。。
private fun clickTest(){
    mPopCity = PopCitySelect(this)
    mPopCity.showAtLocation(llMySettingsUser, Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, 0, 0)
    mPopCity.setOnCitySelectListener(object : PopCitySelect.CitySelectCallBack {
        //回调
        override fun onCitySelectCallBack(oneCity: AllCity, twoCity: AllCity?, threeCity: AllCity?) {
            
        }
    })
}

你可能感兴趣的:(可拖动的pop弹框(Kotlin))