Android滑动冲突解决方法

现阶段你以为过不去的事,天大的事,当过去以后就会发现那只是屁大点事。实际上,也真的只是屁大点事儿。所有发生的都是应当发生的,无论是对是错。你需要为自己所做的任何决定和选择进行承担和负责。你需要做的是不断充实自己,做自己喜欢做的事情,让自己忙碌起来。你要感谢所有的人,所有人的到来,都是为了增加你感情的丰富程度,教会你如何处世如何为人,是他们让你波澜不惊的生活起了波澜,给你的生活增添了色彩,你会感激你所遇到的每一个人。你会发现你自身存在的问题,然后慢慢去修正。人生太丰富多彩了,你必须去接触新鲜的事物,在老的时候才可以回忆。任何一种感情最多只能占自己的百分之十,可以调整所占的幅度,但是都不能当成人生的全部。你要提高其它事情在你生命中所占的比重。



我以前觉得咖啡很苦,而且对身体不好,坚持了很久没有喝。但是有人再次请我喝的时候,我突然发现也没有那么苦。原来感觉会随着时间慢慢地改变。咖啡对身体有好也有坏,原来认知也在慢慢地改变。

我以前特别喜欢吃冰的东西,我所有的东西都是冰的。包括冬天喝的水都是放在冰箱的,最多是放到冰箱外让它变成常温。我冬天还经常吃雪糕,而且一吃就是好几块儿。但是,现在,我也开始尝试喝暖暖的东西,因为对身体好。原来,以前坚持的现在不一定会坚持。


但是,我还是点了一杯红茶,而且是热饮。


我不知道星巴克的咖啡怎么样,我的味觉很迟钝,我喝不出来,对我来说都一样。但是我觉得星巴克的学习氛围很好,也很让人放松。我今天感觉阳光格外地灿烂,天气格外地好,过得很充实。


我突然很喜欢很喜欢现在的自己。我不知道我过去是怎么了,是一个什么样的人,做了什么事。

我只知道我一直在迷茫,一直在痛苦,一直在沉默,一直在排斥,一直在厌恶着周围的一切,一直在自我封闭,一直在找寻自己,一直在问自己是谁?一直在问自己想做什么?到底要做什么?想成为一个什么样的人?到底打算去哪儿?到底想去哪儿?到底该跟别人说什么?到底如何说?到底应该如何与别人相处?


我不知道我伤害了多少人?我不知道我影响了多少人?我不知道我使多少人感到厌烦?我不知道我让多少人感到为难?我不知道多少人觉得我是个麻烦?我不知道我到底让多少人感到不知所措?



但是,我现在很感谢那些人,感谢那些经过我生命的人,感谢自己经历的一切。

我也分不清谁对谁错,反正只要存在问题,那么所有的人都有错。但是,也许我自身的问题更大吧。也许,全部是我的问题。

感谢别人所做的一切努力,不管是对的还是错的,好的还是坏的。

感谢自己所做的努力,感谢自己的挣扎,感谢自己的控制,感谢自己的忍耐,感谢我所做的一切,不管是对的还是错的。

对的是经验,错的是教训。

但是,无论如何,我都感谢,也全部接受。

感谢所有的一切,让我成为一个更好的人!

我现在知道了,我想做一个开心、快乐的人!











动冲突

首先讲解一下什么是滑动冲突。当你需要在一个ScrollView中嵌套使用ListView或者RecyclerView的时候你会发现只有ScrollView能够滑动,而ListView或RecyclerView不能滑动,这个就违背了我们写这段代码的意愿。我们想要的结果是当我们滑动ListView的时候ListView滑动,滑动ListView以外的地方的时候ScrollView滑动。这时候滑动冲突就产生了,我们需要想办法解决这个冲突。

你可以在这里看到这个引文的demo:https://github.com/onlynight/SlidingConfict

View Touch事件分发

首先我们了解下Android的控件组织结构。View是显示组件的基类,ViewGroup继承自View是布局的基类。ViewGroup中可包含View和ViewGroup,这样就形成了View树。View的Touch事件总是从View根节点开始向下传递的,根据点击的位置判断该传递给哪个子View,直到子节点再没有子节点这时候,如果这个事件被该View消耗那么事件的传递就此结束,如果该View没有使用这个事件那么这个事件会依次向上传递直到有View消耗了这个事件,如果没有View消耗这个事件,那么该事件就会被传递给Activity处理。以上就是Vieww Touch事件传递的过程。

我们来看View的dispatchEvent方法:

//View.java
/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    return result;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

从这段代码我们可以看出OnTouchListener的优先级高于onTouchEvent。 
下面我们再来看看ViewGroup的dispatchTouchEvent方法:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }

    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;

    //如果没有拦截再分发下去处理
    if (!canceled && !intercepted) {
        ...
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...
        }
        ...
    }

    return handled;
}

/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

可以看到ViewGroup在处理事件前有一个touch事件是否被拦截onInterceptTouchEvent的判断,如果被拦截则不再向下一级分发;如果没有拦截则向下分发,处理方式会根据ViewGroup中是否包含子元素来判断,如果包含子元素则将事件交由子元素处理touch事件handled = child.dispatchTouchEvent(event);,如果不包含子元素则由自身处理handled = child.dispatchTouchEvent(event);处理流程和View相同。

View Touch事件分发

实线箭头为touch事件正向传递,虚线为向上传递touch事件。

通过上面的分发的逻辑我们可以知道父控件有能力把事件不传递给子View,从而不让子控件接收Touch事件,那么子控件有没有能力让父控件失去响应Touch事件的能力呢,下面我们来看看具体的源码,看源码的顺序是由下而上的,这回我们反其道而行,我们知道事件的入口然后依次向下找。

Activity分发事件到ViewGroup

根据上面的图我们知道View的touch事件是由Activity传递过来的,那么我们先看看Activity有没有类似的方法,正如我们所料,Activity的dispatchTouchEvent函数如下:

/**
 * Called to process touch screen events.  You can override this to
 * intercept all touch screen events before they are dispatched to the
 * window.  Be sure to call this implementation for touch screen events
 * that should be handled normally.
 *
 * @param ev The touch screen event.
 *
 * @return boolean Return true if this event was consumed.
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

显而易见我们要看的是getWindow().superDispatchTouchEvent(ev),我们深入进去看到Window类中的这个方法:

//Window.java
/**
 * Used by custom windows, such as Dialog, to pass the touch screen event
 * further down the view hierarchy. Application developers should
 * not need to implement or call this.
 *
 */
public abstract boolean superDispatchTouchEvent(MotionEvent event);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Window类是个抽象类,它的唯一实现类是PhoneWindow,PhoneWindow类中的实现如下:

//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

mDecor是DecorView,我们看看这个DectorView是从哪里来的:

//PhoneWindow.java
private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();
        ...
    }

    ...

    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
    }

    ...
}

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

protected ViewGroup generateLayout(DecorView decor) {
    ...

    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }

    ...

    return contentParent;
}

//Activity.java
/**
 * Set the activity content from a layout resource.  The resource will be
 * inflated, adding all top-level views to the activity.
 *
 * @param layoutResID Resource ID to be inflated.
 *
 * @see #setContentView(android.view.View)
 * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
 */
public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

这里我们就看出了mDecorView中包含了mContentParent,并且DecorView继承自FramLayout,所以touch事件的分发也符合View的事件分发,mDecorView之后会添加到Activity关联的Window上(这里我们不再深究),下面我们来看DecorView的superDispatchTouchEvent:

//PhoneWindow.java#DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
 
 
  • 1
  • 2
  • 3
  • 4

至此,activity的dispatchTouchEvent方法就最终分发到了我们的布局上,最后总结一下:

Activity#dispatchTouchEvent -> PhoneWindow#superDispatchTouchEvent ->
DecorView#superDispatchTouchEvent -> ViewGroup#dispatchTouchEvent -> View#dispatchTouchEvent
 
 
  • 1
  • 2

解决滑动冲突的原理

看了上面的源码解析,我们知道Viewtouch事件分发过程中重要的三个函数:

  • dispatchTouchEvent 负责touch事件的分发
  • onInterceptTouchEvent 负责拦截touch事件
  • onTouchEvent 最终处理touch事件

