ViewGroup的自界说侧滑菜单

前语

前文咱们了解了ViewGroup的丈量与布局,可是并没有涉及到多少的交互逻辑,而 ViewGroup 的交互逻辑说起来规模其实是比较大的。从哪开端说起呢?

咱们暂时把 ViewGroup 的交互分为几块常识区,

  1. 事情的阻拦。
  2. 事情的处理(内部又分不同的处理办法)。
  3. 子View的移动与和谐。
  4. 父ViewGroup的和谐运动。

然后咱们先简略的做一个介绍,需求留意的是下面每一种办法独自拿出来都是一个常识点或常识面,这儿我个人了解的话,能够当做一个目录,咱们先简略的复习学习一下,心里过一遍,假如遇到哪一个常识点不是那么了解,那咱们也能够独自的对这个技能点进行查找与对应的学习。

而本文介绍完目录之后,咱们会针对其间的一种【子View的和谐运动】,也便是本文的侧滑菜单作用做讲解,后期也会对一些其他常用的作用再做剖析哦。

话不多说,Let’s go

Android自定义ViewGroup的交互,往往都是侧滑菜单开始

一、常用的几种交互办法

一般来说,常见的几种场景通常来说涉及到如下的几种办法。每一种办法又依据不同的作用能够分为不同的办法来完结。

需求留意的是有时分也并非仅有解,也能够经过不同的办法完结相同的作用。也能够经过不同的办法组合起来,完结一些特定的作用。

下面咱们先从事情的分发与阻拦说起:

1.1 事情的阻拦处理

自界说 ViewGroup 的一种分类,还比较常用的便是处理事情的抵触,常用的便是事情的阻拦,这一点就需求了解一点 View 的事情分发与阻拦的机制了。不过信任咱们多多少少都懂一点,究竟也是面试必出题了,下面简略说一下。

事情分发方面的差异:

事情分发机制首要有三个办法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

ViewGroup包括这三个办法,而View则只包括dispatchTouchEvent()、onTouchEvent()两个办法,不包括onInterceptTouchEvent()。

onTouchEvent() 与 dispatchTouchEvent() 信任咱们都有所了解。

onTouchEvent() 是事情的呼应与处理,而dispatchTouchEvent() 是事情的分发。

需求留意的是当某个子View的dispatchTouchEvent()回来true时,会间断Down事情的分发,一起在ViewGroup中记载该子View。接下来的Move和Up事情将由该子View直接进行处理。

而 onInterceptTouchEvent() 便是ViewGroup专有的阻拦处理,虽然子 View 没有阻拦的办法,可是子View能够经过调用办法 getParent().requestDisallowInterceptTouchEvent() 恳求父ViewGroup不阻拦事情。

经过 重写 onInterceptTouchEvent() 或许 运用 requestDisallowInterceptTouchEvent() 即可到达事情阻拦的处理。

关于事情的处理这儿能够引证一张图,十分的明晰:

Android自定义ViewGroup的交互,往往都是侧滑菜单开始

实践的应用,我这儿以 ViewPager2 嵌套 RecyclerView 的场景为例。

Android自定义ViewGroup的交互,往往都是侧滑菜单开始

如图所示的分类列表,咱们能够运用笔直的ViewPager2 嵌套笔直 RV 来完结。(当然了,详细的完结办法有多种,这儿不做相关的扩展讨论),那么就会呈现一个问题。什么时分翻滚子 RV 。什么时分翻滚笔直的父 VP2 。假如咱们有尝试过类似的场景,信任咱们就能了解这其间的坑点,有时分是 VP2 翻滚,有时分是子 RV 翻滚。看脸的。本质上仍是父布局与子布局在笔直翻滚的事情上有抵触的问题。

假如说不想搞这些抵触问题,换一个计划不就行了? 好吧,就算咱们运用其他计划处理了这个问题,那么现在问题是假如换成一个杂乱的分类列表呢?

Android自定义ViewGroup的交互,往往都是侧滑菜单开始

再比方这种杂乱的分类页面,因为数据量比较大,子 RV 的上拉滑动事情中还需求参加上拉加载的时刻。这一个分类滑动完毕之后,还需求切换右上的横向Tab。当横向 Tab 到最终一个了,并且滑动完毕之后,左侧的翻滚Tab才往下走一个。

面临如此杂乱的分类列表翻滚逻辑,仍是推荐运用自界说 ViewGroup 事情阻拦层,由自己操控什么机遇由子 RV 操控滑动,什么机遇由父 VP2 操控滑动。

