简单但是强大的阻尼滚动ViewGroup

前言

上海这两天下雨,刚好上周末又碰上双休,宅在家里没有出去,撸了一个阻尼滑动的自定义ViewGroup,加上今天也不是很忙,就赶紧把它撸完啦。

阻尼滚动貌似是iOS原生支持的效果,每次和PM讨论需求时,一碰到阻尼滑动之类的需求时,就说安卓做不了。以后再碰到类似的需求时,就可以跟PM愉快的装逼啦。

照例,先看效果图。

项目使用

要使用OverScrollLayout给你的项目加上阻尼滚动效果,整个过程只需要两个步骤:

1、在项目的attrs.xml文件中添加如下属性:

    <declare-styleable name="OverScrollLayout">
        <attr name="dampingFactor" format="float" />
        <attr name="dampingDirection">
            <flag name="left" value="0x0001" />
            <flag name="top" value="0x0010" />
            <flag name="right" value="0x0100" />
            <flag name="bottom" value="0x1000" />
        </attr>
    </declare-styleable>

2、在布局文件中引用OverScrollLayout,并分别设置dampingFactordampingDirection的值,他们的含义分别如下:

  • dampingFactor阻尼因子,值越小表示阻力越大,默认为1(即没有阻尼效果)。
  • dampingDirection阻尼方向,顾名思义,支持左、上、右、下这4种阻尼方向。

请务必注意,在xml文件中使用OverScrollLayout给你的布局添加阻尼效果时,OverScrollLayout的直接子View只能有一个!(可以参考ScrollView。)

举个栗子:

SimpleActivity.java

public class SimpleActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple);
    }

    public void onClick(View view) {
        Toast.makeText(this, "button click", Toast.LENGTH_SHORT).show();
    }
}

activity_simple.xml

<?xml version="1.0" encoding="utf-8"?>
<com.xiaohongshu.dampingscrolling.OverScrollLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:dampingDirection="left|top|right|bottom"
    app:dampingFactor="0.7">

    <Button
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_blue_light"
        android:gravity="center"
        android:onClick="onClick"
        android:text="最简单的阻尼滚动"
        android:textColor="@android:color/white" />

</com.xiaohongshu.dampingscrolling.OverScrollLayout>

这样,一个非常简单的支持四向阻尼效果的小项目就出来了!看看效果图:

实现过程

自定义View系列的很多控件都是一点一点的效果加起来,最开始实现一个简单粗糙的逻辑,然后慢慢加上各种优化,最后才变成一个看起来很酷的效果。

而对于这里的DampingViewGroup,整个实现过程分为4个部分,包括:

  • 1、自定义ViewGroup实现单方向的阻尼
  • 2、位运算处理多方向阻尼时的逻辑
  • 3、处理滑动冲突,支持常用ViewGroup作为子View时的阻尼效果
  • 4、解决Button等作为子View时无法响应点击事件的bug

下面来逐个分析吧。

自定义ViewGroup实现单方向的阻尼

先来看看阻尼效果发生的场景(嫌啰嗦的可以跳过):

当一个View在某个方向上无法继续滚动时,如果此时用户继续用手指往该方向进行拖动,则此时该View应该发生阻尼效果;当用户手指松开时,该View回弹到发生阻尼效果之前的位置。

比如一个ScrollView

  • 初始状态下的ScrollView是只能往下滚动的,如果此时用户手指从上往下拖动,则ScrollView应当发生顶部方向的阻尼滚动
  • 而如果将ScrollView滚动到底部之后,用户继续用手指在ScrollView上从下往上拖动,则ScrollView应当发生底部的阻尼滚动效果

那么如果在ScrollView左滑应不应该让它进行阻尼滚动呢?我认为阻尼滚动的效果只是根据用户习惯而产生的一种提升用户体验的需求,ScrollView本身就不能左右滑动,如果此时还加一个左右方向的阻尼滚动,这是不合情理的。

(你牛逼你也可以加啊,反正老板打死的是你又不是我。)

我们先来给一个TextView来加上顶部方向的滑动阻尼,即如果用户在TextView顶部从上往下滑动时,TextView的顶部出现阻尼效果。

