ViewPager源码分析——滑动切换页面处理过程

上周客户反馈Contacts快速滑动界面切换tab有明显卡顿,让优化。

自己验证又没发现卡顿现象,但总得给客户一个技术性的回复,于是看了一下ViewPager源码中处理滑动切换tab的过程。

ViewPager  源码位置: androidframeworkssupportv4javaandroidsupportv4viewViewPager.java

ViewPager其实就是一个重写的ViewGroup,使用ViewPager可以参考SDK中的demo:sdkextrasandroidsupportsamples

ViewPager.java开头的注释中有推荐一个demo,使用了supportv13

* <p>Here is a more complicated example of ViewPager, using it in conjuction
* with {@link android.app.ActionBar} tabs. You can find other examples of using
* ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code.
*
* {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java
* complete}

ViewPager滑动是处理Touch事件,所以有必要了解Touch事件的分发过程。可以参考这篇 http://blog.csdn.net/guolin_blog/article/details/9153747

public class ViewPager extends ViewGroup{
       
         ......
         
         @Override
         public boolean onInterceptTouchEvent(MotionEvent ev) {
             /*
              * This method JUST determines whether we want to intercept the motion.
              * If we return true, onMotionEvent will be called and we do the actual
              * scrolling there.
              */
                     
             final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
                     
             // Always take care of the touch gesture being complete.
             if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                 // Release the drag.
                 if (DEBUG) Log.v(TAG, "Intercept done!");
                 mIsBeingDragged = false;
                 mIsUnableToDrag = false;
                 mActivePointerId = INVALID_POINTER;
                 if (mVelocityTracker != null) {
                     mVelocityTracker.recycle();
                     mVelocityTracker = null;
                 }
                 return false;
             }

             // Nothing more to do here if we have decided whether or not we
             // are dragging.
             if (action != MotionEvent.ACTION_DOWN) {
                 if (mIsBeingDragged) {
                     if (DEBUG) Log.v(TAG, "Intercept returning true!");
                     return true;
                 }
                 if (mIsUnableToDrag) {
                     if (DEBUG) Log.v(TAG, "Intercept returning false!");
                     return false;
                 }
             }

             switch (action) {
                 case MotionEvent.ACTION_MOVE: {
                     /*
                      * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
                      * whether the user has moved far enough from his original down touch.
                      */

                     /*
                     * Locally do absolute value. mLastMotionY is set to the y value
                     * of the down event.
                     */
                     final int activePointerId = mActivePointerId;
                     
                     if (activePointerId == INVALID_POINTER) {
                         // If we don't have a valid id, the touch down wasn't on content.
                         break;
                     }

                     final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
                     final float x = MotionEventCompat.getX(ev, pointerIndex);
                     final float dx = x - mLastMotionX;
                     final float xDiff = Math.abs(dx);
                     final float y = MotionEventCompat.getY(ev, pointerIndex);
                     final float yDiff = Math.abs(y - mInitialMotionY);
                                     
                      boolean isGutterDrag = isGutterDrag(mLastMotionX, dx);
                      boolean canScroll  = canScroll(this, false, (int) dx, (int) x, (int) y);
                                     
                     if (dx != 0 && !isGutterDrag && canScroll) {
                         // Nested view has scrollable area under this point. Let it be handled there.
                         mLastMotionX = x;
                         mLastMotionY = y;
                         mIsUnableToDrag = true;
                         return false;
                     }
                     if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                         if (DEBUG) Log.v(TAG, "Starting drag!");
                         mIsBeingDragged = true;
                         requestParentDisallowInterceptTouchEvent(true);
                         setScrollState(SCROLL_STATE_DRAGGING);
                         mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                                 mInitialMotionX - mTouchSlop;
                         mLastMotionY = y;
                         setScrollingCacheEnabled(true);
                     } else if (yDiff > mTouchSlop) {
                         // The finger has moved enough in the vertical
                         // direction to be counted as a drag...  abort
                         // any attempt to drag horizontally, to work correctly
                         // with children that have scrolling containers.
                         if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                         mIsUnableToDrag = true;
                     }
                     if (mIsBeingDragged) {
                         // Scroll to follow the motion event
                         if (performDrag(x)) {
                             ViewCompat.postInvalidateOnAnimation(this);
                         }
                     }
                     break;
                 }