逻辑都是相通的,这儿咱们以上图的简略分类页面作为示例,也是默许的常用的一个作用,其实当子 RV 翻滚完结之后再交由父 VP2 翻滚。咱们界说的阻拦层自界说ViewGroup如下:

class NestedScrollableHost : FrameLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    private var touchSlop = 0
    private var initialX = 0f
    private var initialY = 0f
    private val parentViewPager: ViewPager2?
        get() {
            var v: View? = parent as? View
            while (v != null && v !is ViewPager2) {
                v = v.parent as? View
            }
            return v as? ViewPager2
        }
    private val child: View? get() = if (childCount > 0) getChildAt(0) else null
    init {
        touchSlop = ViewConfiguration.get(context).scaledTouchSlop
    }
    private fun canChildScroll(orientation: Int, delta: Float): Boolean {
        val direction = -delta.sign.toInt()
        return when (orientation) {
            0 -> child?.canScrollHorizontally(direction) ?: false
            1 -> child?.canScrollVertically(direction) ?: false
            else -> throw IllegalArgumentException()
        }
    }
    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        return handleInterceptTouchEvent(e)
    }
    private fun handleInterceptTouchEvent(e: MotionEvent): Boolean {
        val orientation = parentViewPager?.orientation ?: return false
        if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
            return false
        }
        if (e.action == MotionEvent.ACTION_DOWN) {
            initialX = e.x
            initialY = e.y
            parent.requestDisallowInterceptTouchEvent(true)
        } else if (e.action == MotionEvent.ACTION_MOVE) {
            val dx = e.x - initialX
            val dy = e.y - initialY
            val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
            val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
            val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
            if (scaledDx > touchSlop || scaledDy > touchSlop) {
                return if (isVpHorizontal == (scaledDy > scaledDx)) {
                    //笔直的手势阻拦
                    parent.requestDisallowInterceptTouchEvent(false)
                    true
                } else {
                    if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
                        //子View能翻滚,不阻拦事情
                        parent.requestDisallowInterceptTouchEvent(true)
                        false
                    } else {
                        //子View不能翻滚,直接就阻拦事情
                        parent.requestDisallowInterceptTouchEvent(false)
                        true
                    }
                }
            }
        }
        return false
    }
}

这儿首要的逻辑便是对阻拦做处理,即可完结对应的阻拦到达想要的作用了,而假如是下图中杂乱的分类页面,也是类似的逻辑,仅仅需求手动的操控是否阻拦了罢了,都是能够完结相同的作用的。

而除了阻拦事情的自界说 ViewGroup 的场景之外,咱们用的比较多的便是事情的处理了,事情的处理又分许多,能够自己手撕 onTouchEvent 。也可经过 Scroller 来完结翻滚作用。也能经过 GestureDetector 手势辨认器来帮咱们完结。

下面一起来看看别离怎么完结:

1.2 自行处理事情的几种办法

在之前的 View 和 ViewGroup 的学习中,咱们一般都是自己来处理事情的呼应与阻拦,一般都是经过 MotionEvent 目标,拿到它的事情和一些方位信息,做制作和事情阻拦。

其实除了这一种最根本的办法,还有其他的办法也相同能够操作,分为不同的场景,咱们能够挑选性的运用不同的办法,都能够到达相同的作用。

onTouchEvent

咱们比较常见的便是在 dispatchTouchEvent()、onTouchEvent() 两个办法中经过 MotionEvent 目标来操作属性。

比较常用的便是经过手势记载坐标点,然后进行制作,或许进行事情的阻拦。

例如,假如想制作,咱们能够记载变化的X与Y,然后经过指定的公式转换为制作的变量,然后经过 invalidate 触发重绘,在 onDraw 中取到变化的变量制作出来,到达动画或翻滚或其他的一些作用。


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //按下的时分记载当时操作的是左侧约束圆仍是右侧的约束圆
            downX = event.getX();
            touchLeftCircle = checkTouchCircleLeftOrRight(downX);
            if (touchLeftCircle) {
                //假如是左侧
                //假如超越右侧最大值则不处理
                if (downX + perSlice > mRightCircleCenterX) {
                    return false;
                }
                mLeftCircleCenterX = downX;
            } else {
                //假如是右侧
                //假如超越左侧最小值则不处理
                if (downX - perSlice < mLeftCircleCenterX) {
                    return false;
                }
                mRightCircleCenterX = downX;
            }
        } 
        //中间的进度矩形是依据两头圆心点动态核算的
        mSelectedCornerLineRect.left = mLeftCircleCenterX;
        mSelectedCornerLineRect.right = mRightCircleCenterX;
        //悉数的事情处理完毕,变量赋值完结之后,开端重绘
        invalidate();
        return true;
    }