其中dispatchTouchEvent和onInterceptTouchEvent可以控制touch事件流不传递给子控件,这两个方法中可以控制事件流的向下分发,那么是不是有方法控制事件流向上分发呢?我们找到ViewGroup中有这样一个函数:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...

    boolean handled = false;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }

    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)
            || actionMasked == MotionEvent.ACTION_CANCEL;

    //如果没有拦截再分发下去处理
    if (!canceled && !intercepted) {
        ...
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            ...
        }
        ...
    }

    return handled;
}

/**
 * {@inheritDoc}
 */
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        // We're already in this state, assume our ancestors are too
        return;
    }

    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }

    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

实际上requestDisallowInterceptTouchEvent是修改了disallowIntercept的状态,再结合ViewGroup的dispatchTouchEvent方法查看,我们就明白这个方法的最终意义。ViewGroup的子元素可以通过调用这个方法禁止ViewGroup拦截touch事件。到这里我们就找到了自下而上的touch事件的拦截方法。

滑动冲突两种解决办法

1. 外部拦截法

通过上面的原理分析我们知道我们可以在dispatchTouchEvent的时候不分发事件或者onInterceptTouchEvent时候拦截事件,实际上onInterceptTouchEvent方法是一个空方法,是android专门提供给我们处理touch事件拦截的方法,所以这里我们在onInterceptTouchEvent方法中拦截touch事件。

具体做法就是当你不想把事件传递给子控件的时候在onInterceptTouchEvent方法中返回true即可拦截事件,这时候子控件将不会再接收到这一次的touch事件流(所谓touch事件流是以ACTION_DOWN开始,中间包含若干个ACTION_MOVE,以ACTION_UP结束的一连串事件)。伪代码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if ( condition ) {
        return true;
    }
    return false;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里的condition将会再下一章节中具体讲解。

2. 内部拦截法

首先,我们让父控件拦截除了ACTION_DOWN以外的所有事件,如果连ACTION_DOWN都拦截那么子控件将无法收到任何touch事件:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后,在控件的内部分发事件的时候请求需要的事件(实际上就是禁止父控件拦截事件):

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            //通知父容器不要拦截事件
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:

            if ( <condition> ){
                //通知父容器拦截此事件
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            parent.requestDisallowInterceptTouchEvent(false);
            break;
        default:
            break;
    }

    return super.dispatchTouchEvent(ev);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

这样,就可以解决touch事件的冲突问题,从控件本身解决。内部拦截法使用起来稍显复杂,需要修改两个控件,一般情况下我们都通过外部拦截法解决滑动冲突,如果有特殊情况需要使用内部拦截法才会使用内部拦截法。

事件拦截Condition

试想以下情况:

ScrollView和MapView冲突

MapView的功能是内部可以任意滑动(包括上下,左右以及任意方向滑动),ScrollView需要上下滑动。这时候我们在MapView内部上下滑动时会出现什么结果?我们期望的结果是MapView内部滑动,但是我们看到的实际情况却是ScrollView在上下滑动,滑动冲突就产生了,解决这个滑动冲突的方法很简单,直接上代码:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (isMapViewTouched(ev)) {
            return false;
        } else {
            return super.onInterceptTouchEvent(ev);
        }
    }

private boolean isMapViewTouched(MotionEvent ev) {
    if (getChildCount() == 1) {
        float touchX = ev.getX();
        float touchY = ev.getY() + getScrollY();

        LinearLayout baseLinear = (LinearLayout) getChildAt(0);
        for (int i = 0; i < baseLinear.getChildCount(); i++) {
            View child = baseLinear.getChildAt(i);

            // add map view you want ignore
            if (isMapView(child)) {
                if (touchX < child.getRight() && touchX > child.getLeft() &&
                        touchY < child.getBottom() && touchY > child.getTop()) {
                    return true;
                }
            }
        }
    }
    return false;
}

private boolean isMapView(View child) {
    return child instanceof MapView ||
            child instanceof com.google.android.gms.maps.MapView;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

isMapViewTouched这个函数就是我们这个情况下的condition,具体的含义就是当前点击的是MapView那么所有的touch事件都不允许拦截,交由MapView处理。

这是一种很简单的滑动冲突情况,没有判断滑动的方向以及速度等因素,一般的我们通过判断滑动的方向作为判断条件,下面我们再来看一种情况:

ViewPager和ListView冲突

ViewPager需要左右滑动,ListView需要上下滑动,当我们斜向滑动时就出现了滑动冲突。实际上ViewPage已经解决了这种滑动冲突,这里我们假定它没有解决这种滑动冲突,我们自己来解决这个滑动冲突。当我们斜向滑动时候示意图如下:

斜向滑动示意图

当我们从start滑动到end时,x方向的坐标变化我们称之为dx,y方向的坐标变化我们称之为dy。

  1. 当dx > dy时我们视其为水平滑动
  2. 当dx < dy时我们视其为竖直滑动

通过外部拦截法的代码如下:

//ViewPager.java
int lastX = -1;
int lastY = -1;
boolean isHorizontal = false;
boolean hasDirection = false;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
    int currentX = ev.getX();
    int currentY = ev.getY();

    switch( action ){
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            int dx = Math.abs(currentX - lastX);
            int dy = Math.abs(currentY - lastY);

            // 这里为了保证用户体验,当我们第一次滑动的方向即为这次touch事件流的滑动方向
            if ( hasDirection ) {
                return isHorizontal;
            } else {
                if ( dx > dy ) { // 水平滑动
                    isHorizontal = true;
                    return true;
                } else { // 竖直滑动
                    isHorizontal = false;
                    return false;
                }
            }

            hasDirection = true;
            lastX = currentX;
            lastY = currentY;
            break;
        case MotionEvent.ACTION_UP:
            hasDirection = false;
            break;
    }

    return super.onInterceptTouchEvent(ev);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

滑动冲突解决拓展

滑动冲突的解决方法我们已经知道了,以后无论遇到多么复杂的情况解决滑动冲突的原则都是不变的,根据你的业务需求进行不同的事件拦截即可。

Touch事件的来源深入篇

如果你想知道Activity中的Touch事件是从哪来的,你可以查看任玉刚大神的这篇文章:http://blog.csdn.net/singwhatiwanna/article/details/50775201







三、

android多种滑动冲突的解决方案


一、前言

Android 中解决滑动的方案有2种:外部拦截法 和内部拦截法。

滑动冲突也存在2种场景: 横竖滑动冲突、同向滑动冲突。

所以我就写了4个例子来学习如何解决滑动冲突的,这四个例子分别为: 外部拦截法解决横竖冲突、外部拦截法解决同向冲突、内部拦截法解决横竖冲突、内部拦截法解决同向冲突。

先上效果图:

二、实战

1、外部拦截法,解决横竖冲突

思路是,重写父控件的onInterceptTouchEvent方法,然后根据具体的需求,来决定父控件是否拦截事件。如果拦截返回返回true,不拦截返回false。如果父控件拦截了事件,则在父控件的onTouchEvent进行相应的事件处理。

我的这个例子,是一个横向滑动的ViewGroup里面包含了3个竖向滑动的ListView。下面我附上代码,HorizontalEx.Java:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**
  * Created by blueberry on 2016/6/20.
  *
  * 解决交错的滑动冲突
  *
  * 外部拦截法
  */
public class HorizontalEx extends ViewGroup {
 
  private static final String TAG = "HorizontalEx" ;
 
  private boolean isFirstTouch = true ;
  private int childIndex;
  private int childCount;
  private int lastXIntercept, lastYIntercept, lastX, lastY;
 
  private Scroller mScroller;
  private VelocityTracker mVelocityTracker;
 
  public HorizontalEx(Context context) {
   super (context);
   init();
  }
 
  public HorizontalEx(Context context, AttributeSet attrs) {
   super (context, attrs);
   init();
  }
 
  public HorizontalEx(Context context, AttributeSet attrs, int defStyleAttr) {
   super (context, attrs, defStyleAttr);
   init();
  }
 
  private void init() {
   mScroller = new Scroller(getContext());
   mVelocityTracker = VelocityTracker.obtain();
  }
 
  @Override
  protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
   int width = MeasureSpec.getSize(widthMeasureSpec);
   int height = MeasureSpec.getSize(heightMeasureSpec);
   int widthMode = MeasureSpec.getMode(widthMeasureSpec);
   int heightMode = MeasureSpec.getMode(heightMeasureSpec);
 
   childCount = getChildCount();
   measureChildren(widthMeasureSpec, heightMeasureSpec);
 
   if (childCount == 0 ) {
    setMeasuredDimension( 0 , 0 );
   } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
    width = childCount * getChildAt( 0 ).getMeasuredWidth();
    height = getChildAt( 0 ).getMeasuredHeight();
    setMeasuredDimension(width, height);
   } else if (widthMode == MeasureSpec.AT_MOST) {
    width = childCount * getChildAt( 0 ).getMeasuredWidth();
    setMeasuredDimension(width, height);
   } else {
    height = getChildAt( 0 ).getMeasuredHeight();
    setMeasuredDimension(width, height);
   }
  }
 
  @Override
  protected void onLayout( boolean changed, int l, int t, int r, int b) {
   int left = 0 ;
   for ( int i = 0 ; i < getChildCount(); i++) {
    final View child = getChildAt(i);
    child.layout(left + l, t, r + left, b);
    left += child.getMeasuredWidth();
   }
  }
 
  /**
   * 拦截事件
   * @param ev
   * @return
   */
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
   boolean intercepted = false ;
   int x = ( int ) ev.getX();
   int y = ( int ) ev.getY();
 
   switch (ev.getAction()) {
    /*如果拦截了Down事件,则子类不会拿到这个事件序列*/
    case MotionEvent.ACTION_DOWN:
     lastXIntercept = x;
     lastYIntercept = y;
     intercepted = false;
     if (!mScroller.isFinished()) {
      mScroller.abortAnimation();
      intercepted = true;
     }
     break;
    case MotionEvent.ACTION_MOVE:
     final int deltaX = x - lastXIntercept;
     final int deltaY = y - lastYIntercept;
     /*根据条件判断是否拦截该事件*/
     if (Math.abs(deltaX) > Math.abs(deltaY)) {
      intercepted = true;
     } else {
      intercepted = false;
     }
     break;
    case MotionEvent.ACTION_UP:
     intercepted = false;
     break;
 
   }
   lastXIntercept = x;
   lastYIntercept = y;
   return intercepted;
  }
 
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
   int x = (int) event.getX();
   int y = (int) event.getY();
   mVelocityTracker.addMovement(event);
   ViewConfiguration configuration = ViewConfiguration.get(getContext());
   switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
     if (!mScroller.isFinished()) {
      mScroller.abortAnimation();
     }
     break;
    case MotionEvent.ACTION_MOVE:
     /*因为这里父控件拿不到Down事件,所以使用一个布尔值,
      当事件第一次来到父控件时,对lastX,lastY赋值*/
     if (isFirstTouch) {
      lastX = x;
      lastY = y;
      isFirstTouch = false ;
     }
     final int deltaX = x - lastX;
     scrollBy(-deltaX, 0 );
     break ;
    case MotionEvent.ACTION_UP:
     int scrollX = getScrollX();
     final int childWidth = getChildAt( 0 ).getWidth();
     mVelocityTracker.computeCurrentVelocity( 1000 , configuration.getScaledMaximumFlingVelocity());
     float xVelocity = mVelocityTracker.getXVelocity();
     if (Math.abs(xVelocity) > configuration.getScaledMinimumFlingVelocity()) {
      childIndex = xVelocity < 0 ? childIndex + 1 : childIndex - 1 ;
     } else {
      childIndex = (scrollX + childWidth / 2 ) / childWidth;
     }
     childIndex = Math.min(getChildCount() - 1 , Math.max(childIndex, 0 ));
     smoothScrollBy(childIndex * childWidth - scrollX, 0 );
     mVelocityTracker.clear();
     isFirstTouch = true ;
     break ;
   }
 
   lastX = x;
   lastY = y;
   return true ;
  }
 
  void smoothScrollBy( int dx, int dy) {
   mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500 );
   invalidate();
  }
 
  @Override
  public void computeScroll() {
   if (mScroller.computeScrollOffset()) {
    scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    invalidate();
   }
  }
 
  @Override
  protected void onDetachedFromWindow() {
   super .onDetachedFromWindow();
   mVelocityTracker.recycle();
  }
}

