Launcher3的workspace滑动分析
白智
2023-12-01
Workspace是PagedView的子类,而实际上滑动功能的实现是在PagedView中实现的。在Launcher中,Workspace中有五个CellLayout,分别代表五个分屏。
当左右拖动CellLayout时,就能实现滑动的效果。但实际上CellLayout中还有其他的子View,PagedView是如何避免了来自子View的干扰的呢?这里就需要讨论另一个问题,Android对touch事件的拦截制度。而拦截发生在ViewGroup的onInterceptTouchEvent()和onTouchEvent()以及View的onTouchEvent中。当发生touch事件时,系统会产生一个MotionEvent并且沿着View Tree开始传递。首先获取MotionEvent是View Tree的根节点,根节点通常是一个ViewGroup,ViewGroup将在onInterceptTouchEvent()中获取MotionEvent并决定是否继续向下传递。当在ViewGroup.onInterceptEvent()中返回true时,将截获MotionEvent,View Tree下面的View将无法获得MotionEvent,转而交给当前ViewGroup的onTouchEvent()方法。如果onTouchEvent中返回false,那么MotionEvent将沿着View Tree向上传给上一层。
在Launcher的View tree中,从上到下的主要的节点有,DragLayer,Workspace,CellLayout。DragLayer层的主要任务是负责对图标和AppWidget进行拖拽,Workspace则主要负责左右滑动,CellLayout则用于容纳各种桌面的快捷方式.
滑动功能主要分两步:1、在onInterceptTouchEvent中进行拦截。2、在onTouchEvent中进行滑动。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (DISABLE_TOUCH_INTERACTION) {
return false;
}
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onTouchEvent will be called and we do the actual
* scrolling there.
*/
acquireVelocityTrackerAndAddMovement(ev);
// Skip touch handling if there are no pages to swipe
if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev);
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
Log.i("Garment1111", "onInterceptTouchEvent---11111mTouchState:"+mTouchState);
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) &&
(mTouchState == TOUCH_STATE_SCROLLING)) { //貌似不会进来这里
Log.i("Garment1111", "onInterceptTouchEvent---ACTION_MOVE11111");
return true;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: { //按下后,手指在屏幕下滑动触发move事件
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
Log.i("Garment1111", "onInterceptTouchEvent---ACTION_MOVE2222222");
if (mActivePointerId != INVALID_POINTER) {
determineScrollingStart(ev); //在这个函数里面判断手指滑动是否拦截,若拦截则设置//mTouchState=TOUCH_STATE_SCROLLING, 在函数最后return时,就会return true,进行事件拦截。
Log.i("Garment1111", "onInterceptTouchEvent---22222mTouchState:"+mTouchState);
}
// if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
// event. in that case, treat the first occurence of a move event as a ACTION_DOWN
// i.e. fall through to the next case (don't break)
// (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events
// while it's small- this was causing a crash before we checked for INVALID_POINTER)
break;
}
case MotionEvent.ACTION_DOWN: { //当手指按下时,首先触发该事件
Log.i("Garment1111", "onInterceptTouchEvent---ACTION_DOWN11111");
final float x = ev.getX();
final float y = ev.getY();
// Remember location of down touch
mDownMotionX = x;
mDownMotionY = y;
mDownScrollX = getScrollX();
mLastMotionX = x;
mLastMotionY = y;
float[] p = mapPointFromViewToParent(this, x, y);
mParentDownMotionX = p[0];
mParentDownMotionY = p[1];
mLastMotionXRemainder = 0;
mTotalMotionX = 0;
mActivePointerId = ev.getPointerId(0);
/*
* If being flinged and user touches the screen, initiate drag;
* otherwise don't. mScroller.isFinished should be false when
* being flinged.
*/
final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX());
final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop / 3);
if (finishedScrolling) {
mTouchState = TOUCH_STATE_REST;
if (!mScroller.isFinished() && !mFreeScroll) {
setCurrentPage(getNextPage());
pageEndMoving();
}
} else {
if (isTouchPointInViewportWithBuffer((int) mDownMotionX, (int) mDownMotionY)) {
mTouchState = TOUCH_STATE_SCROLLING;
} else {
mTouchState = TOUCH_STATE_REST;
}
}
// check if this can be the beginning of a tap on the side of the pages
// to scroll the current page
if (!DISABLE_TOUCH_SIDE_PAGES) {
if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) {
if (getChildCount() > 0) {
if (hitsPreviousPage(x, y)) {
mTouchState = TOUCH_STATE_PREV_PAGE;
} else if (hitsNextPage(x, y)) {
mTouchState = TOUCH_STATE_NEXT_PAGE;
}
}
}
}
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
resetTouchState();
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
releaseVelocityTracker();
break;
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/
Log.i("Garment1111", "onInterceptTouchEvent---return:"+(mTouchState != TOUCH_STATE_REST));
return mTouchState != TOUCH_STATE_REST; //当返回true时,对事件进行拦截。
}
简单分析该函数,当手指按下屏幕时,会触发down事件,手指滑动时,会触发move事件,在move事件中,determineScrollingStart(ev)这个函数会进行判断手指是否滑动,并决定是否拦截move事件,拦截则把mTouchState设置为TOUCH_STATE_SCROLLING,在函数最后return mTouchState != TOUCH_STATE_REST;就是返回true,表示拦截move事件。
下面是onTouchEvent()函数:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (DISABLE_TOUCH_INTERACTION) {
return false;
}
super.onTouchEvent(ev);
// Skip touch handling if there are no pages to swipe
if (getChildCount() <= 0) return super.onTouchEvent(ev);
acquireVelocityTrackerAndAddMovement(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
Log.i("Garment1111", "onTouchEvent---ACTION_DOWN11111");
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
abortScrollerAnimation(false);
}
// Remember where the motion event started
mDownMotionX = mLastMotionX = ev.getX();
mDownMotionY = mLastMotionY = ev.getY();
mDownScrollX = getScrollX();
float[] p = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY);
mParentDownMotionX = p[0];
mParentDownMotionY = p[1];
mLastMotionXRemainder = 0;
mTotalMotionX = 0;
mActivePointerId = ev.getPointerId(0);
if (mTouchState == TOUCH_STATE_SCROLLING) {
pageBeginMoving();
}
break;
case MotionEvent.ACTION_MOVE: //相应手指的滑动的动作
Log.i("Garment1111", "onTouchEvent---ACTION_MOVE");
if (mTouchState == TOUCH_STATE_SCROLLING) {
// Scroll to follow the motion event
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) return true;
final float x = ev.getX(pointerIndex);
final float deltaX = mLastMotionX + mLastMotionXRemainder - x;
mTotalMotionX += Math.abs(deltaX);
// 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).
if (Math.abs(deltaX) >= 1.0f) {
mTouchX += deltaX;
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
if (!mDeferScrollUpdate&&ev.getPointerCount()!=2) {
scrollBy((int) deltaX, 0); //对屏幕(workspace)进行滑动
if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX);
} else {
invalidate();
}
mLastMotionX = x;
mLastMotionXRemainder = deltaX - (int) deltaX;
} else {
awakenScrollBars();
}
} else if (mTouchState == TOUCH_STATE_REORDERING) {
// Update the last motion position
mLastMotionX = ev.getX();
mLastMotionY = ev.getY();
// Update the parent down so that our zoom animations take this new movement into
// account
float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY);
mParentDownMotionX = pt[0];
mParentDownMotionY = pt[1];
updateDragViewTranslationDuringDrag();
// Find the closest page to the touch point
final int dragViewIndex = indexOfChild(mDragView);
// Change the drag view if we are hovering over the drop target
boolean isHoveringOverDelete = isHoveringOverDeleteDropTarget(
(int) mParentDownMotionX, (int) mParentDownMotionY);
setPageHoveringOverDeleteDropTarget(dragViewIndex, isHoveringOverDelete);
if (DEBUG) Log.d(TAG, "mLastMotionX: " + mLastMotionX);
if (DEBUG) Log.d(TAG, "mLastMotionY: " + mLastMotionY);
if (DEBUG) Log.d(TAG, "mParentDownMotionX: " + mParentDownMotionX);
if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY);
final int pageUnderPointIndex = getNearestHoverOverPageIndex();
if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView) &&
!isHoveringOverDelete) {
mTempVisiblePagesRange[0] = 0;
mTempVisiblePagesRange[1] = getPageCount() - 1;
getOverviewModePages(mTempVisiblePagesRange);
if (mTempVisiblePagesRange[0] <= pageUnderPointIndex &&
pageUnderPointIndex <= mTempVisiblePagesRange[1] &&
pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) {
mSidePageHoverIndex = pageUnderPointIndex;
mSidePageHoverRunnable = new Runnable() {
@Override
public void run() {
// Setup the scroll to the correct page before we swap the views
snapToPage(pageUnderPointIndex);
// For each of the pages between the paged view and the drag view,
// animate them from the previous position to the new position in
// the layout (as a result of the drag view moving in the layout)
int shiftDelta = (dragViewIndex < pageUnderPointIndex) ? -1 : 1;
int lowerIndex = (dragViewIndex < pageUnderPointIndex) ?
dragViewIndex + 1 : pageUnderPointIndex;
int upperIndex = (dragViewIndex > pageUnderPointIndex) ?
dragViewIndex - 1 : pageUnderPointIndex;
for (int i = lowerIndex; i <= upperIndex; ++i) {
View v = getChildAt(i);
// dragViewIndex < pageUnderPointIndex, so after we remove the
// drag view all subsequent views to pageUnderPointIndex will
// shift down.
int oldX = getViewportOffsetX() + getChildOffset(i);
int newX = getViewportOffsetX() + getChildOffset(i + shiftDelta);
// Animate the view translation from its old position to its new
// position
AnimatorSet anim = (AnimatorSet) v.getTag(ANIM_TAG_KEY);
if (anim != null) {
anim.cancel();
}
v.setTranslationX(oldX - newX);
anim = new AnimatorSet();
anim.setDuration(REORDERING_REORDER_REPOSITION_DURATION);
anim.playTogether(
ObjectAnimator.ofFloat(v, "translationX", 0f));
anim.start();
v.setTag(anim);
}
removeView(mDragView);
onRemoveView(mDragView, false);
addView(mDragView, pageUnderPointIndex);
onAddView(mDragView, pageUnderPointIndex);
mSidePageHoverIndex = -1;
if (mPageIndicator != null) {
mPageIndicator.setActiveMarker(getNextPage());
}
}
};
postDelayed(mSidePageHoverRunnable, REORDERING_SIDE_PAGE_HOVER_TIMEOUT);
}
} else {
removeCallbacks(mSidePageHoverRunnable);
mSidePageHoverIndex = -1;
}
} else {
determineScrollingStart(ev);
}
break;
case MotionEvent.ACTION_UP: //松手后,根据当前所滑动的位移和速度,判断松手后进入到哪一个分屏。
Log.i("Garment1111", "onTouchEvent---ACTION_UP");
if (mTouchState == TOUCH_STATE_SCROLLING) {
final int activePointerId = mActivePointerId;
final int pointerIndex = ev.findPointerIndex(activePointerId);
final float x = ev.getX(pointerIndex);
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
final int deltaX = (int) (x - mDownMotionX);
final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth();
boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
SIGNIFICANT_MOVE_THRESHOLD;
mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x);
boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING &&
Math.abs(velocityX) > mFlingThresholdVelocity;
// if (!mFreeScroll) {//workspace的滑动抬起事件
// In the case that the page is moved far to one direction and then is flung
// in the opposite direction, we use a threshold to determine whether we should
// just return to the starting page, or if we should skip one further.
boolean returnToOriginalPage = false;
if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
Math.signum(velocityX) != Math.signum(deltaX) && isFling) {
returnToOriginalPage = true;//返回当前页面
}
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.
final boolean isRtl = isLayoutRtlNew();
boolean isDeltaXLeft = isRtl ? deltaX > 0 : deltaX < 0;
boolean isVelocityXLeft = isRtl ? velocityX > 0 : velocityX < 0;
if (((isSignificantMove && !isDeltaXLeft && !isFling) ||
(isFling && !isVelocityXLeft)) && mCurrentPage > 0) {
finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
snapToPageWithVelocity(finalPage, velocityX);
Log.i("","####### finalPage 0: "+finalPage+"; "+mCurrentPage);
} else if (((isSignificantMove && isDeltaXLeft && !isFling) ||
(isFling && isVelocityXLeft)) &&
mCurrentPage < getChildCount() - 1) {
finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
snapToPageWithVelocity(finalPage, velocityX);
Log.i("","####### finalPage 1: "+finalPage+"; "+mCurrentPage);
} else {
snapToDestination();
}
// } else {//overview的滑动抬起事件
// if (!mScroller.isFinished()) {
// abortScrollerAnimation(true);
// }
// mScroller.setInterpolator(mDefaultInterpolator);
// float scaleX = getScaleX();
// int vX = (int) (-velocityX * scaleX);
// int initialScrollX = (int) (getScrollX() * scaleX);
//
// mScroller.setInterpolator(mDefaultInterpolator);
// mScroller.fling(initialScrollX,
// getScrollY(), vX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
//
// }
} else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
// at this point we have not moved beyond the touch slop
// (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
// we can just page
int nextPage = Math.max(0, mCurrentPage - 1);
if (nextPage != mCurrentPage) {
snapToPage(nextPage);
} else {
snapToDestination();
}
} else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
// at this point we have not moved beyond the touch slop
// (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
// we can just page
int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
if (nextPage != mCurrentPage) {
snapToPage(nextPage);
} else {
snapToDestination();
}
} else if (mTouchState == TOUCH_STATE_REORDERING) {
// Update the last motion position
mLastMotionX = ev.getX();
mLastMotionY = ev.getY();
// Update the parent down so that our zoom animations take this new movement into
// account
float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY);
mParentDownMotionX = pt[0];
mParentDownMotionY = pt[1];
updateDragViewTranslationDuringDrag();
boolean handledFling = false;
if (!DISABLE_FLING_TO_DELETE) {
// Check the velocity and see if we are flinging-to-delete
PointF flingToDeleteVector = isFlingingToDelete();
if (flingToDeleteVector != null) {
onFlingToDelete(flingToDeleteVector);
handledFling = true;
}
}
if (!handledFling && isHoveringOverDeleteDropTarget((int) mParentDownMotionX,
(int) mParentDownMotionY)) {
onDropToDelete();
}
} else {
if (!mCancelTap) {
onUnhandledTap(ev);
}
}
// Remove the callback to wait for the side page hover timeout
removeCallbacks(mSidePageHoverRunnable);
// End any intermediate reordering states
resetTouchState();
break;
case MotionEvent.ACTION_CANCEL:
if (mTouchState == TOUCH_STATE_SCROLLING) {
snapToDestination();
}
resetTouchState();
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
releaseVelocityTracker();
break;
}
return true;
}
函数分析:当进行move事件拦截后,move事件直接被拦截到onTouchEvent()中的MotionEvent.ACTION_MOVE,在手指滑动的过程中,workspace也会进行移动,scrollBy((int) deltaX, 0) 函数执行该行为,追踪该函数:
public void scrollTo(int x, int y) {
// In free scroll mode, we clamp the scrollX
if (mFreeScroll) {
x = Math.min(x, mFreeScrollMaxScrollX);
x = Math.max(x, mFreeScrollMinScrollX);
}
final boolean isRtl = isLayoutRtlNew();
mUnboundedScrollX = x;
boolean isXBeforeFirstPage = isRtl ? (x > mMaxScrollX) : (x < 0);
boolean isXAfterLastPage = isRtl ? (x < 0) : (x > mMaxScrollX);
if (isXBeforeFirstPage) { //在第一页向右移动时
Log.i("Garment1111", "isXBeforeFirstPage");
super.scrollTo(0, y);
if (mAllowOverScroll) {
if (isRtl) {
overScroll(x - mMaxScrollX);
} else {
overScroll(x);
}
}
} else if (isXAfterLastPage) { //在最后一页向左滑动时
Log.i("Garment1111", "isXAfterLastPage");
super.scrollTo(mMaxScrollX, y);
if (mAllowOverScroll) {
if (isRtl) {
overScroll(x);
} else {
overScroll(x - mMaxScrollX);
}
}
} else { //正常滑动时
Log.i("Garment1111", "scrollTo");
mOverScrollX = x;
super.scrollTo(x, y);
}
mTouchX = x;
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
// Update the last motion events when scrolling
if (isReordering(true)) {
float[] p = mapPointFromParentToView(this, mParentDownMotionX, mParentDownMotionY);
mLastMotionX = p[0];
mLastMotionY = p[1];
updateDragViewTranslationDuringDrag();
}
}
最后scrollBy()函数最终会调用super.scrollTo()进行workspace的滑动动作,根据坐标进行移动一定的位置。
函数分析:当手指从屏幕上放开后,就执行MotionEvent.ACTION_UP中的函数,主要是在mTouchState == TOUCH_STATE_SCROLLING这个判断语句中,下面的代码是判断页面的位置
if (((isSignificantMove && !isDeltaXLeft && !isFling) ||
(isFling && !isVelocityXLeft)) && mCurrentPage > 0) {
finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1; //向左移动一页还是留在本页
snapToPageWithVelocity(finalPage, velocityX);
Log.i("","####### finalPage 0: "+finalPage+"; "+mCurrentPage);
} else if (((isSignificantMove && isDeltaXLeft && !isFling) ||
(isFling && isVelocityXLeft)) &&
mCurrentPage < getChildCount() - 1) {
finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1; //向右移动一页还是留在本页
snapToPageWithVelocity(finalPage, velocityX);
Log.i("","####### finalPage 1: "+finalPage+"; "+mCurrentPage);
}