或许咱们能够经过记载X和Y的坐标,判别滑动的方向然后进行事情的阻拦:

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getRawX();
        int y = (int) ev.getRawY();
        int dealtX = 0;
        int dealtY = 0;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                dealtX = 0;
                dealtY = 0;
                // 确保子View能够接收到Action_move事情
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                dealtX += Math.abs(x - lastX);
                dealtY += Math.abs(y - lastY);
                // 这儿是否阻拦的判别依据是左右滑动,读者可依据自己的逻辑进行是否阻拦
                if (dealtX >= dealtY) { // 左右滑动恳求父 View 不要阻拦
                    getParent().requestDisallowInterceptTouchEvent(true);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

这种办法信任也是咱们见的最多的,看见代码就知道是什么意思,所以这儿就不放图与Demo了,假如想了解,也能够看看我之前的自界说View制作文章,根本都是这个套路。

接下来咱们继续,那么除了原始的 MotionEvent 做移动之外,咱们甚至能够运用 Scroller 来专门做翻滚的操作。仅仅相对来说 Scroller 是比较少用的。(究竟谷歌给咱们的太多的翻滚的控件了),可是掌握之后能够完结一些特其他作用,也是值得一学,下面一起看看吧。

Scroller

Scroller 译为翻滚器,是 ViewGroup 类中原生支撑的一个功能。Scroller 类并不担任翻滚这个动作,仅仅依据要翻滚的开端方位和完毕方位生成中间的过渡方位,然后构成一个翻滚的动画。

Scroller 自身并不神秘与杂乱,它仅仅仿照提供了翻滚时相应数值的变化,复写自界说 View 中的 computeScroll() 办法,在这儿获取 Scroller 中的 mCurrentX 和 mCurrentY,依据自己的规则调用 scrollTo() 办法,就能够到达平稳翻滚的作用。

本质上便是一个继续不断改写 View 的绘图区域的进程,给定一个开端方位、完毕方位、翻滚的继续时刻,Scroller 主动核算出中间方位和翻滚节奏,再调用 invalidate()办法不断改写。

需求留意的是调用scrollTo()和 scrollBy()的差异。其实也不杂乱,咱们翻译为中文的意思,scrollTo是翻滚到xx,scrollBy是翻滚了xx,这样是不是就一下就了解了。

剩下的便是需求重写computeScroll履行翻滚的逻辑。

下面举个简略的栗子:

咱们运用 Scroller仿照一个 简易的 ViewPager 作用。自界说ViewGroup中参加了9个View。并且占满全屏,然后咱们上滑动切换布局,当停手会判别是回到当时View仍是去下一个View。

ViewGroup的丈量与布局在之前的文章中咱们现已反复的复习了,这应该没什么问题:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        //设置ViewGroup的高度
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        mlp.height = mScreenHeight * childCount;
        setLayoutParams(mlp);
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                child.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
            }
        }
    }

然后便是对Touch和翻滚的操作:


    private int mLastY;
    private int mStart;
    private int mEnd;
    private Scroller mScroller;
    ...
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                mStart = getScrollY();
                break;
            case MotionEvent.ACTION_MOVE:
                //当停止动画的时分,它会立刻翻滚到结尾,然后向动画设置为完毕。
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                int dy = mLastY - y;
                if (getScrollY() < 0) {
                    dy = 0;
                }
                //开端翻滚
                scrollBy(0, dy);
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
                mEnd = getScrollY();
                int dScrollY = mEnd - mStart;
                if (dScrollY > 0) {
                    if (dScrollY < mScreenHeight / 3) {
                        mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
                    } else {
                        mScroller.startScroll(0, getScrollY(), 0, mScreenHeight - dScrollY);
                    }
                } else {
                    if (-dScrollY < mScreenHeight / 3) {
                        mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
                    } else {
                        mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
                    }
                }
                invalidate();
                break;
        }
        return true;
    }

那么完结的作用便是如下图所示:

Android自定义ViewGroup的交互,往往都是侧滑菜单开始

是不是相当于一个简配的ViewPager呢。。。

已然咱们的一些事情点击和移动能够经过 MotionEvent 来完结,一些特定的翻滚作用还能经过 Scroller 来完结。有没有更便利的一种办法悉数帮咱们完结呢?

