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后面再改。