自定义View(8) -- 汽车之家折叠列表

先看看汽车之家折叠列表的效果图


汽车之家折叠列表

接着看看实现的效果图

实现的效果

在这篇文章中主要采用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将会报错,将不会触发拖动事件

自定义View(8) -- 汽车之家折叠列表_第1张图片

从字面意思都可以看出需要一个完整的事件,所以需要在 ACTION_DOWN的时候调用 ViewDragHelper.processTouchEvent(ev)


在一步步的分析之下,这个效果就慢慢的完成了。有了新需求的时候,在动手应该理清思路,然后想好使用相关的api,处理一些手势可以使用OnGestureListener,处理拖动可以使用ViewDragHelper,这些都是系统封装好的辅助类,应该要合理的利用这些辅助类。相信如果不使用这些辅助类也可以写出这些效果,但是那样的话也会浪费大量的事件和精力,而且很容易出错。

本文源码下载地址:https://github.com/ChinaZeng/CustomView

你可能感兴趣的:(自定义View(8) -- 汽车之家折叠列表)