接下来便是咱们常用的 GestureDetector 类了。能够协助咱们快速完结点击与翻滚作用。

GestureDetector

GestureDetector类,这个类指明是手势辨认器,它内部封装了一些常用的手势操作的接口,让咱们快速的处理手势事情,比方单机、双击、长按、翻滚等。

通常来说咱们运用 GestureDetector 分为三步:

  1. 初始化 GestureDetector 类。
  2. 界说自己的监听类OnGestureListener,例如完结 GestureDetector.SimpleOnGestureListener。
  3. 在 dispatchTouchEvent 或 onTouchEvent 办法中,经过GestureDetector将 MotionEvent 事情交给监听器 OnGestureListener

例如咱们最简略的比方自界说View,操控View跟从手指移动,咱们之前的做法是手撕 onTouchEvent,在按下的时分记载坐标,移动的时分核算坐标,然后重绘到达View跟从手指移动的作用。那么此时咱们就能运用另一种办法来完结:


  private GestureDetector mGestureDetector;
  private float centerX;
  private float centerY;
  private void init(Context context) {
        mGestureDetector = new GestureDetector(context, new MTouchDetector());
        setClickable(true);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //将Event事情交给监听器 OnGestureListener
        mGestureDetector.onTouchEvent(event);
        return super.dispatchTouchEvent(event);
    }
      private class MTouchDetector extends GestureDetector.SimpleOnGestureListener {
        public boolean onDown(MotionEvent e) {
            YYLogUtils.w("MTouchDetector-onDown");
            return super.onDown(e);
        }
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            centerY -= distanceY;
            centerX -= distanceX;
            //鸿沟处理 ...
             postInvalidate();
        }
    }

上面咱们经过 GestureDetector 来完结了 onTouch 中的制作作用,那么相同的咱们也能够经过 GestureDetector 来完结 onTouch 中的时刻阻拦作用:


  private GestureDetector mGestureDetector;
  private void init(Context context) {
        mGestureDetector = new GestureDetector(context, new MTouchDetector());
        setClickable(true);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        // 先告诉父Viewgroup,不要阻拦,然后再内部判别是否阻拦
        getParent().requestDisallowInterceptTouchEvent(true);
        //将Event事情交给监听器 OnGestureListener
        mGestureDetector.onTouchEvent(event);
        return super.dispatchTouchEvent(event);
    }
      private class MTouchDetector extends GestureDetector.SimpleOnGestureListener {
        public boolean onDown(MotionEvent e) {
            YYLogUtils.w("MTouchDetector-onDown");
            return super.onDown(e);
        }
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
         if (1.732 * Math.abs(distanceX) >= Math.abs(distanceY)) {
                YYLogUtils.w("恳求不要阻拦我");
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;
            } else {
                YYLogUtils.w("阻拦我");
                getParent().requestDisallowInterceptTouchEvent(false);
                return false;
            }
        }
        ...
    }

GestureDetector 甚至能完结 Scroller 的作用,完结山寨ViewPager的作用,


  private GestureDetector mGestureDetector;
  private void init(Context context) {
        mGestureDetector = new GestureDetector(context, new MTouchDetector());
        setClickable(true);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        //将Event事情交给监听器 OnGestureListener
        mGestureDetector.onTouchEvent(event);
        return super.dispatchTouchEvent(event);
    }
      private class MTouchDetector extends GestureDetector.SimpleOnGestureListener {
        public boolean onDown(MotionEvent e) {
            YYLogUtils.w("MTouchDetector-onDown");
            return super.onDown(e);
        }
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
           //直接移动
           scrollBy((int) distanceX, getScrollY());
        }
        ...
    }