调用代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
  public void showOutHVData(List<String> data1, List<String> data2, List<String> data3) {
   ListView listView1 = new ListView(getContext());
   ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, data1);
   listView1.setAdapter(adapter1);
 
   ListView listView2 = new ListView(getContext());
   ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, data2);
   listView2.setAdapter(adapter2);
 
   ListView listView3 = new ListView(getContext());
   ArrayAdapter<String> adapter3 = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, data3);
   listView3.setAdapter(adapter3);
 
   ViewGroup.LayoutParams params
     = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
     ViewGroup.LayoutParams.MATCH_PARENT);
 
   mHorizontalEx.addView(listView1, params);
   mHorizontalEx.addView(listView2, params);
   mHorizontalEx.addView(listView3, params);
  }

其实外部拦截的主要思想都在于对onInterceptTouchEvent的重写。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
   boolean intercepted = false ;
   int x = ( int ) ev.getX();
   int y = ( int ) ev.getY();
 
   switch (ev.getAction()) {
    /*如果拦截了Down事件,则子类不会拿到这个事件序列*/
    case MotionEvent.ACTION_DOWN:
     lastXIntercept = x;
     lastYIntercept = y;
     intercepted = false;
     if (!mScroller.isFinished()) {
      mScroller.abortAnimation();
      intercepted = true;
     }
     break;
    case MotionEvent.ACTION_MOVE:
     final int deltaX = x - lastXIntercept;
     final int deltaY = y - lastYIntercept;
     /*根据条件判断是否拦截该事件*/
     if (Math.abs(deltaX) > Math.abs(deltaY)) {
      intercepted = true ;
     } else {
      intercepted = false ;
     }
     break ;
    case MotionEvent.ACTION_UP:
     intercepted = false ;
     break ;
 
   }
   lastXIntercept = x;
   lastYIntercept = y;
   return intercepted;
  }

这几乎是一个实现外部拦截事件的模板,这里一定不要在ACTION_DOWN 中返回 true,否则会让子VIew没有机会得到事件,因为如果在ACTION_DOWN的时候返回了 true,同一个事件序列ViewGroup的disPatchTouchEvent就不会在调用onInterceptTouchEvent方法了。

还有就是 在ACTION_UP中返回false,因为如果父控件拦截了ACTION_UP,那么子View将得不到UP事件,那么将会影响子View的 Onclick方法等。但这对父控件是没有影响的,因为如果是父控件子ACITON_MOVE中 就拦截了事件,他们UP事件必定也会交给它处理,因为有那么一条定律叫做:父控件一但拦截了事件,那么同一个事件序列的所有事件都将交给他处理。这条结论在我的上一篇文章中已经分析过。

最后就是在 ACTION_MOVE中根据需求决定是否拦截。

2、内部拦截法,解决横竖冲突

内部拦截主要依赖于父控件的 requestDisallowInterceptTouchEvent方法,关于这个方法我的上篇文章其实已经分析过。他设置父控件的一个标志(FLAG_DISALLOW_INTERCEPT)

这个标志可以决定父控件是否拦截事件,如果设置了这个标志则不拦截,如果没设这个标志,它就会调用父控件的onInterceptTouchEvent()来询问父控件是否拦截。但这个标志对Down事件无效。

可以参考一下源码:   

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Handle an initial down.
   if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    //清楚标志
    resetTouchState();
   }
 
   // Check for interception.
   final boolean intercepted;
   if (actionMasked == MotionEvent.ACTION_DOWN
     || mFirstTouchTarget != null ) {
     //标志
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0 ;
    if (!disallowIntercept) {
     intercepted = onInterceptTouchEvent(ev);
     ev.setAction(action); // restore action in case it was changed
    } else {
     intercepted = false ;
    }
   } else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true ;
   }

那么我们如果想使用 内部拦截法拦截事件。

第一步:

a、我们要重写父控件的onInterceptTouchEvent,在ACTION_DOWN的时候返回false,负责的话子View调用requestDisallowInterceptTouchEvent也将无能为力。

b、还有就是其他事件的话都返回true,这样就把能否拦截事件的权利交给了子View。

第二步:

在子View的dispatchTouchEvent中 来决定是否让父控件拦截事件。

a. 先要在MotionEvent.ACTION_DOWN:的时候使用mHorizontalEx2.requestDisallowInterceptTouchEvent(true);,负责的话,下一个事件到来时,就交给父控件了。

b. 然后在MotionEvent.ACTION_MOVE: 根据业务逻辑决定是否调用mHorizontalEx2.requestDisallowInterceptTouchEvent(false);来决定父控件是否拦截事件。

