自定义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将会报错,将不会触发拖动事件


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


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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容