能够看到咱们直接在 GestureDetector 的 onScroll 回调中直接 scrollBy 有上面那种 Scroller 的作用了,比较跟手可是不能指定跳转到页面,可是假如想要更好的ViewPager作用,咱们需求结合 Scroller 合作的运用就能够有更好的作用。


  private GestureDetector mGestureDetector;
  private int currentIndex;
  private int startX;
  private int endX;
  private Scroller mScroller;
  private void init(Context context) {
        mGestureDetector = new GestureDetector(context, new MTouchDetector());
        setClickable(true);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = (int) event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                endX = (int) event.getX();
                int tempIndex = currentIndex;
                if (startX - endX > getWidth() / 2) {   
                    tempIndex++;
                } else if (endX - startX > getWidth() / 2) {  
                    tempIndex--;
                }
                scrollIndex(tempIndex);
                break;
        }
        return true;
    }
    private class MTouchDetector extends GestureDetector.SimpleOnGestureListener {
        public boolean onDown(MotionEvent e) {
            YYLogUtils.w("MTouchDetector-onDown");
            return super.onDown(e);
        }
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
           //直接移动
           scrollBy((int) distanceX, getScrollY());
           return true;
        }
        ...
    }
      private void scrollIndex(int tempIndex) {
        //第一页不能滑动
        if (tempIndex < 0) {
            tempIndex = 0;
        }
        //最终一页不能滑动
        if (tempIndex > getChildCount() - 1) {
            tempIndex = getChildCount() - 1;
        }
        currentIndex = tempIndex;
        mScroller.startScroll(getScrollX(), 0, currentIndex * getWidth() - getScrollX(), 0);
        postInvalidate();
    }
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            postInvalidate();
        }
    }

这样经过 GestureDetector 结合 Scroller 就能够到达,按着翻滚的作用和放开主动翻滚到指定索引的作用了。

GestureDetector 的确是很便利,协助咱们封装了事情的逻辑,咱们只需求对相应的时刻做出呼应即可,我愿称之为万能事情处理器。

除了这些独自的事情的处理,在同一个ViewGroup中假如有多个子View,咱们还能经过 ViewDragHelper 来完结子 View 的自在翻滚,甚至当其间一个View翻滚的一起,我能够做对应的变化,(哟,是不是有behavior那味了 )

1.3 子View的翻滚与和谐交互

一句话来介绍 ViewDragHelper ,它是用于在 ViewGroup 内部拖动视图的。

ViewDragHelper 也是谷歌帮咱们封装好的工具类, 其本质便是内部封装了MotionEvent 和 Scroller,记载了移动的X和Y,让 Scroller 去履行翻滚逻辑,然后完结让 ViewGroup 内部的子 View 能够实翻滚与和谐翻滚的逻辑。

