likes
comments
collection
share

Android源码角度分析事件分发消费之应用篇

作者站长头像
站长
· 阅读数 9

自定义简单ViewPager

首先我们做一个简单的ViewPager,废话不说了,上代码。java 代码

public class ScrollerLayout extends ViewGroup {
    private final int mTouchSlop;
    private Scroller mScroller;
    private int leftBorder;
    private int rightBorder;
    private int downX;
    private int lastMoveX;
    private int moveX;


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

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

    public ScrollerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
        //这个地方是获取系统默认判定为滑动的最小值
        ViewConfiguration configuration = ViewConfiguration.get(context);
        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++){
            measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec);
        }

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if(!changed){
           return;
        }
        int childCount = getChildCount();
        for(int i = 0;i<childCount;i++){
            View child =  getChildAt(i);
            child.layout(i*child.getMeasuredWidth(),0,(i+1)*child.getMeasuredWidth(),child.getMeasuredHeight());
        }
        leftBorder = getChildAt(0).getLeft();
        rightBorder = getChildAt(getChildCount() - 1).getRight();
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = (int) event.getX();
                lastMoveX = downX;
            case MotionEvent.ACTION_MOVE:
                //etX()是表示Widget相对于自身左上角的x坐标,而getRawX()是表示相对于屏幕左上角的x坐标值
                moveX = (int) event.getRawX();
                int scrolledX = (lastMoveX - moveX);
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                    scrollTo(rightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                lastMoveX = moveX;
                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);
       Flag2: //return true;
    }

    @Override
    public void computeScroll() {
        // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}

xml 布局

<com.weshape3d.customviews.mycostomviews.ScrollerLayout
       android:layout_width="match_parent"
       android:layout_height="50dp">
      <TextView
          android:gravity="center"
          android:textColor="#ff0000"
          android:text="1111"
          android:layout_width="match_parent"
          android:layout_height="match_parent" />
      <TextView
          android:gravity="center"
          android:textColor="#222"
          android:text="USD发hi爱的是否"
          android:layout_width="match_parent"
          android:layout_height="match_parent" />
      <TextView
          android:gravity="center"
          android:textColor="#ff0000"
          android:text="333"
          android:layout_width="match_parent"
          android:layout_height="match_parent" />
   </com.weshape3d.customviews.mycostomviews.ScrollerLayout>

主要实现的逻辑地方我加了注释,自定义ViewGroup中布局、测量不是本篇重点。 主要看根据手势滑动简单的实现的逻辑:

  • 当按下手势触发的时候,记录下按下的X点不做拦截
  • 当滑动且大于等于预先设定的判定滑动的值的时候在onIntercept()方法中拦截手势
  • 拦截后会交给他自身的OnTouchEvent去处理 利用View 自身的scrollBy()方法做滑动

    问题

    大家会发现把这段代码运行之后,会有左右滑不动!!

    原因

    原因很简单因为ACTION_DOWN手势按下的时候,没有找到消费它的目标就导致后续的ACTION_MOVE 和ACTION_UP等事件得不到分发了!

    解决

  • 在布局中把TextView换成Button;这样之所以可以是因为Button控件默认是CLICKABLE的,这就导致当ACITION传递过来的时候,就被消费了,致使后续的事件也会传递过来,这样在onInterceptTouchEvent()方法中当符合滑动条件的时候,我们就把事件拦截,在onTouchEvent()方法中让ViewGroup做滑动。
  • 代码中Flag2处,让ViewGroup的onTouchEvent,返回true。

RefreshSwipLayout怎么进行事件消费和分发

Now,满心欢喜的打开开RefreshSwipLayout源码

 public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();

        final int action = MotionEventCompat.getActionMasked(ev);
        int pointerIndex;

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
                mActivePointerId = ev.getPointerId(0);
                mIsBeingDragged = false;

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                mInitialDownY = ev.getY(pointerIndex);
                break;

            case MotionEvent.ACTION_MOVE:
                if (mActivePointerId == INVALID_POINTER) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                    return false;
                }

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                final float y = ev.getY(pointerIndex);
                startDragging(y);
                break;

            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }

        return mIsBeingDragged;
    }

咦?跟预想的不太一样!ACTION_POINTER_UP ACTION_POINTER_DOWN是什么鬼很高级的样子!先稳住。

  • ACTION_POINTER_UP,ACTION_POINTER_DOWN表示第一个手指之外的其他手指按下和抬起的时候,触发的
  • 可以按这么多手机,我怎么知道哪是一个?有下面这些方法,有了这些就可以轻松的知道哪个手指了