需求拆分到这种程度,是不是已经非常简单了:重写ViewGrouponTouchEvent()方法,并判断当用户手指从上往下拖动时将ViewGroup的内容往下移即可。

onTouchEvent处理滑动效果

来看最简单的自定义ViewGroup的阻尼效果代码:

public class OverScrollLayout extends FrameLayout {

    private static final float DEFAULT_FATOR = 1;
    /**
     * 阻尼因子
     */
    private float mFator = DEFAULT_FATOR;
    private Scroller mScroller;
    /**
     * 记录上一次触摸事件
     */
    private MotionEvent mLastMotionEvent;

    public OverScrollLayout(Context context) {
        this(context, null);
    }

    public OverScrollLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public OverScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionEvent = MotionEvent.obtain(event);
                break;
            case MotionEvent.ACTION_MOVE:

                int dx = (int) (event.getRawX() - mLastMotionEvent.getRawX());
                int dy = (int) (event.getRawY() - mLastMotionEvent.getRawY());
                if (Math.abs(dx) < Math.abs(dy) && dy > 0) {

                    smoothScrollBy(0, -(int) (dy * mFator));
                }
                mLastMotionEvent = MotionEvent.obtain(event);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:

                smoothScrollTo(0, 0);
                break;
        }
        return true;
    }

    private void smoothScrollTo(int fx, int fy) {

        int dx = fx - mScroller.getFinalX();
        int dy = fx - mScroller.getFinalY();
        smoothScrollBy(dx, dy);
    }

    private void smoothScrollBy(int dx, int dy) {

        mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
        super.computeScroll();
    }
}    

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<com.xiaohongshu.dampingscrolling.OverScrollLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_blue_light"
        android:gravity="center"
        android:text="最简单的阻尼滚动"
        android:textColor="@android:color/white" />
</com.xiaohongshu.dampingscrolling.OverScrollLayout>

代码很简单,有几点要说一下。

1、首先,OverScrollLayout没有选择直接继承ViewGroup,而是继承自FrameLayout,这就意味着我们不需要去考虑onMeasureonLayoutonDraw等等的事情,非常省事。

从另一方面讲,对于这个需求,继承自ViewGroup也是非常没有必要的。

2、其次,在onTouchEvent方法中处理MotionEvent等事件时,碰到MotionEvent.ACTION_DOWN时一定要记得返回true,否则就无法收到后面的一系列MotionEvent事件了。

3、保存上一次触摸事件的MotionEvent对象时,一定要用MotionEvent.obtain(event)的方式来获取它的拷贝,而不是直接用等于号(=)来保存它的引用。

因为在一次完整的触摸过程中(ACTION_DOWN -> ACTION_MOVE -> … -> ACTION_UP),onTouchEvent方法的形参始终都是同一个MotionEvent对象,如果用等于号(=)来保存上一次触摸的引用的话,做位移运算(event.getRawX() - mLastMotionEvent.getRawX())时就会出错。

4、在ACTION_UP或者ACTION_CANCEL时,记得使ViewGroup回到初始位置。

5、为了使滑动平滑连贯,使用Scroller进行滚动,关于Scroller的用法这里不再赘述。

6、为了产生阻尼效果,代码第41行让ViewGroup滚动的时候让dy乘以了一个系数mFactor,将mFactor的值设置在(0,1]的区间就可以使滚动产生阻尼效果了。

灵活配置阻尼因子

我们叫这个mFactor阻尼因子阻尼因子默认的值为1,所以ViewGroup滚动时没有阻尼效果。想产生阻尼效果很简单,直接修改阻尼因子的值即可,可是怎样使阻尼因子可以动态配置呢?类似于给TextView配置text、给ImageView配置src。。

我们在res文件夹新建一个attrs.xml文件,这个文件可以存放自定义控件的各种属性,属性一旦定义好就可以在布局文件中进行引用啦。

比如我们要给OverScrollLayout添加一个阻尼因子的属性,可以这样定义:

    <declare-styleable name="OverScrollLayout">
        <attr name="dampingFactor" format="float" />
    </declare-styleable>