上代码HorizontalEx2.java:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
  * Created by blueberry on 2016/6/20.
  * 内部拦截
  * 和 ListViewEx配合使用
  */
public class HorizontalEx2 extends ViewGroup {
 
  private int lastX, lastY;
  private int childIndex;
  private Scroller mScroller;
  private VelocityTracker mVelocityTracker;
 
  public HorizontalEx2(Context context) {
   super (context);
   init();
  }
 
  public HorizontalEx2(Context context, AttributeSet attrs) {
   super (context, attrs);
   init();
  }
 
  public HorizontalEx2(Context context, AttributeSet attrs, int defStyleAttr) {
   super (context, attrs, defStyleAttr);
   init();
  }
 
  private void init() {
   mScroller = new Scroller(getContext());
   mVelocityTracker = VelocityTracker.obtain();
  }
 
  @Override
  protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
   int width = MeasureSpec.getSize(widthMeasureSpec);
   int widthMode = MeasureSpec.getMode(widthMeasureSpec);
   int height = MeasureSpec.getSize(heightMeasureSpec);
   int heightMode = MeasureSpec.getMode(heightMeasureSpec);
 
   int childCount = getChildCount();
   measureChildren(widthMeasureSpec, heightMeasureSpec);
 
   if (childCount == 0 ) {
    setMeasuredDimension( 0 , 0 );
   } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
    height = getChildAt( 0 ).getMeasuredHeight();
    width = childCount * getChildAt( 0 ).getMeasuredWidth();
    setMeasuredDimension(width, height);
   } else if (widthMode == MeasureSpec.AT_MOST) {
    width = childCount * getChildAt( 0 ).getMeasuredWidth();
    setMeasuredDimension(width, height);
   } else {
    height = getChildAt( 0 ).getMeasuredHeight();
    setMeasuredDimension(width, height);
   }
  }
 
  @Override
  protected void onLayout( boolean changed, int l, int t, int r, int b) {
   int leftOffset = 0 ;
   for ( int i = 0 ; i < getChildCount(); i++) {
    View child = getChildAt(i);
    child.layout(l + leftOffset, t, r + leftOffset, b);
    leftOffset += child.getMeasuredWidth();
   }
  }
 
  /**
   * 不拦截Down事件,其他一律拦截
   * @param ev
   * @return
   */
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
   if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    if (!mScroller.isFinished()) {
     mScroller.abortAnimation();
     return true ;
    }
    return false ;
   } else {
    return true ;
   }
  }
 
  private boolean isFirstTouch = true ;
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
   int x = ( int ) event.getX();
   int y = ( int ) event.getY();
   mVelocityTracker.addMovement(event);
   ViewConfiguration configuration = ViewConfiguration.get(getContext());
   switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
     if (!mScroller.isFinished()) {
      mScroller.abortAnimation();
     }
     break ;
    case MotionEvent.ACTION_MOVE:
     if (isFirstTouch) {
      isFirstTouch = false ;
      lastY = y;
      lastX = x;
     }
     final int deltaX = x - lastX;
     scrollBy(-deltaX, 0 );
     break ;
    case MotionEvent.ACTION_UP:
     isFirstTouch = true ;
     int scrollX = getScrollX();
     mVelocityTracker.computeCurrentVelocity( 1000 , configuration.getScaledMaximumFlingVelocity());
     float mVelocityX = mVelocityTracker.getXVelocity();
     if (Math.abs(mVelocityX) > configuration.getScaledMinimumFlingVelocity()) {
      childIndex = mVelocityX < 0 ? childIndex + 1 : childIndex - 1 ;
     } else {
      childIndex = (scrollX + getChildAt( 0 ).getWidth() / 2 ) / getChildAt( 0 ).getWidth();
     }
     childIndex = Math.min(getChildCount() - 1 , Math.max( 0 , childIndex));
     smoothScrollBy(childIndex*getChildAt( 0 ).getWidth()-scrollX, 0 );
     mVelocityTracker.clear();
     break ;
   }
 
   lastX = x;
   lastY = y;
   return true ;
  }
 
  private void smoothScrollBy( int dx, int dy) {
   mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500 );
   invalidate();
  }
 
  @Override
  public void computeScroll() {
   if (mScroller.computeScrollOffset()){
    scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
    postInvalidate();
   }
  }
 
  @Override
  protected void onDetachedFromWindow() {
   super .onDetachedFromWindow();
   mVelocityTracker.recycle();
  }
}

ListViewEx.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
  * 内部拦截事件
  */
public class ListViewEx extends ListView {
 
  private int lastXIntercepted, lastYIntercepted;
 
  private HorizontalEx2 mHorizontalEx2;
 
  public ListViewEx(Context context) {
   super (context);
  }
 
  public ListViewEx(Context context, AttributeSet attrs) {
   super (context, attrs);
  }
 
  public ListViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
   super (context, attrs, defStyleAttr);
  }
 
  public HorizontalEx2 getmHorizontalEx2() {
   return mHorizontalEx2;
  }
 
  public void setmHorizontalEx2(HorizontalEx2 mHorizontalEx2) {
   this .mHorizontalEx2 = mHorizontalEx2;
  }
 
  /**
   * 使用 outter.requestDisallowInterceptTouchEvent();
   * 来决定父控件是否对事件进行拦截
   * @param ev
   * @return
   */
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
   int x = ( int ) ev.getX();
   int y = ( int ) ev.getY();
   switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
     mHorizontalEx2.requestDisallowInterceptTouchEvent( true );
     break ;
    case MotionEvent.ACTION_MOVE:
     final int deltaX = x-lastYIntercepted;
     final int deltaY = y-lastYIntercepted;
     if (Math.abs(deltaX)>Math.abs(deltaY)){
      mHorizontalEx2.requestDisallowInterceptTouchEvent( false );
     }
     break ;
    case MotionEvent.ACTION_UP:
     break ;
   }
   lastXIntercepted = x;
   lastYIntercepted = y;
   return super .dispatchTouchEvent(ev);
  }
}

调用代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Override
public void showInnerHVData(List<String> data1, List<String> data2, List<String> data3) {
 
  ListViewEx listView1 = new ListViewEx(getContext());
  ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, data1);
  listView1.setAdapter(adapter1);
  listView1.setmHorizontalEx2(mHorizontalEx2);
 
  ListViewEx listView2 = new ListViewEx(getContext());
  ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, data2);
  listView2.setAdapter(adapter2);
  listView2.setmHorizontalEx2(mHorizontalEx2);
 
  ListViewEx listView3 = new ListViewEx(getContext());
  ArrayAdapter<String> adapter3 = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, data3);
  listView3.setAdapter(adapter3);
  listView3.setmHorizontalEx2(mHorizontalEx2);
 
  ViewGroup.LayoutParams params
    = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    ViewGroup.LayoutParams.MATCH_PARENT);
 
  mHorizontalEx2.addView(listView1, params);
  mHorizontalEx2.addView(listView2, params);
  mHorizontalEx2.addView(listView3, params);
}

至此,2种拦截方法已经学习完毕,下面我们来学习如何解决同向滑动冲突。

其实和上面的2个例子思路是一样的,只是用来判断是否拦截的那块逻辑不同而已。

下面的例子,是一个下拉刷新的一个控件。

3、外部拦截 解决同向滑动冲突

RefreshLayoutBase.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
package com.blueberry.sample.widget.refresh;
 
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ProgressBar;
import android.widget.Scroller;
import android.widget.TextView;
 
import com.blueberry.sample.R;
 
/**
 
  *外部拦截(同向)
  *
  */
public abstract class RefreshLayoutBase<T extends View> extends ViewGroup {
 
  private static final String TAG = "RefreshLayoutBase" ;
 
  public static final int STATUS_LOADING = 1 ;
  public static final int STATUS_RELEASE_TO_REFRESH = 2 ;
  public static final int STATUS_PULL_TO_REFRESH = 3 ;
  public static final int STATUS_IDLE = 4 ;
  public static final int STATUS_LOAD_MORE = 5 ;
  private static int SCROLL_DURATION = 500 ;
 
  protected ViewGroup mHeadView;
  protected ViewGroup mFootView;
  private T contentView;
  private ProgressBar headProgressBar;
  private TextView headTv;
  private ProgressBar footProgressBar;
  private TextView footTv;
 
  private boolean isFistTouch = true ;
 
