Summary
The sliding of a ViewGroup must be related to its dispatchTouchEvent(), onInterceptTouchEvent(), onTouchEvent(). ViewPager rewrote the latter two. Let's look at them one by one. First of all, there are two ways for ViewPager to generate view movement based on gesture. One is to drag with finger when MOVE, the other is to slide to the specified page after UP, and the sliding is realized by Scroller + computeScroll().
onInterceptTouchEvent()
Viewpager has an mScrollState member that maintains the status of the ViewPager's current page, which may be assigned three values.
/** * Indicates that the pager is in an idle, settled state. The current page * is fully in view and no animation is in progress. */ //Current page is idle, no animation public static final int SCROLL_STATE_IDLE = 0; /** * Indicates that the pager is currently being dragged by the user. */ //Being dragged public static final int SCROLL_STATE_DRAGGING = 1; /** * Indicates that the pager is in the process of settling to a final position. */ //Moving towards the final position public static final int SCROLL_STATE_SETTLING = 2;
As the onInterceptTouchEvent() comment says, the method is simply to determine whether we should intercept the Touch event, and scrolling is left to onTouchEvent(). Because there are break s in every branch of switch, I changed the order of source code and put DOWN in front of MOVE, which is more clear.
@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 a set of gestures ends, return false if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the drag. if (DEBUG) Log.v(TAG, "Intercept done!"); resetTouch(); return false; } // Nothing more to do here if we have decided whether or not we // are dragging. if (action != MotionEvent.ACTION_DOWN) { //If it's being drag ged, intercept if (mIsBeingDragged) { if (DEBUG) Log.v(TAG, "Intercept returning true!"); return true; } //No drag, no interception if (mIsUnableToDrag) { if (DEBUG) Log.v(TAG, "Intercept returning false!"); return false; } } switch (action) { case MotionEvent.ACTION_DOWN: { /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ //Re-assign these four variables to indicate the beginning of a set of gestures mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); //Get the id of the first touch point mActivePointerId = ev.getPointerId(0); //Set allowable drag-and-drop to false mIsUnableToDrag = false; //Mark start scrolling mIsScrollStarted = true; //This Scroller was created in initViewPager(), where the x,y values in //Scroller are calculated manually. mScroller.computeScrollOffset(); //If we are moving towards the final state at this time, and there is still a certain distance from the final position. if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { // Let the user 'catch' the pager as it animates. //Let users grab the Pager //Stop scrolling mScroller.abortAnimation(); //??? mPopulatePending = false; //Update cached page information (mCurItem changes when scrolling?) populate(); //Represents dragging mIsBeingDragged = true; //Parent ViewGroup is not allowed to intercept requestParentDisallowInterceptTouchEvent(true); //Setting up a new state setScrollState(SCROLL_STATE_DRAGGING); } else { //What I understand here is that we are approaching the final state and the distance is small enough to interfere with the motion. completeScroll(false); mIsBeingDragged = false; } .... break; } 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. */ //The annotations are very clear. If you can enter this place, it means mIsBeingDragged == false //Here, check if the user has moved far enough from the original location to assign a value to mIsBeingDragged //id of the first touch point final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } //I don't quite understand. It looks like I made a conversion for the first touch point id. final int pointerIndex = ev.findPointerIndex(activePointerId); //Touch point abscissa final float x = ev.getX(pointerIndex); //Transverse migration final float dx = x - mLastMotionX; //Absolute value of lateral migration final float xDiff = Math.abs(dx); //portrait final float y = ev.getY(pointerIndex); final float yDiff = Math.abs(y - mInitialMotionY); ... //This means that if the sub-View can slide in this area, it is handed over to the sub-View and not intercepted. //The source code for canScroll is posted at the back, looking for slidable ones in the sub-View if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } //If the absolute value of lateral migration is greater than the minimum value and yDiff/xDiff < 0.5f if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); //Intercept! mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); //Save the current location mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollingCacheEnabled(true); } else if (yDiff > mTouchSlop) { //If enough distance is moved lengthwise, no interception is allowed. // 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 dragging is possible, dragging is produced here, which is very important. This is analyzed later. if (performDrag(x)) { ViewCompat.postInvalidateOnAnimation(this); } } break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } ... return mIsBeingDragged; }
Traversing through the sub-View, the contact is within the boundaries of the sub-View, and the sub-View can slide back to true
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { // TODO: Add versioned support here for transformed views. // This will not work for transformed views in Honeycomb+ final View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } return checkV && ViewCompat.canScrollHorizontally(v, -dx); }
performDrag()
The previous onInterceptTouchEvent() judged many situations, basically judging drag according to the situation, and then assigning the variable mIsBeingDragged to represent whether to intercept the next series of gestures. At the end of MOVE, when drag is available, we will go into this method to let the page follow the finger gesture.
private boolean performDrag(float x) { boolean needsInvalidate = false; final float deltaX = mLastMotionX - x; mLastMotionX = x; float oldScrollX = getScrollX(); //ViewPager's view abscissa float scrollX = oldScrollX + deltaX; final int width = getClientWidth(); //Sub-View left boundary and sub-View right boundary float leftBound = width * mFirstOffset; float rightBound = width * mLastOffset; boolean leftAbsolute = true; boolean rightAbsolute = true; //Current first and last page information final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); //If the first page information is not item 0 of the data, update the leftBound if (firstItem.position != 0) { leftAbsolute = false; leftBound = firstItem.offset * width; } //Empathy if (lastItem.position != mAdapter.getCount() - 1) { rightAbsolute = false; rightBound = lastItem.offset * width; } //boundary condition if (scrollX < leftBound) { if (leftAbsolute) { float over = leftBound - scrollX; needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); } scrollX = leftBound; } else if (scrollX > rightBound) { if (rightAbsolute) { float over = scrollX - rightBound; needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); } scrollX = rightBound; } // Don't lose the rounded component mLastMotionX += scrollX - (int) scrollX; //Sliding View scrollTo((int) scrollX, getScrollY()); //Important methods pageScrolled((int) scrollX); return needsInvalidate; }
pageScrolled()
The performDrag() method slides the view of the ViewPager (through the scrollTo() method) and calls the method. Now let's look at this method. Overall, this approach does a number of things:
1. The current page information is obtained from scrollX of the view.
2. The ratio of sliding distance and pixels are calculated.
3.onPageScrolled(currentPage, pageOffset, offsetPixels)
Let's say that if sliding causes two Pages to be displayed in the ViewPager display area, infoForCurrentScrollPosition() returns the ItemInfo on the left.
private boolean pageScrolled(int xpos) { //The incoming parameter refers to the distance from which the ViewPager view slides. ... //Get the page information that should be displayed according to scrollX final ItemInfo ii = infoForCurrentScrollPosition(); final int width = getClientWidth(); final int widthWithMargin = width + mPageMargin; final float marginOffset = (float) mPageMargin / width; //The position of the current page (this position refers to the position in the data list) final int currentPage = ii.position; //Here we calculate the ratio of the sliding distance of the current page view to the width of the current page. //If the second item is 1, it represents the ratio of the sliding distance (and page width) of the current page view. final float pageOffset = (((float) xpos / width) - ii.offset) / (ii.widthFactor + marginOffset); //Pixels representing view sliding final int offsetPixels = (int) (pageOffset * widthWithMargin); mCalledSuper = false; onPageScrolled(currentPage, pageOffset, offsetPixels); if (!mCalledSuper) { throw new IllegalStateException( "onPageScrolled did not call superclass implementation"); } return true; }
onPageScrolled(currentPage, pageOffset, offsetPixels)
This method is called in many scrolling places. We can rewrite this method to achieve some animation effects. Throughout this approach, several things have been done:
1. Scroll DecorView
2. The onPageScrolled() of the callback interface is the interface we added ourselves.
3. Realizing animation
protected void onPageScrolled(int position, float offset, int offsetPixels) { // Offset any decor views if needed - keep them on-screen at all times. //1 if (mDecorChildCount > 0) { final int scrollX = getScrollX(); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); final int width = getWidth(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) continue; final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; int childLeft = 0; switch (hgrav) { default: childLeft = paddingLeft; break; case Gravity.LEFT: childLeft = paddingLeft; paddingLeft += child.getWidth(); break; case Gravity.CENTER_HORIZONTAL: childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); break; case Gravity.RIGHT: childLeft = width - paddingRight - child.getMeasuredWidth(); paddingRight += child.getMeasuredWidth(); break; } childLeft += scrollX; final int childOffset = childLeft - child.getLeft(); if (childOffset != 0) { child.offsetLeftAndRight(childOffset); } } } //2 dispatchOnPageScrolled(position, offset, offsetPixels); //3 if (mPageTransformer != null) { final int scrollX = getScrollX(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.isDecor) continue; final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); mPageTransformer.transformPage(child, transformPos); } } mCalledSuper = true; }
To sum up, mIsBeingDragged is assigned in onInterceptTouchEvent() according to different conditions. If it is slidable in MOVE, scrollTo is called to slide the view to form drag effect. Then pageScrolled() gets the information and offset of the current page and passes it to onPageScrolled(), onPageScrolled() moves Decor View and calls back the interface. Generate animation.
onInterceptTouchEvent() produces drag effect, but the main thing is to judge whether to intercept. Next, let's look at onTouchEvent().
onTouchEvent()
Throughout the whole method, MOVE is still dragging, while UP calculates the next page nextPage according to the current page, offset of the current page, speed and lateral movement distance, and then calls setCurrent Item Internal () to generate sliding.
@Override public boolean onTouchEvent(MotionEvent ev) { //Some judgments, omissions ... mVelocityTracker.addMovement(ev); final int action = ev.getAction(); boolean needsInvalidate = false; switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { //Immediately set x and y in Scroller to final values mScroller.abortAnimation(); mPopulatePending = false; //Update page information that needs to be cached according to mCurIndex populate(); // Remember where the motion event started //Record mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = ev.getPointerId(0); break; } case MotionEvent.ACTION_MOVE: //If it's not in drag (this may be because there's no gesture-consuming sub-View, so go back and let the ViewPager process it) if (!mIsBeingDragged) { final int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { // A child has consumed some touch events and put us into an inconsistent // state. needsInvalidate = resetTouch(); break; } //Calculating lateral and vertical migration final float x = ev.getX(pointerIndex); final float xDiff = Math.abs(x - mLastMotionX); final float y = ev.getY(pointerIndex); final float yDiff = Math.abs(y - mLastMotionY); //If the lateral migration is large enough and the lateral migration is larger than the longitudinal migration, drag can be started. 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 you can drag if (mIsBeingDragged) { // Scroll to follow the motion event final int activePointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(activePointerIndex); //Implementing drag, which has been analyzed on performance Drag needsInvalidate |= performDrag(x); } break; case MotionEvent.ACTION_UP: //If it is dragged up if (mIsBeingDragged) { //Calculating x-velocity final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( velocityTracker, mActivePointerId); mPopulatePending = true; //ViewPager display width final int width = getClientWidth(); //View Lateral Sliding Distance final int scrollX = getScrollX(); //Calculate the current page information according to scrollX. final ItemInfo ii = infoForCurrentScrollPosition(); //Marginal proportions final float marginOffset = (float) mPageMargin / width; //Location of the current page in the data list final int currentPage = ii.position; //Calculate the current page offset final float pageOffset = (((float) scrollX / width) - ii.offset) / (ii.widthFactor + marginOffset); //Transverse migration final int activePointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(activePointerIndex); final int totalDelta = (int) (x - mInitialMotionX); //Determine the page where the final state is after up int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); //Important. Here's the key to sliding. setCurrentItemInternal(nextPage, true, true, initialVelocity); needsInvalidate = resetTouch(); } break; ... case MotionEventCompat.ACTION_POINTER_DOWN: { final int index = MotionEventCompat.getActionIndex(ev); final float x = ev.getX(index); //Multi-touch, change another finger and update mLastMotionX and mActivePointerId mLastMotionX = x; mActivePointerId = ev.getPointerId(index); break; } case MotionEventCompat.ACTION_POINTER_UP: //Apparently the next finger is raised by multi-touch, to update mLastMotionX onSecondaryPointerUp(ev); mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); break; } if (needsInvalidate) { ViewCompat.postInvalidateOnAnimation(this); } return true; }
setCurrentItemInternal()
The purpose of this method is to determine whether onMeasure() or scrollToItem() should be given to complete the page's set.
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { //Some robustness judgments, omitted ... final int pageLimit = mOffscreenPageLimit; //With regard to skipping sliding, we will not update the pages involved in the process of skipping. if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { // We are doing a jump by more than one page. To avoid // glitches, we want to keep all current pages in the view // until the scroll ends. for (int i = 0; i < mItems.size(); i++) { mItems.get(i).scrolling = true; } } final boolean dispatchSelected = mCurItem != item; //I remember when we started our last article, we were going to get into this branch, but it's not going to happen here. if (mFirstLayout) { mCurItem = item; if (dispatchSelected) { dispatchOnPageSelected(item); } requestLayout(); } else {//Here we update the page information and slide to the target page. populate(item); scrollToItem(item, smoothScroll, velocity, dispatchSelected); } }
scrollToItem()
Depending on whether it is smoothScroll for different sliding, smoothScrollTo() or direct scrollTo().
private void scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected) { final ItemInfo curInfo = infoForPosition(item); int destX = 0; if (curInfo != null) { final int width = getClientWidth(); //Calculating offset destX = (int) (width * Math.max(mFirstOffset, Math.min(curInfo.offset, mLastOffset))); } //If it is smooth sliding if (smoothScroll) { //Later specific analysis smoothScrollTo(destX, 0, velocity); //Callback interface if (dispatchSelected) { dispatchOnPageSelected(item); } } else { //Callback interface if (dispatchSelected) { dispatchOnPageSelected(item); } //End sliding directly with scrollTo completeScroll(false); scrollTo(destX, 0); pageScrolled(destX); } }
smoothScrollTo()
void smoothScrollTo(int x, int y, int velocity) { ... //x-Axis Sliding Starting Position int sx; boolean wasScrolling = (mScroller != null) && !mScroller.isFinished(); //If you're scrolling at this point if (wasScrolling) { //Update start location sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX(); mScroller.abortAnimation(); setScrollingCacheEnabled(false); } else { sx = getScrollX(); } //y-axis sliding starting position int sy = getScrollY(); int dx = x - sx; int dy = y - sy; if (dx == 0 && dy == 0) { completeScroll(false); populate(); setScrollState(SCROLL_STATE_IDLE); return; } setScrollingCacheEnabled(true); setScrollState(SCROLL_STATE_SETTLING); final int width = getClientWidth(); final int halfWidth = width / 2; //Sliding distance, distance affects time final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio); //Sliding time int duration; velocity = Math.abs(velocity); if (velocity > 0) { duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); } else { final float pageWidth = width * mAdapter.getPageWidth(mCurItem); final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); duration = (int) ((pageDelta + 1) * 100); } duration = Math.min(duration, MAX_SETTLE_DURATION); // Reset the "scroll started" flag. It will be flipped to true in all places // where we call computeScrollOffset(). mIsScrollStarted = false; //The important thing is to use Scroller to generate elastic sliding. mScroller.startScroll(sx, sy, dx, dy, duration); //Redraw for callback ViewCompat.postInvalidateOnAnimation(this); }
computeScroll()
From the previous analysis, we know that ViewPager uses Scroller to generate conversational sliding. The key of Scroller to generate elastic sliding is that it calls back computeScroll() in onDraw(), then slides with scrollTo() in this method and applies for redrawing again. ViewPager rewrites this method, and after calling scrollTo(), pageScrolled(x) is also called to update Decor View, call back interface, and generate animation. Then apply for redrawing.
@Override public void computeScroll() { mIsScrollStarted = true; if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); if (oldX != x || oldY != y) { scrollTo(x, y); if (!pageScrolled(x)) { mScroller.abortAnimation(); scrollTo(0, y); } } // Keep on drawing until the animation has finished. ViewCompat.postInvalidateOnAnimation(this); return; } // Done with scroll, clean up state. completeScroll(true); }
Summary
ViewPager's drag and slide are all finished, drag is responded to in onInterceptTouchEvent() and onTouchEvent() Move, scrollTo method is used to form the view movement, and pageScrolled() is used to complete the processing of related things, including Decor View, interface method callback, animation; Up of onTouchEvent() may produce smooth sliding, using initialization time. Scroller defined.
Here we have a general understanding of the mobile process of ViewPager's view, and some details can be quickly positioned. Next, I want to look at the interaction between ViewPager and Fragment.