declare-styleable即表示一组自定义属性的集合,name后面跟自定义控件的名字。

declare-styleable里面就是填写各种自定义属性的地方,我们这里暂时只定义阻尼因子,所以定义一个属性dampingFactor,属性对应的值的类型是float

这样,自定义属性就定义好了,快去布局文件中引用吧。

完整的布局文件代码如下:

<?xml version="1.0" encoding="utf-8"?>
<com.xiaohongshu.dampingscrolling.OverScrollLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:dampingFactor="0.7">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_blue_light"
        android:gravity="center"
        android:text="最简单的阻尼滚动"
        android:textColor="@android:color/white" />

</com.xiaohongshu.dampingscrolling.OverScrollLayout>

代码第3行,在自定义控件或自定义控件的父控件的属性中引用命名空间,然后就可以引用自定义属性了。

第6行,引用自定义属性,并给其赋值。

好了,自定义属性的配置就到这里了,怎么在代码中获取并引用呢?

OverScrollLayout的构造方法中添加如下代码:

    public OverScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.OverScrollLayout, defStyleAttr, 0);
        int count = a.getIndexCount();
        for (int i = 0; i < count; i++) {
            int index = a.getIndex(i);
            switch (index) {
                case R.styleable.OverScrollLayout_dampingFactor:
                    mFator = a.getFloat(index, DEFAULT_FATOR);
                    break;
            }
        }
        a.recycle();

        mScroller = new Scroller(context);
    }

代码第4~5行,将AttributeSet按照我们先前定义的自定义属性的格式解析成我们需要的TypedArray对象。

第6~14行,遍历所有的属性,并取出其中的值。由于之前定义dampingFactor属性的时候定义值的类型是float,所以这里用TypedArray.getFloat()来获取。

第15行,回收该TypedArray对象。

至此,自定义属性的代码获取也写好啦。阻尼因子值为0.7时的效果图如下:

位运算处理多方向阻尼时的逻辑

单方向的阻尼效果已经处理好了,现在来增加对其余3个方向的支持,所以我们在attrs.xml文件中新增一个自定义属性dampingDirection,用来表示阻尼方向

阻尼方向可以有4个,为什么用一个自定义属性就可以实现了呢?回想一下,我们平常开发中哪些控件只用一个自定义属性就可以支持多种状态的。

比如:

  • ActivityAndroidMainfest.xml文件中注册时的windowSoftInputMode
  • EditTextinputTypeimeOptions
  • TextViewautoLink
  • Viewgravitylayout_gravity
  • ··· 等等

我们以TextViewautoLink来举例,当在布局文件中给TextViewautoLink设置了下列值当中的一种时,当TextView中的字符串符合该值相对应的正则表达式时,则会提供自动高亮和点击的功能。

  • none: 什么都不匹配
  • web: 匹配网页地址
  • email: 匹配邮件地址
  • phone: 匹配电话号码
  • map: 匹配居住地址
  • all: 匹配全部,相当于web|email|phone|map,即匹配所有的网页地址、邮件地址、电话号码、居住地址

当然这几种之间也可以随意搭配,比如web|emailweb|phone|mapemail|phone|map,等等。他们是怎么做到可以同时存在,不会彼此影响或者覆盖彼此的设置呢?

为了明白autoLink的工作原理,我们先来看看它的定义方式,我们在sdk\platforms\android-23\data\res\values\路径下找到了Android自带的attrs.xml文件。

搜索autoLink,在attrs.xml文件中第1459~1475行有如下代码:

    <!-- Controls whether links such as urls and email addresses are
         automatically found and converted to clickable links.  The default
         value is "none", disabling this feature. -->
    <attr name="autoLink">
        <!-- Match no patterns (default). -->
        <flag name="none" value="0x00" />
        <!-- Match Web URLs. -->
        <flag name="web" value="0x01" />
        <!-- Match email addresses. -->
        <flag name="email" value="0x02" />
        <!-- Match phone numbers. -->
        <flag name="phone" value="0x04" />
        <!-- Match map addresses. -->
        <flag name="map" value="0x08" />
        <!-- Match all patterns (equivalent to web|email|phone|map). -->
        <flag name="all" value="0x0f" />
    </attr>