  protected int currentStatus = STATUS_IDLE;
  private int mScreenWidth;
  private int mScreenHeight;
  private int mLastXIntercepted;
  private int mLastYIntercepted;
  private int mLastX;
  private int mLastY;
  protected int mInitScrollY = 0 ;
  private int mTouchSlop;
 
  protected Scroller mScoller;
 
  private OnRefreshListener mOnRefreshListener;
 
  public RefreshLayoutBase(Context context) {
   this (context, null );
  }
 
  public RefreshLayoutBase(Context context, AttributeSet attrs) {
   this (context, attrs, 0 );
  }
 
  public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
   super (context, attrs, defStyleAttr);
   getScreenSize();
   initView();
   mScoller = new Scroller(context);
   mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
   setPadding( 0 , 0 , 0 , 0 );
  }
 
  public void setContentView(T view) {
   addView(view, 1 );
  }
 
  public OnRefreshListener getOnRefreshListener() {
   return mOnRefreshListener;
  }
 
  public void setOnRefreshListener(OnRefreshListener mOnRefreshListener) {
   this .mOnRefreshListener = mOnRefreshListener;
  }
 
  private void initView() {
   setupHeadView();
   setupFootView();
  }
 
  private void getScreenSize() {
   WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
   DisplayMetrics metrics = new DisplayMetrics();
   wm.getDefaultDisplay().getMetrics(metrics);
   mScreenWidth = metrics.widthPixels;
   mScreenHeight = metrics.heightPixels;
  }
 
  private int dp2px( int dp) {
   WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
   DisplayMetrics metrics = new DisplayMetrics();
   wm.getDefaultDisplay().getMetrics(metrics);
   return ( int ) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
  }
 
  /**
   * 设置头布局
   */
  private void setupHeadView() {
   mHeadView = (ViewGroup) View.inflate(getContext(), R.layout.fresh_head_view, null );
   mHeadView.setBackgroundColor(Color.RED);
   headProgressBar = (ProgressBar) mHeadView.findViewById(R.id.head_progressbar);
   headTv = (TextView) mHeadView.findViewById(R.id.head_tv);
   /*设置 实际高度为 1/4 ,但内容区域只有 100dp*/
   ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, mScreenHeight / 4);
   mHeadView.setLayoutParams(layoutParams);
   mHeadView.setPadding(0, mScreenHeight / 4 - dp2px(100), 0, 0);
   addView(mHeadView);
  }
 
  /**
   * 设置尾布局
   */
  private void setupFootView() {
   mFootView = (ViewGroup) View.inflate(getContext(), R.layout.fresh_foot_view, null);
   mFootView.setBackgroundColor(Color.BLUE);
   footProgressBar = (ProgressBar) mFootView.findViewById(R.id.fresh_foot_progressbar);
   footTv = (TextView) mFootView.findViewById(R.id.fresh_foot_tv);
   addView(mFootView);
  }
 
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   int widthSize = MeasureSpec.getSize(widthMeasureSpec);
   int widthMode = MeasureSpec.getMode(widthMeasureSpec);
   int height = MeasureSpec.getSize(heightMeasureSpec);
   int heightMode = MeasureSpec.getMode(heightMeasureSpec);
 
   int finalHeight = 0;
   for (int i = 0; i < getChildCount(); i++) {
    View child = getChildAt(i);
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
    finalHeight += child.getMeasuredHeight();
   }
 
   if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
    widthSize = getChildAt(0).getMeasuredWidth();
    setMeasuredDimension(widthSize, finalHeight);
   } else if (widthMode == MeasureSpec.AT_MOST) {
    widthSize = getChildAt(0).getMeasuredWidth();
    setMeasuredDimension(widthSize, height);
   } else {
    setMeasuredDimension(widthSize, finalHeight);
   }
 
  }
 
 
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
   int topOffset = 0;
   for (int i = 0; i < getChildCount(); i++) {
    View child = getChildAt(i);
    child.layout(getPaddingLeft(), getPaddingTop() + topOffset, r, getPaddingTop() + child.getMeasuredHeight() + topOffset);
    topOffset += child.getMeasuredHeight();
   }
   mInitScrollY = mHeadView.getMeasuredHeight() + getPaddingTop();
   scrollTo(0, mInitScrollY);
 
  }
 
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
   boolean intercepted = false;
   int x = (int) ev.getX();
   int y = (int) ev.getY();
   switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
     mLastXIntercepted = x;
     mLastYIntercepted = y;
     break;
    case MotionEvent.ACTION_MOVE:
     final int deltaY = x - mLastYIntercepted;
     if (isTop() && deltaY > 0 && Math.abs(deltaY) > mTouchSlop) {
      /*下拉*/
      intercepted = true;
     }
     break;
    case MotionEvent.ACTION_UP:
     break;
   }
   mLastXIntercepted = x;
   mLastYIntercepted = y;
   return intercepted;
  }
 
  private void doRefresh() {
   Log.i(TAG, "doRefresh: ");
   if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
    mScoller.startScroll(0, getScrollY(), 0, mInitScrollY - getScrollY(), SCROLL_DURATION);
    currentStatus = STATUS_IDLE;
   } else if (currentStatus == STATUS_PULL_TO_REFRESH) {
    mScoller.startScroll(0,getScrollY(),0,0-getScrollY(),SCROLL_DURATION);
    if (null != mOnRefreshListener) {
     currentStatus = STATUS_LOADING;
     mOnRefreshListener.refresh();
    }
   }
   invalidate();
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
   int x = (int) event.getX();
   int y = (int) event.getY();
   switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
     if (!mScoller.isFinished()) {
      mScoller.abortAnimation();
     }
     mLastX = x;
     mLastY = y;
     break;
    case MotionEvent.ACTION_MOVE:
     if (isFistTouch) {
      isFistTouch = false;
      mLastX = x;
      mLastY = y;
     }
     final int deltaY = y - mLastY;
     if (currentStatus != STATUS_LOADING) {
      changeScrollY(deltaY);
     }
     break;
    case MotionEvent.ACTION_UP:
     isFistTouch = true;
     doRefresh();
     break;
   }
 
   mLastX = x;
   mLastY = y;
   return true;
  }
 
  private void changeScrollY(int deltaY) {
   Log.i(TAG, "changeScrollY: ");
   int curY = getScrollY();
   if (deltaY > 0) {
    /*下拉*/
    if (curY - deltaY > getPaddingTop()) {
     scrollBy(0, -deltaY);
    }
   } else {
    /*上拉*/
    if (curY - deltaY <= mInitScrollY) {
     scrollBy(0, -deltaY);
    }
   }
 
   curY = getScrollY();
   int slop = mInitScrollY / 2;
   if (curY > 0 && curY <=slop) {
    currentStatus = STATUS_PULL_TO_REFRESH;
   } else if (curY > 0 && curY >= slop) {
    currentStatus = STATUS_RELEASE_TO_REFRESH;
   }
  }
 
  @Override
  public void computeScroll() {
   if (mScoller.computeScrollOffset()) {
    scrollTo(mScoller.getCurrX(), mScoller.getCurrY());
    postInvalidate();
   }
  }
 
  /**
   * 加载完成调用这个方法
   */
  public void refreshComplete() {
   mScoller.startScroll(0, getScrollY(), 0, mInitScrollY - getScrollY(), SCROLL_DURATION);
   currentStatus = STATUS_IDLE;
   invalidate();
  }
 
  /**
   * 显示 Footer
   */
  public void showFooter() {
   if(currentStatus==STATUS_LOAD_MORE) return ;
   currentStatus = STATUS_LOAD_MORE ;
   mScoller.startScroll(0, getScrollY(), 0, mFootView.getMeasuredHeight()
     , SCROLL_DURATION);
   invalidate();
 
  }
 
 
  /**
   * loadMore完成之后调用
   */
  public void footerComplete() {
   mScoller.startScroll( 0 , getScrollY(), 0 , mInitScrollY - getScrollY(), SCROLL_DURATION);
   invalidate();
   currentStatus = STATUS_IDLE;
  }
 
  public interface OnRefreshListener {
   void refresh();
  }
 
  abstract boolean isTop();
 
  abstract boolean isBottom();
 
}

它是一个抽象类,需要编写子类继承isTop()和 isBottom()方法。下面给出它的一个实现类:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.blueberry.sample.widget.refresh;
 
import android.content.Context;
import android.util.AttributeSet;
import android.widget.AbsListView;
import android.widget.ListView;
 
