- 要实现view跟随手指拖拽,可以获取view的layoutParams,然后重新设置它的位置
先看一下效果图吧(图一):
图一
- 我们在mMenuView.rlMySettingsUserPopAll(最父级别的view下的RelactiveLayout布局)的onTouch里进行逻辑处理(这里的mPointPosition是Point对象,存放相应坐标。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
}
- 答案显然是可以的,就是效果嘛。。。嘿嘿嘿,有点尴尬!
效果(图三):
图三
- 继续分析一波咯,为啥会这样呢,我找了好久。。。最后终于功夫不负有心人,我发现RecyclerView的onTouch事件的MotionEvent.ACTION_DOWN事件根本不会走,被它的item拦截了,所以我们最开始按下屏幕的坐标没有设置上去。我就在适配器里的item上也添加了一个onTouch方法,用来记录坐标用:
- 这里的onTouch事件肯定要返回false了,不得别的onTouch就监听不到了
效果(图四):
图四
- 现在还有什么问题呢,我们的RecyclerView滑动效果呢???(问号三联)+ (滑稽三联),不急,请容我再思考一稍稍。我们可以继承api的RelactiveLayout布局,然后手动设置它是否拦截事件
- 修改后的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
}
}
- 然后修改RecyclerView的onTouch代码:
- 思路:只要记录坐标就行了(虽然这里的ACTION_DOWN不会触发,但当RecyclerView里item为空时就会触发了哦),然后移动的时候判断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是宏洋大神的BaseAdapter,RecyclerView里的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?) {
}
})
}