launcher 常規切頁:https://blog.csdn.net/a396604593/article/details/125305234
循環切頁
我們知道,launcher切頁是在packages\apps\Launcher3\src\com\android\launcher3\PagedView.java的onTouchEvent中實現的。
1、滑動限制
public boolean onTouchEvent(MotionEvent ev) {case MotionEvent.ACTION_MOVE:mOrientationHandler.setPrimary(this, VIEW_SCROLL_BY, movedDelta);
}
...
//pagedview重寫@Overridepublic void scrollTo(int x, int y) {//注釋掉x y的坐標顯示,讓頁面能切到首頁和末尾繼續下發x y
// x = Utilities.boundToRange(x,
// mOrientationHandler.getPrimaryValue(mMinScroll, 0), mMaxScroll);
// y = Utilities.boundToRange(y,
// mOrientationHandler.getPrimaryValue(0, mMinScroll), mMaxScroll);Log.d(TAG," scrollTo: "+x +" , "+y +" mMinScroll: "+mMinScroll+" mMaxScroll: "+mMaxScroll);super.scrollTo(x, y);}
2、循環切頁時,我們需要手動繪制頁面上去,讓循環切頁看上去和正常切頁一樣
packages\apps\Launcher3\src\com\android\launcher3\Workspace.java
@Overrideprotected void dispatchDraw(Canvas canvas) {boolean restore = false;int restoreCount = 0;boolean fastDraw = //mTouchState != TOUCH_STATE_SCROLLING &&getNextPage() == INVALID_PAGE;if (fastDraw && mIsPageInTransition) {Log.d(TAG," dispatchDraw 666 getScrollX(): "+getScrollX()+" "+mScroller.getCurrX());drawChild(canvas, getChildAt(getCurrentPage()), getDrawingTime());//在非滑動中、非臨界條件的正常情況下繪制屏幕} else{Log.d(TAG," dispatchDraw 000 getScrollX(): "+getScrollX()+" "+mScroller.getCurrX());long drawingTime = getDrawingTime();int width = getWidth()+ 22;float scrollPos = (float) getScrollX() / width;boolean endlessScrolling = true;int leftScreen;int rightScreen;boolean isScrollToRight = false;int childCount = getChildCount();//其值為1、2、3----if (scrollPos < 0 && endlessScrolling) {//屏幕是向左滑到臨界leftScreen = childCount - 1;rightScreen = 0;} else {//屏幕向右滑動到臨界leftScreen = Math.min( (int) scrollPos, childCount - 1 );rightScreen = leftScreen + 1;if (endlessScrolling) {rightScreen = rightScreen % childCount;isScrollToRight = true;}}if (isScreenNoValid(leftScreen)) {if (rightScreen == 0 && !isScrollToRight) { // 向左滑動,如果rightScreen為0int offset = childCount * width;Log.d(TAG," dispatchDraw 111 width: "+width+" getScrollX(): "+getScrollX()+" offset: "+offset);canvas.translate(-offset, 0);drawChild(canvas, getChildAt(leftScreen), drawingTime);canvas.translate(+offset, 0);} else {Log.d(TAG," dispatchDraw 222 width: "+width+" getScrollX(): "+getScrollX());drawChild(canvas, getChildAt(leftScreen), drawingTime);}}if (scrollPos != leftScreen && isScreenNoValid(rightScreen)) {//向右滑動if (endlessScrolling && rightScreen == 0 && isScrollToRight) {int offset = childCount * width;Log.d(TAG," dispatchDraw 333 width: "+width+ " getScrollX(): "+getScrollX()+" offset: "+offset);canvas.translate(+offset, 0);drawChild(canvas, getChildAt(rightScreen), drawingTime);canvas.translate(-offset, 0);} else {Log.d(TAG," dispatchDraw 444 width: "+width+" getScrollX(): "+getScrollX());drawChild(canvas, getChildAt(rightScreen), drawingTime);}}}}//判斷非臨界條件下所在的屏幕,如果是//臨界則返回falseprivate boolean isScreenNoValid(int screen) {return screen >= 0 && screen < getChildCount();}
3、松手后,我們需要讓循環切頁和正常切頁一樣動畫自然切過去
假設一共有 0 1 2 三頁,我們需要從 0 切到 2 3切到 0 ,而不是 0 1 2 , 2 1 0
重新回到launcher切頁是在packages\apps\Launcher3\src\com\android\launcher3\PagedView.java的onTouchEvent
public boolean onTouchEvent(MotionEvent ev) {case MotionEvent.ACTION_UP:int finalPage;// We give flings precedence over large moves, which is why we short-circuit our// test for a large move if a fling has been registered. That is, a large// move to the left and fling to the right will register as a fling to the right.if (((isSignificantMove && !isDeltaLeft && !isFling) ||(isFling && !isVelocityLeft))
// && mCurrentPage > 0 //切到0時繼續走這里,finalPage = -1) {finalPage = returnToOriginalPage? mCurrentPage : mCurrentPage - getPanelCount();snapToPageWithVelocity(finalPage, velocity);} else if (((isSignificantMove && isDeltaLeft && !isFling) ||(isFling && isVelocityLeft))
// &&mCurrentPage < getChildCount() - 1 //切到最后一頁時繼續切頁,finalPage = 4) {finalPage = returnToOriginalPage? mCurrentPage : mCurrentPage + getPanelCount();snapToPageWithVelocity(finalPage, velocity);} else {snapToDestination();}
}
上面修改后,進入snapToPageWithVelocity(finalPage, velocity);這個方法的finalPage值在循環切頁時就會超出 0 1 2,變成 -1 或者4。那么我們需要在snapToPageWithVelocity中繼續處理一下
切頁最終會調用到protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate)
方法,
whichPage和delta是分開的,這就讓0到-1(2)、2 - 3(0)成為可能。
因為scroll本身是一條線,mScroller.startScroll(mOrientationHandler.getPrimaryScroll(this), 0, delta, 0, duration);
關鍵的2個參數是whichPage和delta。
假設 0 到 -1切頁
我們可以給whichPage傳入2,給delta傳入0到-1的值,在切頁結束后,再把頁面瞬移到最后一頁的scroll值。
這樣就完成了循環切頁,并且保證whichPage和delta最終結果正確。
protected boolean snapToPageWithVelocity(int whichPage, int velocity) {//緩慢滑動if (Math.abs(velocity) < mMinFlingVelocity) {// If the velocity is low enough, then treat this more as an automatic page advance// as opposed to an apparent physical response to flingingreturn snapToPage(whichPage, mPageSnapAnimationDuration);}//快速滑動Log.d(TAG," snapToPageWithVelocity whichPage 111: "+whichPage);//循環切頁頁面數修正boolean isLoopLeft = false;boolean isLoopRight = false;if (whichPage ==-1){whichPage = getPageCount() -1;isLoopLeft = true;}if (whichPage == getPageCount()){whichPage = 0;isLoopRight = true;}Log.d(TAG," snapToPageWithVelocity whichPage 222: "+whichPage);whichPage = validateNewPage(whichPage);int halfScreenSize = mOrientationHandler.getMeasuredSize(this) / 2;//關鍵在這里,newLoc的值int newLoc = getScrollForPage(whichPage,isLoopLeft,isLoopRight);int delta = newLoc - mOrientationHandler.getPrimaryScroll(this);Log.d(TAG," snapToPageWithVelocity whichPage 666 delta: "+delta);int duration = 0;}//重寫getScrollForPage方法,根據isLoopLeft和isLoopRight計算滾動坐標public int getScrollForPage(int index ,boolean isLoopLeft,boolean isLoopRight) {Log.d(TAG," getScrollForPage 111 index: "+index);if (isLoopLeft){Log.d(TAG," getScrollForPage 222 index: "+index);return -mPageScrolls[1];}if (isLoopRight){Log.d(TAG," getScrollForPage 333 index: "+index);return mPageScrolls[1] * (mPageScrolls.length) ;}return getScrollForPage(index);}public int getScrollForPage(int index) {// TODO(b/233112195): Use !pageScrollsInitialized() instead of mPageScrolls == null, once we// root cause where we should be using runOnPageScrollsInitialized().if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) {return 0;} else {return mPageScrolls[index];}}
緩慢滑動直接調用的return snapToPage(whichPage, mPageSnapAnimationDuration);
還需要額外處理一下滾動坐標
protected boolean snapToPage(int whichPage, int duration, boolean immediate) {//循環切頁頁面數修正//這段代碼很蠢,快速滑動和緩慢滑動有相同的邏輯,但是沒有提煉出來,寫了兩遍Log.d(TAG," snapToPage whichPage 111: "+whichPage);boolean isLoopLeft = false;boolean isLoopRight = false;if (whichPage ==-1){whichPage = getPageCount() -1;isLoopLeft = true;}if (whichPage == getPageCount()){whichPage = 0;isLoopRight = true;}Log.d(TAG," snapToPage whichPage 222: "+whichPage);whichPage = validateNewPage(whichPage);Log.d(TAG," snapToPage whichPage 333: "+whichPage);//關鍵在這里,newLoc的值int newLoc = getScrollForPage(whichPage,isLoopLeft,isLoopRight);final int delta = newLoc - mOrientationHandler.getPrimaryScroll(this);Log.d(TAG," snapToPage whichPage 666 delta: "+delta);return snapToPage(whichPage, delta, duration, immediate);}
4、onPageEndTransition 頁面切換結束后,修正scroll值
packages\apps\Launcher3\src\com\android\launcher3\Workspace.java
protected void onPageEndTransition() {super.onPageEndTransition();updateChildrenLayersEnabled();if (mDragController.isDragging()) {if (workspaceInModalState()) {// If we are in springloaded mode, then force an event to check if the current touch// is under a new page (to scroll to)mDragController.forceTouchMove();}}if (mStripScreensOnPageStopMoving) {stripEmptyScreens();mStripScreensOnPageStopMoving = false;}// Inform the Launcher activity that the page transition ended so that it can react to the// newly visible page if it wants to.mLauncher.onPageEndTransition();//頁面切換結束后,修正scroll值Log.d(TAG," snapToPage whichPage 777 getNextPage(): "+getNextPage()+" getScrollX(): "+getScrollX()+" "+mMaxScroll+" "+mMinScroll);if(getScrollX()< mMinScroll || getScrollX() > mMaxScroll){Log.e(TAG," snapToPage snapToPageImmediately 888 getNextPage(): "+getNextPage());snapToPageImmediately(getNextPage());}}
以上基本上完成了循環切頁的功能。
5、循環切頁不跟手
假設0 到-1切頁,0頁繼續向右滑動,可以跟手,但是向左滑動頁面不動。
排查滑動問題。
發現workspace中dispatchDraw里面的getScrollX拿到的值不變。
滾動值是PagedView#scrollTo回調回來的。懷疑PagedView#onTouchEvent 中move時傳入的值有問題。
打斷點發現走入了邊緣回彈邏輯,delta值被改了。
float direction = mOrientationHandler.getPrimaryValue(dx, dy);float delta = mLastMotion + mLastMotionRemainder - direction;Log.d(TAG," ACTION_MOVE 111 delta: "+delta);int width = getWidth();int height = getHeight();int size = mOrientationHandler.getPrimaryValue(width, height);final float displacement = mOrientationHandler.getSecondaryValue(dx, dy)/ mOrientationHandler.getSecondaryValue(width, height);mTotalMotion += Math.abs(delta);if (mAllowOverScroll) {//注釋掉邊緣回彈效果的坐標修正
// float consumed = 0;
// if (delta < 0 && mEdgeGlowRight.getDistance() != 0f) {
// consumed = size * mEdgeGlowRight.onPullDistance(delta / size, displacement);
// } else if (delta > 0 && mEdgeGlowLeft.getDistance() != 0f) {
// consumed = -size * mEdgeGlowLeft.onPullDistance(
// -delta / size, 1 - displacement);
// }
// delta -= consumed;}delta /= mOrientationHandler.getPrimaryScale(this);Log.d(TAG," ACTION_MOVE 222 delta: "+delta);// Only scroll and update mLastMotionX if we have moved some discrete amount. We// keep the remainder because we are actually testing if we've moved from the last// scrolled position (which is discrete).mLastMotion = direction;int movedDelta = (int) delta;mLastMotionRemainder = delta - movedDelta;if (delta != 0) {Log.d(TAG," ACTION_MOVE movedDelta: "+movedDelta);mOrientationHandler.setPrimary(this, VIEW_SCROLL_BY, movedDelta);
尾注
以上基本上實現了循環切頁功能。自己寫的demo功能,自測ok了,有bug后面再改。