先看看汽车之家折叠列表的效果图
接着看看实现的效果图
在这篇文章中主要采用ViewDragHelper
这个类,这个是系统提供的一个处理view
拖动的一个类。具体请查看相关资料,在这就不多说。
先来解析实现的思路,view
的移动采用ViewDragHelper
即可,如果下方是一般的View
的话就差不多了,但是如果是ListView
或者RecyclerView
之类的话主要处理一个事件拦截的逻辑。首先要清楚ListView
或者RecyclerView
在处理事件的时候调用了getParent().requestDisallowInterceptTouchEvent(true);
请求父布局不拦截事件,所以当拦截的时候不能让ListView
或者RecyclerView
接受到MOVE
事件。逻辑很简单,就是当下面的ListView
或者RecyclerView
到顶部 并且是下拉的时候就需要使用ViewDragHelper
来响应拖动,如果上面的菜单是打开状态的话那么也需要响应,这时候就需要拦截MOVE
事件来处理拖动。逻辑就是这么简单,但是细节的东西有很多,不能马虎并且熟悉相关的api
。
接下来开始撸码
这里我选择继承FrameLayout
,在初始化的时候创建ViewDragHelper
,资源加载完毕了得到需要拖动的mDragView
,在测量之后获取到最大拖动的距离,也就是上方菜单的高度,当手指抬起的时候判断是需要关闭还是打开
class VerticalDragListView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0)
: FrameLayout(context, attrs, defStyleAttr) {
private var mDragView: View? = null//拖动的view
private var mMenuViewHeight: Int = 0 //拖动的view 高度
private var mMenuIsOpen: Boolean = false//是否打开
private var mViewDragHelper: ViewDragHelper? = null //拖动的辅助类
private val mCallback: ViewDragHelper.Callback = object : ViewDragHelper.Callback() {
//指定view是否可以拖动
override fun tryCaptureView(child: View, pointerId: Int): Boolean {
return mDragView == child
}
//返回移动的距离
override fun clampViewPositionVertical(child: View?, top: Int, dy: Int): Int {
//滑动的范围只能是在menu的高度
var t: Int = top
if (top <= 0) t = 0
if (top >= mMenuViewHeight) t = mMenuViewHeight
return t
}
//手松开的时候回调 打开还是关闭
override fun onViewReleased(releasedChild: View?, xvel: Float, yvel: Float) {
//打开菜单
if (mDragView!!.top >= mMenuViewHeight / 2) {
mViewDragHelper?.settleCapturedViewAt(0, mMenuViewHeight)
mMenuIsOpen = true
} else {//关闭菜单
mViewDragHelper?.settleCapturedViewAt(0, 0)
mMenuIsOpen = false
}
invalidate()
}
}
//响应滚动
override fun computeScroll() {
if (mViewDragHelper!!.continueSettling(true)) invalidate()
}
init {
mViewDragHelper = ViewDragHelper.create(this, mCallback)
}
override fun onFinishInflate() {
super.onFinishInflate()
if (childCount != 2) throw RuntimeException("childCount只能包含两个子布局")
mDragView = getChildAt(1)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (changed) mMenuViewHeight = getChildAt(0).measuredHeight
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
mViewDragHelper?.processTouchEvent(event)
return true
}
}
在这需要注意一点,当手指松开判断打开或者关闭菜单需要调用invalidate()
并且重写computeScroll()
函数来响应。
如果下方的view
不是ListView
或者RecyclerView
之类的话,到这就可以了,但是实际开发中,下方一般是这种,所以就需要按照上面说的处理事件拦截
private var mDownY: Float = 0.0f
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
// 菜单打开要拦截
if (mMenuIsOpen) {
return true
}
// 向下滑动拦截,不让ListView或者RecyclerView做处理
// 谁拦截谁 父View拦截子View ,但是子 View 可以调这个方法
// requestDisallowInterceptTouchEvent 请求父View不要拦截,改变的其实就是 mGroupFlags 的值
when (ev!!.action) {
MotionEvent.ACTION_DOWN -> {
mDownY = ev.y
// 让 DragHelper 拿一个完整的事件
mViewDragHelper!!.processTouchEvent(ev)
}
MotionEvent.ACTION_MOVE -> {
val moveY = ev.y
if (moveY - mDownY > 0 && !canChildScrollUp()) {
// 向下滑动 && 滚动到了顶部,拦截不让ListView或者RecyclerView做处理
return true
}
}
}
return super.onInterceptTouchEvent(ev)
}
/**
* @return Whether it is possible for the child view of this layout to
* * scroll up. Override this if the child view is a custom view.
*/
fun canChildScrollUp(): Boolean {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mDragView is AbsListView) {
val absListView = mDragView as AbsListView
return absListView.childCount > 0 && (absListView.firstVisiblePosition > 0 || absListView.getChildAt(0)
.top < absListView.paddingTop)
} else {
return ViewCompat.canScrollVertically(mDragView, -1) || mDragView!!.scrollY > 0
}
} else {
return ViewCompat.canScrollVertically(mDragView, -1)
}
}
这里需要注意,如果不在ACTION_DOWN
的时候调用mViewDragHelper.processTouchEvent(ev)
的话,那么ViewDragHelper
将会报错,将不会触发拖动事件
从字面意思都可以看出需要一个完整的事件,所以需要在
ACTION_DOWN
的时候调用ViewDragHelper.processTouchEvent(ev)
在一步步的分析之下,这个效果就慢慢的完成了。有了新需求的时候,在动手应该理清思路,然后想好使用相关的api
,处理一些手势可以使用OnGestureListener
,处理拖动可以使用ViewDragHelper
,这些都是系统封装好的辅助类,应该要合理的利用这些辅助类。相信如果不使用这些辅助类也可以写出这些效果,但是那样的话也会浪费大量的事件和精力,而且很容易出错。