/**
  * Created by blueberry on 2016/6/21.
  *
  * RefreshLayoutBase 的一个实现类
  */
public class RefreshListView extends RefreshLayoutBase<ListView> {
 
  private static final String TAG = "RefreshListView" ;
 
  private ListView listView;
  private OnLoadListener loadListener;
 
  public RefreshListView(Context context) {
   super (context);
  }
 
  public RefreshListView(Context context, AttributeSet attrs) {
   super (context, attrs);
  }
 
  public RefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
   super (context, attrs, defStyleAttr);
  }
 
  public ListView getListView() {
   return listView;
  }
 
  public void setListView( final ListView listView) {
   this .listView = listView;
   setContentView(listView);
 
   this .listView.setOnScrollListener( new AbsListView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
    }
 
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
 
     /*这里存在一个bug: 当listView滑动到底部的时候,如果下拉也会出现footer
     * 这是因为,暂时还没有想到如何判断是下拉还是上拉。
     * 如果要解决此问题,我觉得应该重写listView 的onTouchEvent来判断手势方向
     * 次模块主要解决竖向滑动冲突,故现将此问题放下。
     * */
     if (currentStatus == STATUS_IDLE
       && getScrollY() <= mInitScrollY && isBottom()
       ) {
      showFooter();
      if ( null != loadListener) {
       loadListener.onLoadMore();
      }
     }
 
    }
   });
  }
 
  public OnLoadListener getLoadListener() {
   return loadListener;
  }
 
  public void setLoadListener(OnLoadListener loadListener) {
   this .loadListener = loadListener;
  }
 
  @Override
  boolean isTop() {
   return listView.getFirstVisiblePosition() == 0
     && getScrollY() <= mHeadView.getMeasuredHeight();
  }
 
  @Override
  boolean isBottom() {
   return listView.getLastVisiblePosition() == listView.getAdapter().getCount() - 1 ;
  }
 
  public interface OnLoadListener {
   void onLoadMore();
  }
}

4、内部拦截法解决同向滑动

同样是一个下拉刷新组件,因为实现原理都一样,所以这个写的比较随意些。主要还是如果解决滑动冲突。

RefreshLayoutBase2.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
package com.blueberry.sample.widget.refresh;
 
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Scroller;
 
import com.blueberry.sample.R;
 
import java.util.ArrayList;
import java.util.List;
 
/**
  * Created by blueberry on 2016/6/22.
  * 结合内部类 ListVieEx
  * 内部拦截法,同向
  */
public class RefreshLayoutBase2 extends ViewGroup {
 
  private static final String TAG = "RefreshLayoutBase2" ;
 
  private static List<String> datas;
 
  static {
   datas = new ArrayList<>();
   for ( int i = 0 ; i < 40 ; i++) {
    datas.add( "数据—" + i);
   }
  }
 
  private ViewGroup headView;
  private ListViewEx lv;
 
  private int lastY;
  public int mInitScrollY;
 
  private Scroller mScroller;
 
  public RefreshLayoutBase2(Context context) {
   this (context, null );
  }
 
  public RefreshLayoutBase2(Context context, AttributeSet attrs) {
   this (context, attrs, 0 );
 
  }
 
  public RefreshLayoutBase2(Context context, AttributeSet attrs, int defStyleAttr) {
   super (context, attrs, defStyleAttr);
   mScroller = new Scroller(context);
   setupHeadView(context);
   setupContentView(context);
 
  }
 
  @Override
  protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
   int widthSize = MeasureSpec.getSize(widthMeasureSpec);
   int widthMode = MeasureSpec.getMode(widthMeasureSpec);
   int height = MeasureSpec.getSize(heightMeasureSpec);
   int heightMode = MeasureSpec.getMode(heightMeasureSpec);
 
   int finalHeight = 0 ;
   for ( int i = 0 ; i < getChildCount(); i++) {
    View child = getChildAt(i);
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
    finalHeight += child.getMeasuredHeight();
   }
 
   if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
    widthSize = getChildAt( 0 ).getMeasuredWidth();
    setMeasuredDimension(widthSize, finalHeight);
   } else if (widthMode == MeasureSpec.AT_MOST) {
    widthSize = getChildAt( 0 ).getMeasuredWidth();
    setMeasuredDimension(widthSize, height);
   } else {
    setMeasuredDimension(widthSize, finalHeight);
   }
 
  }
 
  @Override
  protected void onLayout( boolean changed, int l, int t, int r, int b) {
   int topOffset = 0 ;
   for ( int i = 0 ; i < getChildCount(); i++) {
    View child = getChildAt(i);
    child.layout(getPaddingLeft(), getPaddingTop() + topOffset, r, getPaddingTop() + child.getMeasuredHeight() + topOffset);
    topOffset += child.getMeasuredHeight();
   }
   mInitScrollY = headView.getMeasuredHeight() + getPaddingTop();
   scrollTo( 0 , mInitScrollY);
 
  }
 
  /**
   * 不拦截Down 其他一律拦截
   * @param ev
   * @return
   */
  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {
   if (ev.getAction() == MotionEvent.ACTION_DOWN) return false ;
   return true ;
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
   int y = ( int ) event.getY();
   switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
     break ;
    case MotionEvent.ACTION_MOVE:
     final int deltaY = y-lastY;
     Log.i(TAG, "onTouchEvent: deltaY: " +deltaY);
     if (deltaY >= 0 && lv.isTop() && getScrollY() - deltaY >=getPaddingTop()) {
       scrollBy( 0 , -deltaY);
     }
     break ;
    case MotionEvent.ACTION_UP:
     this .postDelayed( new Runnable() {
      @Override
      public void run() {
       mScroller.startScroll( 0 ,getScrollY(), 0 ,mInitScrollY-getScrollY());
       invalidate();
      }
     }, 2000 );
     break ;
   }
 
   lastY = y ;
   return true ;
  }
 
  private void setupHeadView(Context context) {
   headView = (ViewGroup) View.inflate(context, R.layout.fresh_head_view, null );
   headView.setBackgroundColor(Color.RED);
   ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300 );
   addView(headView, params);
  }
 
  public void setupContentView(Context context) {
   lv = new ListViewEx(context, this );
   lv.setBackgroundColor(Color.BLUE);
   ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), android.R.layout.simple_list_item_1, datas);
   lv.setAdapter(adapter);
   addView(lv, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
  }
 
  @Override
  public void computeScroll() {
   if (mScroller.computeScrollOffset()){
    scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
    postInvalidate();
   }
  }
 
  public static class ListViewEx extends ListView {
 
   private RefreshLayoutBase2 outter;
 
   public ListViewEx(Context context, RefreshLayoutBase2 outter) {
    super (context);
    this .outter = outter;
   }
 
   public ListViewEx(Context context, AttributeSet attrs) {
    super (context, attrs);
   }
 
   public ListViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
    super (context, attrs, defStyleAttr);
   }
 
   /**
    * 使用 outter.requestDisallowInterceptTouchEvent();
    * 来决定父控件是否对事件进行拦截
    * @param ev
    * @return
    */
   @Override
   public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
     case MotionEvent.ACTION_DOWN:
      outter.requestDisallowInterceptTouchEvent( true );
      break ;
     case MotionEvent.ACTION_MOVE:
 
      if ( isTop() && outter.getScrollY() <= outter.mInitScrollY) {
       outter.requestDisallowInterceptTouchEvent( false );
      }
      break ;
 
    }
    return super .dispatchTouchEvent(ev);
   }
 
   public boolean isTop() {
    return getFirstVisiblePosition() == 0 ;
   }
  }
}












注:

1、转载原文地址:

http://blog.csdn.net/tgbus18990140382/article/details/53887982

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1117/574.html

http://www.jb51.net/article/104882.htm




2、

滑动速度跟踪类VelocityTracker介绍


VelocityTracker 是一个跟踪触摸事件滑动速度的帮助类,用于实现flinging以及其它类似的手势。它的原理是把触摸事件 MotionEvent 对象传递给VelocityTracker的addMovement(MotionEvent)方法,然后分析MotionEvent 对象在单位时间类发生的位移来计算速度。你可以使用getXVelocity() 或getXVelocity()获得横向和竖向的速率到速率时,但是使用它们之前请先调用computeCurrentVelocity(int)来初始化速率的单位 。

主要函数

Public Methods
void addMovement( MotionEvent event)

Add a user's movement to the tracker.

void clear()

Reset the velocity tracker back to its initial state.