                 case MotionEvent.ACTION_DOWN: {
                     /*
                      * Remember location of down touch.
                      * ACTION_DOWN always refers to pointer index 0.
                      */
                     mLastMotionX = mInitialMotionX = ev.getX();
                     mLastMotionY = mInitialMotionY = ev.getY();
                     mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                     mIsUnableToDrag = false;
                     

                     mScroller.computeScrollOffset();
                     if (mScrollState == SCROLL_STATE_SETTLING &&
                             Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                         // Let the user 'catch' the pager as it animates.
                         mScroller.abortAnimation();
                         mPopulatePending = false;
                         populate();
                         mIsBeingDragged = true;
                         requestParentDisallowInterceptTouchEvent(true);
                         setScrollState(SCROLL_STATE_DRAGGING);
                     } else {
                         completeScroll(false);
                         mIsBeingDragged = false;
                     }

                     if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
                             + " mIsBeingDragged=" + mIsBeingDragged
                             + "mIsUnableToDrag=" + mIsUnableToDrag);
                     break;
                 }

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

             if (mVelocityTracker == null) {
                 mVelocityTracker = VelocityTracker.obtain();
             }
             mVelocityTracker.addMovement(ev);

             /*
              * The only time we want to intercept motion events is if we are in the
              * drag mode.
              */
             return mIsBeingDragged;
         }

         @Override
         public boolean onTouchEvent(MotionEvent ev) {
             if (mFakeDragging) {
                 // A fake drag is in progress already, ignore this real one
                 // but still eat the touch events.
                 // (It is likely that the user is multi-touching the screen.)
                 return true;
             }

             if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
                 // Don't handle edge touches immediately -- they may actually belong to one of our
                 // descendants.
                 return false;
             }

             if (mAdapter == null || mAdapter.getCount() == 0) {
                 // Nothing to present or scroll; nothing to touch.
                 return false;
             }

             if (mVelocityTracker == null) {
                 mVelocityTracker = VelocityTracker.obtain();
             }
             mVelocityTracker.addMovement(ev);

             final int action = ev.getAction();
             boolean needsInvalidate = false;
                     
             switch (action & MotionEventCompat.ACTION_MASK) {
                 case MotionEvent.ACTION_DOWN: {
                     mScroller.abortAnimation();
                     mPopulatePending = false;
                     populate();

                     // Remember where the motion event started
                     mLastMotionX = mInitialMotionX = ev.getX();
                     mLastMotionY = mInitialMotionY = ev.getY();
                     mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                     break;
                 }
                 case MotionEvent.ACTION_MOVE:
                     if (!mIsBeingDragged) {
                         final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                         final float x = MotionEventCompat.getX(ev, pointerIndex);
                         final float xDiff = Math.abs(x - mLastMotionX);
                         final float y = MotionEventCompat.getY(ev, pointerIndex);
                         final float yDiff = Math.abs(y - mLastMotionY);
                         if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
                         if (xDiff > mTouchSlop && xDiff > yDiff) {
                             if (DEBUG) Log.v(TAG, "Starting drag!");
                             mIsBeingDragged = true;
                             requestParentDisallowInterceptTouchEvent(true);
                             mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                                     mInitialMotionX - mTouchSlop;
                             mLastMotionY = y;
                             setScrollState(SCROLL_STATE_DRAGGING);
                             setScrollingCacheEnabled(true);

                             // Disallow Parent Intercept, just in case
                             ViewParent parent = getParent();
                             if (parent != null) {
                                 parent.requestDisallowInterceptTouchEvent(true);
                             }
                         }
                     }
                     // Not else! Note that mIsBeingDragged can be set above.
                     if (mIsBeingDragged) {
                         // Scroll to follow the motion event
                         final int activePointerIndex = MotionEventCompat.findPointerIndex(
                                 ev, mActivePointerId);
                         final float x = MotionEventCompat.getX(ev, activePointerIndex);
                         needsInvalidate |= performDrag(x);
                     }
                     break;
                 case MotionEvent.ACTION_UP:
                     if (mIsBeingDragged) {
                         final VelocityTracker velocityTracker = mVelocityTracker;
                         velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                         int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
                                 velocityTracker, mActivePointerId);
                         mPopulatePending = true;
                         final int width = getClientWidth();
                         final int scrollX = getScrollX();
                         final ItemInfo ii = infoForCurrentScrollPosition();
                         final int currentPage = ii.position;
                         final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
                         final int activePointerIndex =
                                 MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                         final float x = MotionEventCompat.getX(ev, activePointerIndex);
                         final int totalDelta = (int) (x - mInitialMotionX);
                         int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                                 totalDelta);
                         setCurrentItemInternal(nextPage, true, true, initialVelocity);

                         mActivePointerId = INVALID_POINTER;
                         endDrag();
                         needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
                     }
                     break;
                 case MotionEvent.ACTION_CANCEL:
                     if (mIsBeingDragged) {
                         scrollToItem(mCurItem, true, 0, false);
                         mActivePointerId = INVALID_POINTER;
                         endDrag();
                         needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
                     }
                     break;
                 case MotionEventCompat.ACTION_POINTER_DOWN: {
                     final int index = MotionEventCompat.getActionIndex(ev);
                     final float x = MotionEventCompat.getX(ev, index);
                     mLastMotionX = x;
                     mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                     break;
                 }
                 case MotionEventCompat.ACTION_POINTER_UP:
                     onSecondaryPointerUp(ev);
                     mLastMotionX = MotionEventCompat.getX(ev,
                             MotionEventCompat.findPointerIndex(ev, mActivePointerId));
                     break;
             }
             if (needsInvalidate) {
                 ViewCompat.postInvalidateOnAnimation(this);
             }
             return true;
         }
         
         ......
     }