上面我们定义阻尼因子时,使用的定义方式如下:

        <attr name="dampingFactor" format="float" />

这种方式定义出来的属性在xml中只能有一个固定的值,而如果要同时支持多个值以表示多种状态,则需要对自定义属性的值进行约束和声明。比如这样:

<attr name="属性名">
        <flag name="属性值a" value="属性值a的标识" />
        <flag name="属性值b" value="属性值b的标识" />
</attr>

这样,就可以在布局文件中用属性值a|属性值b来使属性值a属性值b同时生效了。

顺便提一下,这里是个属性值指定类型为flag,除此之外,可以指定为enum,比如LinearLayoutorientation的值:

    <attr name="orientation">
        <!-- Defines an horizontal widget. -->
        <enum name="horizontal" value="0" />
        <!-- Defines a vertical widget. -->
        <enum name="vertical" value="1" />
    </attr>

flagenum有什么区别呢?结合我们平时的使用方式,答案一目了然:flag声明的属性值可以同时存在,彼此互不干扰;用enum声明的值则相互排斥,只能同时存在一种。

扯远了,回到正题。刚才说到将属性值的类型声明为flag之后,在布局文件中就可以给一个属性同时设置多种属性值了,但是它们怎么做到同时生效而且又互不干扰呢?

这就要复习一下位运算了。

位运算即基于二进制位的计算,包含几种种基本的运算:按位与、按位或、按位异或、按位取反、左移、带符号右移、无符号右移。我们这里主要用到与运算或运算,所以主要讲这两个,想对位运算了解的更多的同学可以百度,或者去看我的这篇文章位运算,安卓源码里的各种Flag

先来看看与运算或运算的计算方法。

与运算

符号:&

描述:相同位的两个数字都为1,则为1;若有一个不为1,则为0。

举例:a=0110,b=1010,则a&b的计算结果如下,

或运算

符号:|

描述:相同位只要一个为1即为1。

举例:a=0110,b=1010,则a|b的计算结果如下,

好了,基本的运算复习到这里,我们再回头看看autoLink的设置方式。

属性值属性值的值属性值对应的二进制
none0x000000
web0x010001
email0x020010
phone0x040100
map0x081000
all0x0f1111

表格中是autoLink属性对应的各种值,假设我们用mAutoLinkFlags来保存TextView的各种值,并且初始状态为0:

假设我们在布局文件中设置autoLink的属性值为web|email,即0001|0010,则根据或运算的计算方式,可以得到mAutoLinkFlags = 0011

那我们在代码中怎么知道mAutoLinkFlags有没有设置web或者email呢?根据与运算的计算原理,我们可以用公式:

mAutoLinkFlags & flag == flag

来判断mAutoLinkFlags中有没有设置flag的值。

比如我们要判断有没有设置web,则用mAutoLinkFlags & web == web来判断,即0011 & 0001 == 0001,很显然,这个等式是成立的。

比如我们要判断有没有设置phone,则用mAutoLinkFlags & phone == phone来判断,即0011 & 0100 == 0100,左边算出来的值是0,左边!=右边,等式不成立,所以mAutoLinkFlags中是没有设置phone属性的。

我们通常用一个int类型的值来存储所有的flags,每种flag在该值中占据1个bit的位置,并用该bit上的值为1或0来表示flag对应的两种状态。

我们知道,int类型在Java中占据4个Byte,每个Byte有8个bit,一共有32个bit。所以理想情况下一个int类型的变量最多可以存放多达32种不同的flags,并且他们可以同时生效互不干扰。

如果不这么做,你可能需要用32个布尔变量来存放所有的属性值。。

当然,各种属性值的取值也不是随便取的,必须保证他们的值转成二进制之后“只能占据一个bit的位置”,所以你看autoLink属性对应的各种值,他们转成二进制之后都只占据一个bit的位置,比如000100100100,而不存在0011这种值存在,如果这样的话就会对其他的属性值的判断产生干扰了。。

回想Android中的很多源码里面,都用这种方式来标志对象的各种属性和状态的,想对位运算了解的更多的同学可以百度,或者去看我的这篇文章位运算,安卓源码里的各种Flag