怎么运用?固定的套路:


    private void initView() {
        //经过回调,奉告告诉了移动了多少,接触方位,接触速度
        viewDragHelper = ViewDragHelper.create(this, callback);
    }
    /**
     * 接触事情传递给ViewDragHelper
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;  //传递给viewDragHelper。回来true,消费此事情
    }
    /**
     * 是否需求传递给viewDragHelper阻拦事情
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
        return result;        //让传递给viewDragHelper判别是否需求阻拦
    }
     //回调处理有许多,依据不同的需求来完结
     private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        @Override     //是否捕获child的接触事情,是否能移动
        public boolean tryCaptureView(View child, int pointerId) {
            return child == redView || child == blueView;  //能够移动赤色view
        }
        @Override  //chlid的移动后的回调,监听
        public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
           // Log.d("tag", "被移动了");
        }
        @Override   //控件水平可拖拽的规模,现在不能约束鸿沟,用于手指抬起,view动画移动到的方位
        public int getViewHorizontalDragRange(View child) {
            return getMeasuredWidth() - child.getMeasuredWidth();
        }
        @Override   //控件笔直可拖拽的规模,现在不能约束鸿沟,用于手指抬起,view动画移动到的方位
        public int getViewVerticalDragRange(View child) {
            return getMeasuredHeight() - child.getMeasuredHeight();
        }
        @Override    //操控水平移动的方向。多少间隔,left = child.getleft() + dx;
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //在这儿约束最大的移动间隔,不能出鸿沟
            if (left < 0) {
                left = 0;
            } else if (left > getMeasuredWidth() - child.getMeasuredWidth()) {
                left = getMeasuredWidth() - child.getMeasuredWidth();
            }
            return left;
        }
        @Override      //操控笔直移动的方向。多少间隔
        public int clampViewPositionVertical(View child, int top, int dy) {
            //在这儿约束最大的移动间隔,不能出鸿沟
            if (top < 0) {
                top = 0;
            } else if (top > getMeasuredHeight() - child.getMeasuredHeight()) {
                top = getMeasuredHeight() - child.getMeasuredHeight();
            }
            return top;
        }
        @Override      //当时child移动后,其他view跟着做对应的移动。用于做随同移动
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            //判别当蓝色的移动的时分,赤色跟着移动相同的间隔
            if (changedView == blueView) {
                redView.layout(redView.getLeft() + dx, redView.getTop() + dy, redView.getRight()
                        + dx, redView.getBottom() + dy);
            } else if (changedView == redView) {
                blueView.layout(blueView.getLeft() + dx, blueView.getTop() + dy, blueView.getRight()
                        + dx, blueView.getBottom() + dy);
            }
        }
        @Override    //手指抬起后,履行相应的逻辑
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //以分界线判别在左面仍是右边
            int centerLeft = getMeasuredWidth() / 2 - releasedChild.getMeasuredWidth() / 2;
            if (releasedChild.getLeft() < centerLeft) {
                //左面移动。移动到的间隔
                viewDragHelper.smoothSlideViewTo(releasedChild, 0, releasedChild.getTop());
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);  //改写整个view
            } else {
                //右边移动。移动到的间隔
                viewDragHelper.smoothSlideViewTo(releasedChild, getMeasuredWidth() -
                        releasedChild.getMeasuredWidth(), releasedChild.getTop());
                ViewCompat.postInvalidateOnAnimation(DragLayout.this);    //改写整个view
            }
        }
    };
    @Override
    public void computeScroll() {
        //假如正在移动中,继续改写
        if (viewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(DragLayout.this);
        }
    }

ViewDragHelper (这名字真的取的很好),其实便是翻滚(拖拽)的协助类,能够独自的翻滚 ViewGroup 其间的一个子View,也能够用于多个子View的和谐翻滚。

这也是本期侧滑菜单选用的计划,多个子View的和谐翻滚的应用。

关于更多 ViewDragHelper 的基础运用,咱们假如不了解能够看鸿洋的老文章【传送门】

关于View/ViewGroup的事情,除了这些常用的之外,还有例如多指触控事情,缩放的事情 ScaleGestureDecetor 等,因为比较少用,这儿就不过多的介绍,其实逻辑与道理都是差不多的,假如有用到的话,能够再查阅对应的文档哦。

1.4 ViewGroup之间的嵌套与和谐作用

前面讲到的都是ViewGroup内部的事情处理,关于ViewGroup之间的嵌套翻滚来说的话,其实这是另一个话题了,跟自界说ViewGroup内部的事情处理比较,属实是另一个分支了,演变为多个处理计划,多个常识点了。

我之前的文章有过简略的介绍,现在首要是分几种思路

  1. NestedScrolling机制
  2. CoordinatorLayout + Behavior
  3. CoordinatorLayout + AppBarLayout
  4. ConstraintLayout / MotionLayout 机制

NestedScrollingParent 与 NestedScrollingChild,NestedScrolling 机制能够让父view和子view在翻滚时进行合作,其根本流程如下:当子view开端翻滚之前,能够告诉父view,让其先于自己进行翻滚,子view翻滚之后,还能够告诉父view继续翻滚。

能够看看我之前的文章【传送门】

因为手撕 NestedScrolling 仍是有点难度,关于一些嵌套翻滚的需求,谷歌推出了 NestedScrollView 来完结嵌套翻滚。而关于一些常见的、场景化的和谐作用来说,谷歌推出 CoordinatorLayout 封装类,能够结合 Behavior 完结一些自界说的和谐作用。

虽说 Behavior 的界说比 NestedScrolling 算简略一点了,可是也比较杂乱啊,有没有更简略的,关于一些更常见的场景,谷歌说能够结合 AppBarLayout 做出一些常见的翻滚作用。也的确处理了咱们大部分翻滚作用。

关于这一点能够看看我之前的文章【传送门】

虽然经过监听 AppBarLayout 的高度变化百分比,能够做出各种各样的其他布局的和谐动画作用。可是一个是功率问题,一个是难度问题,总有一些特定的作用无法完结。

所以谷歌推出了 ConstraintLayout / MotionLayout 能更便利的做出各种和谐作用。

关于这一点能够看看我之前的文章【传送门】

那么到此根本就处理了外部ViewGroup之前的嵌套与和谐问题。

这儿就不翻开说了,这是另外一个体系,有需求的同学能够自行查找了解一些。咱们仍是回归正题。

关于自界说 ViewGroup 的事情相关,咱们就先开始的整理出一个目录了,接下来咱们仍是快看看怎么界说一个侧滑菜单吧。

二、ViewDragHelper的侧滑菜单完结

目录列好了之后,咱们就能够按需挑选或组合就能够完结对应的作用。

比方咱们这一期的侧滑菜单,其实便是涉及到了交互与嵌套的问题,而咱们经过上述的学习,咱们就知道咱们能够有多种办法来完结。

  1. 比方手撕 onTouchEvent + Scroller(为了主动回来)
  2. 再简略点 GestureDetector + Scroller(为了主动回来)
  3. 再简略点 ViewDragHelper 即可(便是对Scroller的封装)

咱们这儿就以最简略的 ViewDragHelper 计划来完结

咱们分为内容布局和右侧躲藏的删去布局,默许的布局办法是内容布局占满布局宽度,让删去布局到屏幕外。

首先咱们要丈量与布局:

private View contentView;
private View deleteView;
private int contentWidth;
private int contentHeight;
private int deleteWidth;
private int deleteHeight;
public class SwipeLayout extends FrameLayout {
    //完结初始化,获取控件
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        contentView = getChildAt(0);
        deleteView = getChildAt(1);
    }
    //完结丈量,获取高度,宽度
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        contentWidth = contentView.getMeasuredWidth();
        contentHeight = contentView.getMeasuredHeight();
        deleteWidth = deleteView.getMeasuredWidth();
        deleteHeight = deleteView.getMeasuredHeight();
    }
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        contentView.layout(0, 0, contentWidth, contentHeight);
        deleteView.layout(contentView.getRight(), 0, contentView.getRight() + deleteWidth, deleteHeight);
    }
}

咱们直接继承 FrameLayout 也不必自行丈量了,布局的时分咱们布局到屏幕外的右侧即可。

接下来咱们就运用 viewDragHelper 来操作子View了。都是固定的写法


    private void init() {
        //是否处理接触,是否处理阻拦
        viewDragHelper = ViewDragHelper.create(this, callback);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return viewDragHelper.shouldInterceptTouchEvent
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                float moveX = event.getX();
                float moveY = event.getY();
                float dx = moveX - downX;
                float dy = moveY - downY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    //在水平移动。恳求父类不要阻拦
                    requestDisallowInterceptTouchEvent(true);
                }
                downX = moveX;
                downY = moveY;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        viewDragHelper.processTouchEvent(event);
        return true;
    }

留意的是这儿对阻拦的事情做了方向上的判别,都是已学的内容。接下来的重点便是 callback 回调的处理。


    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        //点击ContentView和右侧的DeleteView都能够触发事情
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == contentView || child == deleteView;
        }
        //控件水平可拖拽的规模,最多也就拖出一个右侧DeleteView的宽度
        @Override
        public int getViewHorizontalDragRange(View child) {
            return deleteWidth;
        }
        //操控水平移动的方向间隔
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            //做鸿沟的约束
            if (child == contentView) {
                if (left > 0) left = 0;
                if (left < -deleteWidth) left = -deleteWidth;
            } else if (child == deleteView) {
                if (left > contentWidth) left = contentWidth;
                if (left < contentWidth - deleteWidth) left = contentWidth - deleteWidth;
            }
            return left;
        }
        //当时child移动后,其他view跟着做对应的移动。用于做随同移动
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            //做内容布局移动的时分,删去布局跟着相同的移动
            if (changedView == contentView) {
                deleteView.layout(deleteView.getLeft() + dx, deleteView.getTop() + dy,
                        deleteView.getRight() + dx, deleteView.getBottom() + dy);
            } else if (changedView == deleteView) {
                //当删去布局移动的时分,内容布局做相同的移动
                contentView.layout(contentView.getLeft() + dx, contentView.getTop() + dy,
                        contentView.getRight() + dx, contentView.getBottom() + dy);
            }
        }
        @Override
        public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //松开之后,缓慢滑动,看是到翻开状况仍是到封闭状况
            if (contentView.getLeft() < -deleteWidth / 2) {
                //翻开
                open();
            } else {
                //封闭
                close();
            }
        }
    };
    /**
     * 翻开开关的的办法
     */
    public void open() {
        viewDragHelper.smoothSlideViewTo(contentView, -deleteWidth, 0);
        ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
    }
    /**
     * 封闭开关的办法
     */
    public void close() {
        viewDragHelper.smoothSlideViewTo(contentView, 0, 0);
        ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
    }
    /**
     * 重写移动的办法
     */
    @Override
    public void computeScroll() {
        if (viewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(SwipeLayout.this);
        }
    }