按照ACTION_DOWN, ACTION_MOVE, ACTION_UP顺序分析滑动页面

1,ACTION_DOWN 

  第一次按下时onInterceptTouchEvent先处理

  第一次按下时mIsBeingDragged = false;所以ACTION_DOWN传给ViewPager当前页面子View处理——如:联系人列表,直至ACTION_DOWN处理完

2,ACTION_MOVE

  如果处理ACTION_DOWN时没执行requestParentDisallowInterceptTouchEvent(true);则 onInterceptTouchEvent处理ACTION_MOVE

  

        //判断水平移动,一般canScroll都为false,所以不会进入这里。如果进入,则onInterceptTouchEvent返回false,ACTION_MOVE传递给当前页面中的View处理。
              if (dx != 0 && !isGutterDrag && canScroll) {
                   //isGutterDrag——是否从屏幕边缘滑动, canScroll——ViewPager当前页面中的子View是否支持水平滑动
                   //Contacts中canScroll始终未false, 所以不会进入这里。
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
              }
      
              //判断水平,竖直位移           
              if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                  //mTouchSlop在ViewPager初始化时获得为16dp, 也就是(水平位移>16dp && 水平位移/2>竖直位移)
                    if (DEBUG) Log.v(TAG, "Starting drag!");
                    mIsBeingDragged = true;  //mIsBeingDragged是onInterceptTouchEvent返回的返回值,true表示onTouchEvent要处理ACTION_MOVE,不再往下传递
                    requestParentDisallowInterceptTouchEvent(true);//disallowIntercept设为true,即将发生的ACTION_UP不会进入onInterceptTouchEvent
                    setScrollState(SCROLL_STATE_DRAGGING);
                    mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                            mInitialMotionX - mTouchSlop;
                    mLastMotionY = y;
                    setScrollingCacheEnabled(true);
             } else if (yDiff > mTouchSlop) {
                 //如果不满足(水平位移>16dp && 水平位移/2>竖直位移),但——竖直位移>16dp, onInterceptTouchEvent返回false,ACTION_MOVE传递给当前页面中的View处理
                    // The finger has moved enough in the vertical
                    // direction to be counted as a drag...  abort
                    // any attempt to drag horizontally, to work correctly
                    // with children that have scrolling containers.
                    if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                    mIsUnableToDrag = true;
             }
             if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    if (performDrag(x)) {
                        ViewCompat.postInvalidateOnAnimation(this);//页面滑动
                    }
             }
               
             
             //满足(水平位移>16dp && 水平位移/2>竖直位移)onTouchEvent处理ACTION_MOVE
              if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    final int activePointerIndex = MotionEventCompat.findPointerIndex(
                            ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    needsInvalidate |= performDrag(x);
              }
              
            ACTION_MOVE事件是滑动页面时执行最多的,

3,ACTION_UP
  如果第2步执行requestParentDisallowInterceptTouchEvent(true)并且return rue, 则由onTouchEvent直接处理。

  

case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                    ......//获得要切换的页面
                    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                            totalDelta);
                    //这里实现页面切换        
                    setCurrentItemInternal(nextPage, true, true, initialVelocity);
            ......
                }

  setCurrentItemInternal(....)方法实现页面切换,切换到哪个页面时由determineTargetPage(....)返回的值决定。

      private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
            int targetPage;
            Log.i("antoon", TAG+", determineTargetPage, Math.abs(deltaX) = "+Math.abs(deltaX)+", mFlingDistance = "+mFlingDistance);
            Log.i("antoon", TAG+", determineTargetPage, Math.abs(velocity) = "+Math.abs(velocity)+", mMinimumVelocity = "+mMinimumVelocity);
            if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
                targetPage = velocity > 0 ? currentPage : currentPage + 1;
            } else {            
                final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
                targetPage = (int) (currentPage + pageOffset + truncator);               
            }

            if (mItems.size() > 0) {
                final ItemInfo firstItem = mItems.get(0);
                final ItemInfo lastItem = mItems.get(mItems.size() - 1);

                // Only let the user target pages we have items for
                targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));
            }

            return targetPage;
        }

  经Log输出,mFlingDistance=75, mMinimumVelocity=1200, 所以对于快速滑动要满足 (水平滑动距离>75px && 滑动速率>1200px/s)才会切换页面

      else {//这是对缓慢滑动的处理。  pageOffset决定切换哪个页面 。
                final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;
                targetPage = (int) (currentPage + pageOffset + truncator);
           }

综上ViewPager快速滑动切换页面需要满足条件: 1,(水平位移>16dp && 水平位移/2>竖直位移)触发页面滑动,
                       2,(水平滑动距离>75px && 滑动速率>1200px/s)触发页面切换。

原文地址:https://www.cnblogs.com/antoon/p/4256630.html