//获取触控所属手指的位置,也就是按时间先后放在屏幕上排序第几个
int indext = event.getActionIndex();
//根据位置获取手指ID
int pointID = event.getPointerId(index);
//根据ID获取是第几个
int index =  event.findPointerIndex(indext);
  • 获取位置的时候
//获取第几个手指x
 float x = ev.getX(pointerIndex);

看透他的实质,拆穿他的本质之前,我们先拎着几个问题:

  • 像我们上面自定义ViewPager一样遇到的问题,RefreshSwipLayout是怎么解决的?
  • 两个手指多个手势滑动的时候,怎么处理?(多点触控)

    SwipeRefreshLayout中的onInterceptTouchEvent

    看源码,必要的地方加了注释和删减
public boolean onInterceptTouchEvent(MotionEvent ev) {
    ensureTarget();ensureTarget();
    //获取多点触控的动作
    final int action = MotionEventCompat.getActionMasked(ev);
    int pointerIndex;
    switch (action) {
        case MotionEvent.ACTION_DOWN:
        /**
        第一个手指按下时,获取他的pointID。
        pointID是从触摸点在整个从按下到抬起过程中,唯一不变的id
        **/
            mActivePointerId = ev.getPointerId(0);
            mIsBeingDragged = false;
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }
            //记录下按下的位置
            mInitialDownY = ev.getY(pointerIndex);
            break;

        case MotionEvent.ACTION_MOVE:
            //找到要跟踪的触控点pointerIndex,用它来获取位置
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }
            final float y = ev.getY(pointerIndex);
            //滑动处理
            startDragging(y);
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            mIsBeingDragged = false;
            mActivePointerId = INVALID_POINTER;
            break;
    }
    //返回是否拦截
    return mIsBeingDragged;
}

private void startDragging(float y) {
    final float yDiff = y - mInitialDownY;
    if (yDiff > mTouchSlop && !mIsBeingDragged) {
        mInitialMotionY = mInitialDownY + mTouchSlop;
        mIsBeingDragged = true;
        mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
    }
}

小结

  • 化繁就简,在onInterceptTouchEvent()中做的主要的事情就是在按下的时候记录住触控点的ID也就是第一个触控点的ID并把这个点的Y坐标也记下来。
  • 在MOVE中找出找出我们记录的触控点,并把他当前的Y坐标找出来,交给startDragging()判断要不要拦截

SwipeRefreshLayou的OnTouchEvent

public boolean onTouchEvent(MotionEvent ev) {
    final int action = MotionEventCompat.getActionMasked(ev);
    int pointerIndex = -1;

    switch (action) {
        case MotionEvent.ACTION_DOWN:
        //记录下要用的触控点的ID
            mActivePointerId = ev.getPointerId(0);
            mIsBeingDragged = false;
            break;

        case MotionEvent.ACTION_MOVE: {
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                return false;
            }

            final float y = ev.getY(pointerIndex);
            startDragging(y);

            if (mIsBeingDragged) {
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                if (overscrollTop > 0) {
                    //滑动圆圈
                    moveSpinner(overscrollTop);
                } else {
                    return false;
                }
            }
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN: {
            //当其他手指按下的时候,就把使用的触控点换成当前按下的这个
            pointerIndex = MotionEventCompat.getActionIndex(ev);
            if (pointerIndex < 0) {
                Log.e(LOG_TAG,
                        "Got ACTION_POINTER_DOWN event but have an invalid action index.");
                return false;
            }
            mActivePointerId = ev.getPointerId(pointerIndex);
            break;
        }

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;

        case MotionEvent.ACTION_UP: {
            pointerIndex = ev.findPointerIndex(mActivePointerId);
            if (pointerIndex < 0) {
                Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                return false;
            }
            //如果控件的状态为拖动过程中,返回初始状态
            if (mIsBeingDragged) {
                final float y = ev.getY(pointerIndex);
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                mIsBeingDragged = false;
                finishSpinner(overscrollTop);
            }

            mActivePointerId = INVALID_POINTER;
            return false;
        }
        case MotionEvent.ACTION_CANCEL:
            return false;
    }
    //默认消费这个事件
    return true;
}

我认为必要的地方,都在上面做了注释。SwipRefreshLayout在onTouchEvent方法中做的主要工作简单的说主要三点