现已做了详细的注释了,是不是很清楚了呢? 作用图如下:

Android自定义ViewGroup的交互,往往都是侧滑菜单开始

四、回调与封装

在一些列表上运用的时分咱们需求一个Item只能翻开一个删去布局,那么咱们需求一个管理类来管理,手动的翻开和封闭删去布局。

public class SwipeLayoutManager {
    private SwipeLayoutManager() {
    }
    private static SwipeLayoutManager mInstance = new SwipeLayoutManager();
    public static SwipeLayoutManager getInstance() {
        return mInstance;
    }
    //记载当时翻开的item
    private SwipeLayout currentSwipeLayout;
    public void setSwipeLayout(SwipeLayout layout) {
        this.currentSwipeLayout = layout;
    }
    //封闭当时翻开的item。layout
    public void closeCurrentLayout() {
        if (currentSwipeLayout != null) {
            currentSwipeLayout.close();  //调用的自界说控件的close办法
            currentSwipeLayout=null;
        }
    }
    public boolean isShouldSwipe(SwipeLayout layout) {
        if (currentSwipeLayout == null) {
            //没有翻开
            return true;
        } else {
            //有翻开的
            return currentSwipeLayout == layout;
        }
    }
    //清空currentLayout
    public void clearCurrentLayout() {
        currentSwipeLayout = null;
    }
}

