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()+ 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为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才去修复到正常坐标,连续的滑动会在错的坐标上去滑动,导致松手后滚动动画异常。