讲位运算讲了这么多好像很无聊,接下来讲OverScrollLayout代码里实现多方向阻尼的时候要注意的一些地方。

首先,手指移动时,要判断滚动方向是水平方向还是垂直方向,然后分别处理对应方向的阻尼效果。

其次,在一次触摸过程中,只能存在一个方向上的阻尼效果。比如用户设置了4个方向的阻尼效果,当顶部产生了阻尼效果之后如果用户此时手指水平滚动,那水平反向就不能产生阻尼效果,直到下一次用户手指按下时再做判断。

至此,位运算处理多方向阻尼时的逻辑就讲到这里了。

处理滑动冲突,支持常用ViewGroup作为子View时的阻尼效果

滑动冲突是一个老生常谈的问题了,带滑动效果的自定义ViewGroup大多都需要处理滑动冲突的问题,包括我们这个OverScrollLayout

根据任玉刚老师的《Android开发艺术探索》一书中的描述,解决滑动冲突的办法一般有两种固定思路:外部拦截法内部拦截法

外部拦截法

描述:父控件对所有的事件进行过滤拦截,即当父控件需要处理事件时则拦截,反之则不拦截。

伪代码:

//父控件代码
public boolean onInterceptTouchEvent(MotionEvent event){

    boolean intercepted = false;
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
              intercepted = false;
              break;
        case MotionEvent.ACTION_MOVE:
              if(父控件需要处理此次事件){
                    intercepted = true;
              }else{
                    intercepted = false;
              }
              break;
        case MotionEvent.ACTION_UP:
              intercepted = false;
              break;
    }
    reture intercepted;
}

内部拦截法

描述:父控件始终拦截除ACTION_DOWN以外的所有事件,子View则根据需要来禁止父控件的拦截能力,如果子View需要处理事件,则禁止父控件拦截事件;反之则不禁止。

伪代码:

//父控件代码
public boolean onInterceptTouchEvent(MotionEvent event){

    if(event.getAction() == MotionEvent.ACTION_DOWN){
        return false;
    }
    reture true;
}

//子View代码
public boolean dispatchTouchEvent(MotionEvent event){

    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
              getParent().requestDisallowInterceptTouchEvent(true);
              break;
        case MotionEvent.ACTION_MOVE:
              if(父控件需要处理此次事件){
                  getParent().requestDisallowInterceptTouchEvent(false);
              }
              break;
        case MotionEvent.ACTION_UP:
              break;
    }
    reture super.dispatchTouchEvent(event);
}

对于外部拦截法,比较符合事件的分发机制,而且对子View的侵入性比较小,只涉及到父控件的修改。

对于内部拦截法,这种解决方案稍微麻烦,而且需要同时修改父控件和子View的相关代码。

同时由于我们需要对诸多滑动控件支持阻尼效果,不可能一个个去扩展所有滑动控件,这样工作量大,也不符合设计模式。综上所述,我们采用外部拦截法来处理这里的滑动冲突。

来看看OverScrollLayout中的实现:

1、首先,我们定义了一个接口OnDampingCallback,用来判断子View此时是否需要产生阻尼效果

    public interface OnDampingCallback {

        boolean needDamping(MotionEvent newMotionEvent, MotionEvent oldMotionEvent);
    }

2、其次,在onInterceptTouchEvent()方法中,根据子类实现的OnDampingCallback对象来判断是否需要拦截事件,比如如果子View告诉我们此时需要产生阻尼效果,则我们就开始拦截事件;繁殖则不拦截。

    private OnDampingCallback mOnDampingCallback;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercept;
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            intercept = false;
        } else {
            if (null == mOnDampingCallback) {

                mOnDampingCallback = try2GetOnDampingCallback();
            }
            intercept = mOnDampingCallback.needDamping(ev, mLastInterceptMotionEvent);
        }
        mLastInterceptMotionEvent = MotionEvent.obtain(ev);
        return intercept;
    }

    public void setOnDampingCallback(OnDampingCallback callback) {
        mOnDampingCallback = callback;
    }