void computeCurrentVelocity(int units, float maxVelocity)

Compute the current velocity based on the points that have been collected.

intunitis表示速率的基本时间单位。unitis值为1的表示是,一毫秒时间单位内运动了多少个像素, unitis值为1000表示一秒(1000毫秒)时间单位内运动了多少个像素

floatVelocity表示速率的最大值

void computeCurrentVelocity(int units)

Equivalent to invoking computeCurrentVelocity(int, float)with a maximum velocity of Float.MAX_VALUE.

abstract T getNextPoolable()
float getXVelocity()

Retrieve the last computed X velocity.

float getXVelocity(int id)

Retrieve the last computed X velocity.

float getYVelocity(int id)

Retrieve the last computed Y velocity.

float getYVelocity()

Retrieve the last computed Y velocity.

abstract boolean isPooled()
static  VelocityTracker obtain()

Retrieve a new VelocityTracker object to watch the velocity of a motion.

void recycle()

Return a VelocityTracker object back to be re-used by others.

abstract void setNextPoolable(T element)
abstract void setPooled(boolean isPooled)

示例:

 
 
  1. private VelocityTracker mVelocityTracker;//生命变量  
  2. //在onTouchEvent(MotionEvent ev)中  
  3. if (mVelocityTracker == null) {  
  4.     mVelocityTracker = VelocityTracker.obtain();//获得VelocityTracker类实例  
  5. }  
  6. mVelocityTracker.addMovement(ev);//将事件加入到VelocityTracker类实例中  
  7. //判断当ev事件是MotionEvent.ACTION_UP时:计算速率  
  8. final VelocityTracker velocityTracker = mVelocityTracker;  
  9. // 1000 provides pixels per second  
  10. velocityTracker.computeCurrentVelocity(1, (float)0.01);//设置maxVelocity值为0.1时,速率大于0.01时,显示的速率都是0.01,速率小于0.01时,显示正常  
  11. Log.i("test","velocityTraker"+velocityTracker.getXVelocity());  
  12. velocityTracker.computeCurrentVelocity(1000); //设置units的值为1000,意思为一秒时间内运动了多少个像素  
  13. Log.i("test","velocityTraker"+velocityTracker.getXVelocity());

大体的使用是这样的:

当你需要跟踪触摸屏事件的速度的时候,使用obtain()方法来获得VelocityTracker类的一个实例对象

在onTouchEvent回调函数中,使用addMovement(MotionEvent)函数将当前的移动事件传递给VelocityTracker对象

使用computeCurrentVelocity (int units)函数来计算当前的速度,使用getXVelocity ()、 getYVelocity ()函数来获得当前的速度

下面是一个简单Demo: 

 
 
  1. package com.bxwu.demo.component.activity; 
  2. import android.app.Activity; 
  3. import android.graphics.Color; 
  4. import android.os.Bundle; 
  5. import android.view.MotionEvent; 
  6. import android.view.VelocityTracker; 
  7. import android.view.ViewConfiguration; 
  8. import android.view.ViewGroup.LayoutParams; 
  9. import android.widget.TextView; 
  10.   
  11. public class VelocityTrackerTest extends Activity { 
  12.     private TextView mInfo; 
  13.   
  14.     private VelocityTracker mVelocityTracker; 
  15.     private int mMaxVelocity; 
  16.   
  17.     private int mPointerId; 
  18.   
  19.     @Override 
  20.     protected void onCreate(Bundle savedInstanceState) { 
  21.         super.onCreate(savedInstanceState); 
  22.         
  23.         mVelocityTracker = VelocityTracker.obtain(); 
  24.         mMaxVelocity = ViewConfiguration.get(this).getMaximumFlingVelocity();     
  25.             
  26.         mInfo = new TextView(this); 
  27.         mInfo.setLines(4); 
  28.         mInfo.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); 
  29.         mInfo.setTextColor(Color.WHITE); 
  30.         setContentView(mInfo); 
  31.     } 
  32.   
  33.     @Override 
  34.     public boolean onTouchEvent(MotionEvent event) { 
  35.         final int action = event.getAction(); 
  36.         
  37.         mVelocityTracker.addMovement(event); 
  38.         
  39.         final VelocityTracker verTracker = mVelocityTracker; 
  40.         switch (action) { 
  41.             case MotionEvent.ACTION_DOWN: 
  42.                 //求第一个触点的id, 此时可能有多个触点,但至少一个 
  43.                 mPointerId = event.getPointerId(0); 
  44.                 break; 
  45.   
  46.             case MotionEvent.ACTION_MOVE: 
  47.                 //求伪瞬时速度 
  48.                 verTracker.computeCurrentVelocity(1000, mMaxVelocity); 
  49.                 final float velocityX = verTracker.getXVelocity(mPointerId); 
  50.                 final float velocityY = verTracker.getYVelocity(mPointerId); 
  51.                 recodeInfo(velocityX, velocityY); 
  52.                 break; 
  53.   
  54.             case MotionEvent.ACTION_UP: 
  55.                 releaseVelocityTracker(); 
  56.                 break; 
  57.   
  58.             case MotionEvent.ACTION_CANCEL: 
  59.                 releaseVelocityTracker(); 
  60.                 break; 
  61.   
  62.             default: 
  63.                 break; 
  64.         } 
  65.         return super.onTouchEvent(event); 
  66.     } 
  67.  
  68.     //释放VelocityTracker 
  69.  
  70.     private void releaseVelocityTracker() { 
  71.         if(null != mVelocityTracker) { 
  72.             mVelocityTracker.clear(); 
  73.             mVelocityTracker.recycle(); 
  74.             mVelocityTracker = null; 
  75.         } 
  76.     } 
  77.   
  78.     private static final String sFormatStr = "velocityX=%f\nvelocityY=%f"; 
  79.   
  80.     /** 
  81.      * 记录当前速度 
  82.      * 
  83.      * @param velocityX x轴速度 
  84.      * @param velocityY y轴速度 
  85.      */
  86.     private void recodeInfo(final float velocityX, final float velocityY) { 
  87.         final String info = String.format(sFormatStr, velocityX, velocityY); 
  88.         mInfo.setText(info); 
  89.     } 
  90. }

代码很简单,我们可以求出move过程中的伪瞬时速度, 这样在做很多控件的时候都是可以用到的,比如系统Launcher的分页,

ScrollView滑动等, 可根据此时的速度来计算ACTION_UP后的减速运动等。实现一些非常棒的效果。





3、 

Android Scroller完全解析,关于Scroller你所需知道的一切

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/48719871 
2016大家新年好!这是今年的第一篇文章,那么应CSDN工作人员的建议,为了能给大家带来更好的阅读体验,我也是将博客换成了宽屏版。另外,作为一个对新鲜事物从来后知后觉的人,我终于也在新的一年里改用MarkDown编辑器来写博客了,希望大家在我的博客里也能体验到新年新的气象。 
我写博客的题材很多时候取决于平时大家问的问题,最近一段时间有不少朋友都问到ViewPager是怎么实现的。那ViewPager相信每个人都再熟悉不过了,因此它实在是太常用了,我们可以借助ViewPager来轻松完成页面之间的滑动切换效果,但是如果问到它是如何实现的话,我感觉大部分人还是比较陌生的, 为此我也是做了一番功课。其实说到ViewPager最基本的实现原理主要就是两部分内容,一个是事件分发,一个是Scroller,那么对于事件分发,其实我在很早之前就已经写过了相关的内容,感兴趣的朋友可以去阅读Android事件分发机制完全解析,带你从源码的角度彻底理解,但是对于Scroller我还从来没有讲过,因此本篇文章我们就先来学习一下Scroller的用法,并结合事件分发和Scroller来实现一个简易版的ViewPager。


Scroller是一个专门用于处理滚动效果的工具类,可能在大多数情况下,我们直接使用Scroller的场景并不多,但是很多大家所熟知的控件在内部都是使用Scroller来实现的,如ViewPager、ListView等。而如果能够把Scroller的用法熟练掌握的话,我们自己也可以轻松实现出类似于ViewPager这样的功能。那么首先新建一个ScrollerTest项目,今天就让我们通过例子来学习一下吧。 
先撇开Scroller类不谈,其实任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法,如下图所示: 


