26
2017
09

自定义View系列(8)--越界回弹ScrollView

难度

中等

效果说明

越界回弹的效果不用多说了吧,大家应该都知道, 不知道的看下方效果图。

效果图

效果图

特性说明

  • 支持阻尼系数
  • 支持多指触控
  • 支持上拉回弹、下拉回弹
  • 支持设置开启/关闭回弹:ENABLED_ALL、ENABLED_TOP、ENABLED_BOTTOM 、ENABLED_NONE
  • 不影响原有手势的分发处理
  • 支持设置最大滑动距离
  • 支持设置插值器

实现原理

整体采用offsetTopAndBottom()+ValueAnimator实现。

事件分发处理

重写dispatchTouchEvent(MotionEvent ev)方法,在ACTION_DOWN中判断是否可以下拉或者上拉,如果可以,就拦截此次事件

滑动处理

ACTION_MOVE中,计算每次滑动的差值diffY,然后使用offsetTopAndBottom() 进行滑动

手指抬起处理

ACTION_UP中,获取已滑动的距离scrollY,然后使用ValueAnimator计算每一帧滑动的距离,最后再次使用offsetTopAndBottom()进行滑动

多点触控

多点触控其实很简单,都是有套路可寻的,只要单点触控没问题,多点触控其实很好实现,因为虽然是多点触控,但是实际上只有一个手指处于活跃状态。

关于dispatchTouchEvent(MotionEvent ev)方法

在Android的整个事件传递体系中,很多人都知道dispatchTouchEvent(MotionEvent ev)方法是用来分发事件的,分发后的事件如果由自身处理,则需要重写
onTouchEvent(MotionEvent ev)进行相关操作,但有时候这种方式很麻烦,特别是在我们继承已有的Layout时,比如ScrollViewFrameLayout等,
因为这些Layout本身就有一些事件的处理机制,如何在不破坏已有的处理机制的基础上再加上我们自己的处理逻辑,这是一个较为困难的问题。

dispatchTouchEvent(MotionEvent ev)方法要先于onInterceptTouchEvent(MotionEvent ev)方法执行,我们可以在dispatchTouchEvent(MotionEvent ev)方法中处理一些拦截逻辑,这比在onInterceptTouchEvent(MotionEvent ev)方法中处理拦截逻辑有时候会更好。因为在dispatchTouchEvent(MotionEvent ev)方法中我们可以同时处理事件的拦截以及view的滑动等操作。当然要使用哪一个方法还要视具体情况而定。

在继承ScrollView时,我们既要保证原有的ScrollView的逻辑不变,还要在此基础上添加我们自己的逻辑,使用dispatchTouchEvent(MotionEvent ev)是一个比较好的选择。

核心代码

  @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (mAnimator.isStarted()) mAnimator.cancel();
                mActivePointerId = ev.getPointerId(0);
                mLastY = (int) ev.getY();
                canPullDown = isCanPullDown();
                canPullUp = isCanPullUp();
                break;
            case MotionEvent.ACTION_MOVE:
                final int y = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                int diffY = y - mLastY;
                if ((canPullUp || canPullDown)) {
                    ViewParent parent = getParent();
                    if (parent != null) parent.requestDisallowInterceptTouchEvent(true);
                    move(diffY);
                }
                mLastY = y;
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (canPullDown || canPullUp) {
                    final int scrollY = mChild.getTop();
                    mLastFrameValue = scrollY;
                    mAnimator.setIntValues(scrollY, 0);
                    mAnimator.start();
                }
                mActivePointerId = MotionEvent.INVALID_POINTER_ID;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                final int downActionIndex = ev.getActionIndex();
                mLastY = (int) ev.getY(downActionIndex);
                mActivePointerId = ev.getPointerId(downActionIndex);
                break;
            case MotionEvent.ACTION_POINTER_UP:
                final int upActionIndex = ev.getActionIndex();
                final int pointerId = ev.getPointerId(upActionIndex);
                if (pointerId == mActivePointerId) {
                    final int newPointerIndex = upActionIndex == 0 ? 1 : 0;
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                }
                mLastY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }
        super.dispatchTouchEvent(ev);//分发父view的事件
        return true;
    }

以上是实现整个效果的核心代码,更多详细处理逻辑请移步至PopularEffect

PopularEffect是一个汇集各种效果的仓库,除此之外,还对每种效果的实现方式都做了剖析,并编写了对应的样例。虽然仓库已经建立了快一年,但实际上并没有精力去维护,现在打算把它维护起来,我只要有时间,都会尽量添加一些效果分析和样例,虽然只有我一个人维护这个仓库,注定速度上会很慢(分析效果以及编写样例是最为费时费力的),但我相信水滴石穿! 感兴趣的可以stat一波(手动滑稽)。

上一篇:Android 深入浅出AIDL(二) 下一篇:页面开发总结