mOnDampingCallback可以通过外部方法来设置,在OverScrollLayouttry2GetOnDampingCallback()方法中也默认提供了诸多滑动控件OnDampingCallback的默认实现,即根据滑动控件的不同状态来决定是否需要显示阻尼效果。

    private OnDampingCallback try2GetOnDampingCallback() {
        View child = getChildAt(0);
        if (child instanceof ViewPager) {
            return new ViewPagerDampingCallback((ViewPager) child);
        } else if (child instanceof ScrollView) {
            return new ScrollViewDampingCallback((ScrollView) child);
        } else if (child instanceof HorizontalScrollView) {
            return new HorizontalScrollViewDampingCallback((HorizontalScrollView) child);
        } else if (child instanceof RecyclerView) {
            return new RecyclerViewDampingCallback((RecyclerView) child);
        }
        return new SimpleDampingCallback();
    }

这些OnDampingCallback子类的实现基本大同小异,我们只看一个就好,比如ViewPagerDampingCallback

    public static class ViewPagerDampingCallback implements OnDampingCallback {

        private ViewPager mViewPager;

        public ViewPagerDampingCallback(ViewPager viewPager) {

            mViewPager = viewPager;
        }

        @Override
        public boolean needDamping(MotionEvent newMotionEvent, MotionEvent oldMotionEvent) {

            if (null == newMotionEvent || null == oldMotionEvent) {
                return false;
            }
            //左边是否需要处理阻尼效果
            boolean isLeftDamping = isMoving2Left(newMotionEvent, oldMotionEvent) && mViewPager.getCurrentItem() == 0;
            //右边是否需要处理阻尼效果
            boolean isRightDamping = isMoving2Right(newMotionEvent, oldMotionEvent) && mViewPager.getCurrentItem() == mViewPager.getAdapter().getCount() - 1;
            //左边或右边需要处理阻尼效果
            return isLeftDamping || isRightDamping;
        }
    }

needDamping()方法中,如果用户手指当前往右移即ViewPager应当显示左边的内容,而且ViewPager当前先第0个,则表示ViewPager此时需要显示阻尼滚动的效果,所以isLeftDamping = trueneedDamping()的返回值也为true。

所以OverScrollLayout中的onInterceptTouchEvent()也返回true,表示会拦截此次滑动事件,拦截过后MotionEvent就走到了onTouchEvent()中,就回到我们最开始讨论的阻尼效果产生的地方。

这里就不再赘述了。

解决Button等作为子View时无法响应点击事件的bug

代码写到这里,大部分问题都处理完了,还有一个问题就是处理点击事件的问题。

我们知道,手指在手机屏幕上点击的时候,产生的MotionEvent一定会产生若干的ACTION_MOVE事件,而如果这些事件被误拦截了,子View是永远也无法响应点击事件的。

所以我们在OverScrollLayout中的onInterceptTouchEvent()判断是否拦截事件时,除了获取OnDampingCallback.needDamping()的值,还要判断当前事件是点击还是滑动。

修改后的方法如下:

    private static final int HOVER_TAP_SLOP = 20;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        boolean intercept;
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            intercept = false;
        } else {
            if (null == mOnDampingCallback) {

                mOnDampingCallback = try2GetOnDampingCallback();
            }
            intercept = isMoving(ev, mLastInterceptMotionEvent) && mOnDampingCallback.needDamping(ev, mLastInterceptMotionEvent);
        }
        mLastInterceptMotionEvent = MotionEvent.obtain(ev);
        return intercept;
    }

    private boolean isMoving(MotionEvent newMotionEvent, MotionEvent oldMotionEvent) {

        int dx = (int) (newMotionEvent.getRawX() - oldMotionEvent.getRawX());
        int dy = (int) (newMotionEvent.getRawY() - oldMotionEvent.getRawY());
        return Math.abs(dx) > HOVER_TAP_SLOP || Math.abs(dy) > HOVER_TAP_SLOP;
    }

其中HOVER_TAP_SLOP是从ViewConfiguration中直接复制出来的值。

至此,OverScrollLayout的所有逻辑就分析完啦。

项目地址

各位请自取。

https://github.com/aishang5wpj/DampingScrolling

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值