这两个方法都是用于对View进行滚动的,那么它们之间有什么区别呢?简单点讲,scrollBy()方法是让View相对于当前的位置滚动某段距离,而scrollTo()方法则是让View相对于初始的位置滚动某段距离。这样讲大家理解起来可能有点费劲,我们来通过例子实验一下就知道了。 
修改activity_main.xml中的布局文件,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.guolin.scrollertest.MainActivity">

    <Button
        android:id="@+id/scroll_to_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scrollTo"/>

    <Button
        android:id="@+id/scroll_by_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scrollBy"/>

</LinearLayout>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

外层我们使用了一个LinearLayout,然后在里面包含了两个按钮,一个用于触发scrollTo逻辑,一个用于触发scrollBy逻辑。 
接着修改MainActivity中的代码,如下所示:

public class MainActivity extends AppCompatActivity {

    private LinearLayout layout;

    private Button scrollToBtn;

    private Button scrollByBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        layout = (LinearLayout) findViewById(R.id.layout);
        scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);
        scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);
        scrollToBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                layout.scrollTo(-60, -100);
            }
        });
        scrollByBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                layout.scrollBy(-60, -100);
            }
        });
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

没错,代码就是这么简单。当点击了scrollTo按钮时,我们调用了LinearLayout的scrollTo()方法,当点击了scrollBy按钮时,调用了LinearLayout的scrollBy()方法。那有的朋友可能会问了,为什么都是调用的LinearLayout中的scroll方法?这里一定要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,而LinearLayout中的内容就是我们的两个Button,如果你直接调用button的scroll方法的话,那结果一定不是你想看到的。 
另外还有一点需要注意,就是两个scroll方法中传入的参数,第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动,单位是像素。第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动,单位是像素。 
那说了这么多,scrollTo()和scrollBy()这两个方法到底有什么区别呢?其实运行一下代码我们就能立刻知道了: 


可以看到,当我们点击scrollTo按钮时,两个按钮会一起向右下方滚动,因为我们传入的参数是-60和-100,因此向右下方移动是正确的。但是你会发现,之后再点击scrollTo按钮就没有任何作用了,界面不会再继续滚动,只有点击scrollBy按钮界面才会继续滚动,并且不停点击scrollBy按钮界面会一起滚动下去。 
现在我们再来回头看一下这两个方法的区别,scrollTo()方法是让View相对于初始的位置滚动某段距离,由于View的初始位置是不变的,因此不管我们点击多少次scrollTo按钮滚动到的都将是同一个位置。而scrollBy()方法则是让View相对于当前的位置滚动某段距离,那每当我们点击一次scrollBy按钮,View的当前位置都进行了变动,因此不停点击会一直向右下方移动。 
通过这个例子来理解,相信大家已经把scrollTo()和scrollBy()这两个方法的区别搞清楚了,但是现在还有一个问题,从上图中大家也能看得出来,目前使用这两个方法完成的滚动效果是跳跃式的,没有任何平滑滚动的效果。没错,只靠scrollTo()和scrollBy()这两个方法是很难完成ViewPager这样的效果的,因此我们还需要借助另外一个关键性的工具,也就我们今天的主角Scroller。 
Scroller的基本用法其实还是比较简单的,主要可以分为以下几个步骤: 
1. 创建Scroller的实例 
2. 调用startScroll()方法来初始化滚动数据并刷新界面 
3. 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 
那么下面我们就按照上述的步骤,通过一个模仿ViewPager的简易例子来学习和理解一下Scroller的用法。 
新建一个ScrollerLayout并让它继承自ViewGroup来作为我们的简易ViewPager布局,代码如下所示:

/**
 * Created by guolin on 16/1/12.
 */
public class ScrollerLayout extends ViewGroup {

    /**
     * 用于完成滚动操作的实例
     */
    private Scroller mScroller;

    /**
     * 判定为拖动的最小移动像素数
     */
    private int mTouchSlop;

    /**
     * 手机按下时的屏幕坐标
     */
    private float mXDown;

    /**
     * 手机当时所处的屏幕坐标
     */
    private float mXMove;

    /**
     * 上次触发ACTION_MOVE事件时的屏幕坐标
     */
    private float mXLastMove;

    /**
     * 界面可滚动的左边界
     */
    private int leftBorder;

    /**
     * 界面可滚动的右边界
     */
    private int rightBorder;

    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 第一步,创建Scroller的实例
        mScroller = new Scroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        // 获取TouchSlop值
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            // 为ScrollerLayout中的每一个子控件测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                // 为ScrollerLayout中的每一个子控件在水平方向上进行布局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
            // 初始化左右边界值
            leftBorder = getChildAt(0).getLeft();
            rightBorder = getChildAt(getChildCount() - 1).getRight();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mXLastMove = mXDown;
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mXDown);
                mXLastMove = mXMove;
                // 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
                if (diff > mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();
                int scrolledX = (int) (mXLastMove - mXMove);
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                    scrollTo(rightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                mXLastMove = mXMove;
                break;
            case MotionEvent.ACTION_UP:
                // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132

整个Scroller用法的代码都在这里了,代码并不长,一共才100多行,我们一点点来看。 
首先在ScrollerLayout的构造函数里面我们进行了上述步骤中的第一步操作,即创建Scroller的实例,由于Scroller的实例只需创建一次,因此我们把它放到构造函数里面执行。另外在构建函数中我们还初始化的TouchSlop的值,这个值在后面将用于判断当前用户的操作是否是拖动。 
接着重写onMeasure()方法和onLayout()方法,在onMeasure()方法中测量ScrollerLayout里的每一个子控件的大小,在onLayout()方法中为ScrollerLayout里的每一个子控件在水平方向上进行布局。如果有朋友对这两个方法的作用还不理解,可以参照我之前写的一篇文章 Android视图绘制流程完全解析,带你一步步深入了解View(二) 。 
接着重写onInterceptTouchEvent()方法, 在这个方法中我们记录了用户手指按下时的X坐标位置,以及用户手指在屏幕上拖动时的X坐标位置,当两者之间的距离大于TouchSlop值时,就认为用户正在拖动布局,然后我们就将事件在这里拦截掉,阻止事件传递到子控件当中。 
那么当我们把事件拦截掉之后,就会将事件交给ScrollerLayout的onTouchEvent()方法来处理。如果当前事件是ACTION_MOVE,说明用户正在拖动布局,那么我们就应该对布局内容进行滚动从而影响拖动事件,实现的方式就是使用我们刚刚所学的scrollBy()方法,用户拖动了多少这里就scrollBy多少。另外为了防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用scrollTo()方法来回到边界位置。 
如果当前事件是ACTION_UP时,说明用户手指抬起来了,但是目前很有可能用户只是将布局拖动到了中间,我们不可能让布局就这么停留在中间的位置,因此接下来就需要借助Scroller来完成后续的滚动操作。首先这里我们先根据当前的滚动位置来计算布局应该继续滚动到哪一个子控件的页面,然后计算出距离该页面还需滚动多少距离。接下来我们就该进行上述步骤中的第二步操作,调用startScroll()方法来初始化滚动数据并刷新界面。startScroll()方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。 
现在前两步都已经完成了,最后我们还需要进行第三步操作,即重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的curX和curY坐标传入,然后刷新界面从而完成平滑滚动的操作。 
现在ScrollerLayout已经准备好了,接下来我们修改activity_main.xml布局中的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<com.example.guolin.scrollertest.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is first child view"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is second child view"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is third child view"/>

</com.example.guolin.scrollertest.ScrollerLayout>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

可以看到,这里我们在ScrollerLayout中放置了三个按钮用来进行测试,其实这里不仅可以放置按钮,放置任何控件都是没问题的。 
最后MainActivity当中删除掉之前测试的代码:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

好的,所有代码都在这里了,现在我们可以运行一下程序来看一看效果了,如下图所示: 


怎么样,是不是感觉有点像一个简易的ViewPager了?其实借助Scroller,很多漂亮的滚动效果都可以轻松完成,比如实现图片轮播之类的特效。当然就目前这一个例子来讲,我们只是借助它来学习了一下Scroller的基本用法,例子本身有很多的功能点都没有去实现,比如说ViewPager会根据用户手指滑动速度的快慢来决定是否要翻页,这个功能在我们的例子中并没有体现出来,不过大家也可以当成自我训练来尝试实现一下。


好的,那么本篇文章就到这里,相信通过这篇文章的学习,大家已经能够熟练掌握Scroller的使用方法了,当然ViewPager的内部实现要比这复杂得多,如果有朋友对ViewPager的源码感兴趣也可以尝试去读一下,不过一定需要非常扎实的基本功才行。








  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值