咱们还需求对翻开封闭的状况做管理

    enum SwipeState {
        Open, Close;
    }
    private SwipeState currentState = SwipeState.Close; //默许为封闭

假如是翻开的状况,咱们还需求对事情做阻拦的处理

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
        if (!SwipeLayoutManager.getInstance().isShouldSwipe(this)) {
            //在此封闭现已翻开的item。
            SwipeLayoutManager.getInstance().closeCurrentLayout();
            result = true;
        }
        return result;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //假如当时的是翻开的,下面的逻辑不能履行了
        if (!SwipeLayoutManager.getInstance().isShouldSwipe(this)) {
            requestDisallowInterceptTouchEvent(true);
            return true;
        }
        ...
    }

回调的处理,在 onViewPositionChanged 的移动回调中,咱们能够经过内容布局的left是否为0 或许 -deleteWidth 就能够判别当时的布局状况是否是翻开状况。

    private OnSwipeStateChangeListener listener;
    public void seOnSwipeStateChangeListener(OnSwipeStateChangeListener listener) {
        this.listener = listener;
    }
    public interface OnSwipeStateChangeListener {
        void Open();
        void Close();
    }
    ...
       //当时child移动后,其他view跟着做对应的移动。用于做随同移动
        @Override
        public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
            //做内容布局移动的时分,删去布局跟着相同的移动
            if (changedView == contentView) {
                deleteView.layout(deleteView.getLeft() + dx, deleteView.getTop() + dy,
                        deleteView.getRight() + dx, deleteView.getBottom() + dy);
            } else if (changedView == deleteView) {
                //当删去布局移动的时分,内容布局做相同的移动
                contentView.layout(contentView.getLeft() + dx, contentView.getTop() + dy,
                        contentView.getRight() + dx, contentView.getBottom() + dy);
            }
            //判别开,关的逻辑
            if (contentView.getLeft() == 0 && currentState != SwipeState.Close) {
                //封闭删去栏.删去实例
                currentState = SwipeState.Close;
                if (listener != null) {
                    listener.Close();    //在此回调封闭办法
                }
                SwipeLayoutManager.getInstance().clearCurrentLayout();
            } else if (contentView.getLeft() == -deleteWidth && currentState != SwipeState.Open) {
                //开启删去栏。获取实例
                currentState = SwipeState.Open;
                if (listener != null) {
                    listener.Open();     //在此回调翻开办法
                }
                SwipeLayoutManager.getInstance().setSwipeLayout(SwipeLayout.this);
            }
        }

这样就完结了悉数的逻辑啦,其实了解之后并不杂乱。

跋文

其实关于侧滑回来的作用,网络上有许多的计划,这也仅仅其间的一种,为了便利咱们了解 viewDragHelper 的运用,其实它还能够用于许多其他的场景,比方底部菜单的展现,Grid网格的动态改换等等。

最近公司的项目抓的很紧,所以更新时刻没有那么安稳,后边的计划大概还有两期,尽量在年前更玩相关系列吧。。。

好了,关于本文的内容假如想查看源码能够点击这儿 【传送门】。你也能够重视我的这个Kotlin项目,我有时刻都会继续更新。

常规,我如有讲解不到位或错漏的当地,期望同学们能够指出交流。

假如感觉本文对你有一点点的启发,还望你能点赞支撑一下,你的支撑是我最大的动力。

Ok,这一期就此完结。

Android自定义ViewGroup的交互,往往都是侧滑菜单开始