如何實現兩個ViewPager的聯動
以前寫過一篇文章,講的是如何實現zaker5.0的引導界面效果,見 仿zaker最新版本引導界面的視圖聯動效果(修改viewpager實現) ,沒有寫完就了事了,這篇文章算是對那篇的繼續。
我們先來看看最終效果:
聯動ViewPager的意思就是當一個viewpager在滑動的時候,另外一個ViewPager也跟著滑動,而且兩者是同步的。
如果ViewPager有關于移動距離的回調接口,這事兒就好辦了,遺憾的是沒有,只有一個OnPageChangeListener,我試過在OnPageChangeListener中根據onPageScrolled(int position, float positionOffset, int positionOffsetPixels)的參數來做,但是失敗了。
那就只有自定義ViewPager了。
我直接將ViewPager的源碼沖v4中拿出來,去掉不必要的一些東西,直到不會再出現找不到類為止,
除了需要將ViewPager拿出來之外,還需要把相關的PagerAdapter類也拿出來,不然ViewPager使用的是自己的而adapter用的是v4中的,可能會出問題。
為了實現聯動,在ViewPager中增加一個private變量mFollowViewPager
(同時增加變量的set方法):
private ViewPager mFollowViewPager; public void setFlolwViewPager(ViewPager page){ mFollowViewPager = page; }
mFollowViewPager
表示的是隨著當前ViewPager滾動的另一個ViewPager。
我的想法是在當前ViewPager滾動的相關代碼處,調用mFollowViewPager
的scrollTo方法。 那么在哪里加入比較好呢,經過仔細跟蹤ViewPager的行為,我發現當手指未松開的時候,performDrag方法處理相關的移動,他調用了自己的scrollTo來實現自身的平移,因此我們只需要在performDrag方法中加入如下代碼:
//add by jcodecraeer final float pageOffset = scrollX / width; if(mFollowViewPager!=null){ mFollowViewPager.scrollTo( (int)(pageOffset*mFollowViewPager.getWidth()), mFollowViewPager.getScrollY()); }
注意,并不是主ViewPager移動了多遠,mFollowViewPager
就移動多遠,因為兩個ViewPager的寬度可能不一樣,所以需要轉換一下,上面的代碼中final float pageOffset = scrollX / width;
pageOffset
就只轉換得到的值。
改寫后的performDrag
如下:
private boolean performDrag(float x) { boolean needsInvalidate = false; final float deltaX = mLastMotionX - x; mLastMotionX = x;float oldScrollX = getScrollX(); float scrollX = oldScrollX + deltaX; final int width = getWidth(); float leftBound = width * mFirstOffset; float rightBound = width * mLastOffset; boolean leftAbsolute = true; boolean rightAbsolute = true; final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); if (firstItem.position != 0) { leftAbsolute = false; leftBound = firstItem.offset * width; } if (lastItem.position != mAdapter.getCount() - 1) { rightAbsolute = false; rightBound = lastItem.offset * width; } 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; scrollTo((int) scrollX, getScrollY()); pageScrolled((int) scrollX); //add by jcodecraeer final float pageOffset = scrollX / width; if(mFollowViewPager!=null){ mFollowViewPager.scrollTo( (int)(pageOffset*mFollowViewPager.getWidth()), mFollowViewPager.getScrollY()); } return needsInvalidate;
}</pre>
光處理了手指未離開屏幕階段的平移還不夠,手指松開了,ViewPager還會自己繼續一定一段距離,因此mFollowViewPager也應該跟著移動,我們想下,手指松開是不是該在 case MotionEvent.ACTION_UP中處理的呢?
我們找到相關代碼:
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 = getWidth(); 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(); }</pre> <p>其中,<code class="js spaces"></code><code class="js plain">setCurrentItemInternal(nextPage, </code><code class="js keyword">true</code><code class="js plain">, </code><code class="js keyword">true</code><code class="js plain">, initialVelocity)</code>是關鍵,他的代碼如下:</p>
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { if (mAdapter == null || mAdapter.getCount() <= 0) { setScrollingCacheEnabled(false); return; } if (!always && mCurItem == item && mItems.size() != 0) { setScrollingCacheEnabled(false); return; } if (item < 0) { item = 0; } else if (item >= mAdapter.getCount()) { item = mAdapter.getCount() - 1; } final int pageLimit = mOffscreenPageLimit; 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; populate(item); scrollToItem(item, smoothScroll, velocity, dispatchSelected); }可以看到
調用了
setCurrentItemInternal
中
scrollToItem(item, smoothScroll, velocity, dispatchSelected);來實現手指松開后的繼續平移效果。也就是說對于
mFollowViewPager
,如果我們也同樣調用setCurrentItemInternal
就可以使他也跟著移動了。照著這個思路我們改寫case MotionEvent.ACTION_UP的代碼段:
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 = getWidth(); 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); //add by jcodecraeer if(mFollowViewPager!=null){ mFollowViewPager.setCurrentItemInternal(nextPage, true, true, initialVelocity); } mActivePointerId = INVALID_POINTER; endDrag(); needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); }
至此,我們完成了所有的修改,其實也沒改幾行。
那么在activity中如何使用改造后的ViewPager讓兩個ViewPager聯動呢?假設有一個是mViewPager,有一個是mFollowViewPager,我想讓
mFollowViewPager
隨著mViewPager
動,則:mPager.setFollowViewPager(mFollowViewPager);
需要注意的是在我接下來給出的demo中,我屏蔽了的所有觸摸事件,讓主ViewPager覆蓋在
followViewPager
followViewPager之上,這跟我要實現的效果穩合的。如果你要讓
也能反過來使主ViewPager也能跟著移動不妨反過來調用:
followViewPager
mFollowViewPager.setFollowViewPager(mPager);
但是我不確定這種雙向調用是否會出現問題,因為我并沒有很嚴格的考慮從mFollowViewPager
變量在移動過后本應該導致的一些狀態變化(比如相關的變量)。讀者可以試一試,然后改進。
源碼下載地址:http://jcodecraeer.com/a/opensource/2014/1031/1885.html
關于ViewPager被改造的地方都用add by jcodecraeer 標注(不包括為了刪除的那些不必要的代碼)