Android13 launcher循环切页

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重写
    @Override
    public 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

@Override
    protected 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()+ mPageSpacing;
            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为0
                    int 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);
                }
            }
        }
    }
    //判断非临界条件下所在的屏幕,如果是//临界则返回false
    private 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 flinging
            return 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后面再改。

bug1:

快速切页的时候,比如从 0 到-1 ,再从-1 到0. 因为scroll了负的位置,在onPageEndTransition才去修复到正常坐标,连续的滑动会在错的坐标上去滑动,导致松手后滚动动画异常。

问题分析

bug原因,因为松手后的滚动是超出正常范围的,为了能够让循环滑动看起来自然,之前的方案是:步骤3和步骤4。
也就是先切页到超出范围的页面,然后在onPageEndTransition 里调整滚动值,保证下次切页正确。
但是这样就必须等循环切页完全结束,不然连续滑动会因为没有走onPageEndTransition导致后续切页异常。

解决思路

在onPageEndTransition 里调整scrollX太慢了,我们需要把修复时机提前。
假设有0 1 2 三页,从0到2的过程分解:
以前的思路是:跟手0到-0.5,松手-0.5滚动到-1,切页结束后设置scrollX为2。
现在的思路:跟手0到-0.5,松手的瞬间设置scrollX的值为2.5,然后2.5到2。把它看做是2到3,切页失败回到2的过程。
这样就不需要等onPageEndTransition才修复ScrollX。上面的第四步也就不需要了。

步骤3 优化后的方案
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.
                    float loopX = 0;
                    if (((isSignificantMove && !isDeltaLeft && !isFling) ||
                            (isFling && !isVelocityLeft))
//                            && mCurrentPage > 0
                    ) {
                        finalPage = returnToOriginalPage
                                ? mCurrentPage : mCurrentPage - getPanelCount();
						//如果切页成功,并且mCurrentPage <= 0,也就是0到-1
                        if (!returnToOriginalPage && mCurrentPage <= 0 ){
                            //根据delta计算loopX
                            loopX = -delta + mPageScrolls[1] * getChildCount();
                            setScrollX((int) loopX);//强行设置loopX
                            finalPage = getChildCount() -1;//修改finalPage
                        }
                        snapToPageWithVelocity(finalPage, velocity);
                        Log.d(TAG," ACTION_UP finalPage 111: "+finalPage+" loopX: "+loopX+" delta: "+delta+" mPageScrolls[1]: "+mPageScrolls[1]);

                    } else if (((isSignificantMove && isDeltaLeft && !isFling) ||
                            (isFling && isVelocityLeft))
//                            &&mCurrentPage < getChildCount() - 1
                    ) {
                        finalPage = returnToOriginalPage
                                ? mCurrentPage : mCurrentPage + getPanelCount();
                        //
                        if (!returnToOriginalPage && mCurrentPage >= getChildCount() - 1){
                            //根据delta计算loopX
                            loopX = - mPageScrolls[1] - delta ;
                            setScrollX((int) loopX);//强行设置loopX
                            finalPage = 0;//修改finalPage
                        }
                        snapToPageWithVelocity(finalPage, velocity);
                        Log.d(TAG," ACTION_UP finalPage 222: "+finalPage+" loopX: "+loopX+" delta: "+delta+" mPageScrolls[1]: "+mPageScrolls[1]);

                    } else {
                        snapToDestination();
                    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值