墨香带你学Launcher之(五)-Workspace滑动
上一章墨香带你学Launcher之(四)-应用安装、更新、卸载时的数据加载介绍了应用的安装、更新、卸载时的数据加载和图标绘制流程,本章我们来介绍承载图标、小部件等的Workspace的布局和滑动操作。
在第一章墨香带你学Launcher之(一)--概述中我们讲过Workspace包含多个CellLayout,每个CellLayout是一个页面,多个CellLayout可以通过滑动切换,这样就可以找到不同的图标,那么Workspace中的CellLayout是如何布局到Workspace中的,Workspace中滑动又是如何处理的,我们按照这两个步骤进行分析。
1.Workspace布局:
首先我们先看一下Workspace的继承逻辑:
launcher01.pngWorkspace继承PagedView,而PagedView又继承ViewGroup,由名字我们可以猜出,PagedView是分页的自定义View,谈到自定义View,我们应该比较熟悉自定义View的原理,此处不再详细讲解,不熟的可以看看我的这篇博客中的详解Android知识梳理。我们直接看Workspace是如何布局的,其实,workspace的布局是在PagedView里面处理的,首先是onMeasure方法,我们看下源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 如果没有子View则按照父类的尺寸进行测量
if (getChildCount() == 0) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
// We measure the dimensions of the PagedView to be larger than the pages so that when we
// zoom out (and scale down), the view is still contained in the parent
//上面这句话是说我们在测量尺寸时要比我们正常状态下的尺寸要大,为什么要
//大,我们在第一章概述中讲过,当你长按桌面时,桌面的workspace会缩小,
//此时弹出菜单,CellLayout缩小,然后你可以拖动CellLayout改变顺序,
//如果你没有放大PagedView的尺寸,你在缩小时,在整个屏幕上的
//workspace就不会沾满整个屏幕,导致你拖动困难。
...
//这里将最大尺寸放大了两倍
int parentWidthSize = (int) (2f * maxSize);
int parentHeightSize = (int) (2f * maxSize);
int scaledWidthSize, scaledHeightSize;
...
mViewport.set(0, 0, widthSize, heightSize);
...
setMeasuredDimension(scaledWidthSize, scaledHeightSize);
}
需要注意的地方已经在上面代码注释了,省略的代码是找到测量尺寸和测量模式,最后将相应的尺寸和模式放置到父View和子View中。
测量完成后就开始布局,也就是回调onLayout函数:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (getChildCount() == 0) {
return;
}
...
// 此处用到一个mIsRtl,这个是判断手机布局是从左到右还是从右到左,我们正常的习惯
// 是从左到右,一些国家,比如阿拉伯语情况下是从右到左,因此此处要进行处理。
final int startIndex = mIsRtl ? childCount - 1 : 0;
final int endIndex = mIsRtl ? -1 : childCount;
final int delta = mIsRtl ? -1 : 1;
...
for (int i = startIndex; i != endIndex; i += delta) {
final View child = getPageAt(i);
if (child.getVisibility() != View.GONE) {
lp = (LayoutParams) child.getLayoutParams();
int childTop;
if (lp.isFullScreenPage) {
childTop = offsetY;
} else {
childTop = offsetY + getPaddingTop() + mInsets.top;
if (mCenterPagesVertically) {
childTop += (getViewportHeight() - mInsets.top - mInsets.bottom - verticalPadding - child.getMeasuredHeight()) / 2;
}
}
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(), childTop + childHeight);
...
childLeft += childWidth + pageGap + getChildGap();
}
}
...
}
上面代码是个for循环,就是从第一个CellLayout到最后一个进行设置位置参数,然后进行布局,Workspace是横向滑动的,因此布局时,所有的CellLayout的顶部和底部距离是一样的,只是要考虑顶部状态栏的高度,横向上,从第一个开始由左向右或者由右向左进行排布即可,(由左向右举例:)也就是固定第一个CellLayout后调整左边距的位置即可,每增加一个CellLayout,后一个的左侧到Workspace左侧边距就增加一个CellLayout的作站用的宽度,依次类推,就可以将所有CellLayout布局完成。这段代码并不难,主要是自定义View的知识。
2.Workspace滑动:
workspace滑动就是onTouchEvent事件,关键代码也在这个方法里面,workspace继承PagedView,因此他的onTouchEvent事件是在PagedView中实现的,我们看一下代码:
public boolean onTouchEvent(MotionEvent ev) {
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:
...
if (mTouchState == TOUCH_STATE_SCROLLING) {
...
}
break;
case MotionEvent.ACTION_MOVE:
if (mTouchState == TOUCH_STATE_SCROLLING) {//滚动
...
} else if (mTouchState == TOUCH_STATE_REORDERING) {//拖动重新排序
...
} else {
determineScrollingStart(ev);
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_SCROLLING) {
...
} else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
...
} else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
...
} else if (mTouchState == TOUCH_STATE_REORDERING) {
...
} else {
...
}
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_POINTER_UP:
...
break;
}
return true;
}
上面代码只是一个onTouchEvent事件的一个框架,在这个框架中有完整的ACTION_DOWN、ACTION_MOVE、ACTION_UP事件,每个事件中都有一个mTouchState的判断,我们看一下,mTouchState有五种状态:
protected final static int TOUCH_STATE_REST = 0;
protected final static int TOUCH_STATE_SCROLLING = 1;
protected final static int TOUCH_STATE_PREV_PAGE = 2;
protected final static int TOUCH_STATE_NEXT_PAGE = 3;
protected final static int TOUCH_STATE_REORDERING = 4;
第一个是初始状态,第二个是滚动状态,第三个是向前翻页状态,第四个是向后翻页状态,最后一个是排序状态,前四个都好理解,那么最后一个是怎么回事呢?我们知道,在长按桌面的情况下,workspace缩小,此时你可以长按CellLayout拖动进行排序,因此出现了这个排序状态,如果只是滑动,则为滚动状态。
(一)ACTION_DOWN事件:
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) {
onScrollInteractionBegin();
pageBeginMoving();
}
触摸事件的起始事件,首先判断如果桌面滑动过程还没有完成,则终止滑动动画(abortScrollerAnimation),然后记录起始x、y的坐标位置,如果是滚动状态,则调用开始滚动方法,onScrollInteractionBegin和pageBeginMoving方法为空方法,你可以做一些准备工作。这个事件主要是记录起始位置。
(二)ACTION_MOVE事件,在这个事件中,分为三种状态:
(1)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);
if (Math.abs(deltaX) >= 1.0f) {
mTouchX += deltaX;
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
scrollBy((int) deltaX, 0);
mLastMotionX = x;
mLastMotionXRemainder = deltaX - (int) deltaX;
} else {
awakenScrollBars();
}
在这段代码中,首先获取有效手指的Index,然后获取有效手指的x坐标位置,因为是横向滑动,所以只需要x坐标即可,根据位置计算滑动距离,然后根据滑动距离调用scrollBy方法滑动workspace,这个方法,我们下面再看。
(2)TOUCH_STATE_REORDERING(排序)事件:
// 记录移动过程中的位置
mLastMotionX = ev.getX();
mLastMotionY = ev.getY();
...
// 更新你正在拖动排序的View的位置
updateDragViewTranslationDuringDrag();
// 查找距离手指最近的CellLayout的Index
final int dragViewIndex = indexOfChild(mDragView);
//查找手指移动到的位置所在的CellLayoutIndex,这个CellLayout是拖动过程中手指到达的位置处的CellLayout,没用动的
final int pageUnderPointIndex = getNearestHoverOverPageIndex();
if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView)) {
...
if (mTempVisiblePagesRange[0] <= pageUnderPointIndex &&
pageUnderPointIndex <= mTempVisiblePagesRange[1] &&
pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) {
mSidePageHoverIndex = pageUnderPointIndex;
mSidePageHoverRunnable = new Runnable() {
@Override
public void run() {
// 在交换位置前先滑动到手指所在的那个CellLayout位置
snapToPage(pageUnderPointIndex);
// 获取CellLayout的变化值,如果拖动的view的index小于手指位置处未动的view的index,则需要-1,也就是向前移动,反之向后移动,index+1
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);
int oldX = getViewportOffsetX() + getChildOffset(i);
int newX = getViewportOffsetX() + getChildOffset(i + shiftDelta);
v.setTranslationX(oldX - newX);
...
}
//移除拖动的View
removeView(mDragView);
//添加被拖动view到新的位置
addView(mDragView, pageUnderPointIndex);
mSidePageHoverIndex = -1;
if (mPageIndicator != null) {
mPageIndicator.setActiveMarker(getNextPage());
}
}
};
postDelayed(mSidePageHoverRunnable, REORDERING_SIDE_PAGE_HOVER_TIMEOUT);
}
} else {
...
}
shiftDelta, lowerIndex, upperIndex这三个值就是确定交换的位置,也就是如果从前向后拖动CellLayout,那么被拖动的Index要变大,反之变小,后两个参数来计算拖动CellLayout的跨度,如果向后拖动,那么中间被跨过的几个Celllayout就要顺序向前移动,反之向后移动,上面for循环就是移动的过程。
(三)ACTION_UP事件,这个事件中分为五种情况:
(1)TOUCH_STATE_SCROLLING事件:
...
//是否是有效事件,也就是滑动位置是否超过了pagedView的40%,
boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
SIGNIFICANT_MOVE_THRESHOLD;
boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING &&
Math.abs(velocityX) > mFlingThresholdVelocity;
if (!mFreeScroll) {
boolean returnToOriginalPage = false;
if (Math.abs(deltaX) > pageWidth * RETURN_TO_ORIGINAL_PAGE_THRESHOLD &&
Math.signum(velocityX) != Math.signum(deltaX) && isFling) {
returnToOriginalPage = true;
}
...
if (((isSignificantMove && !isDeltaXLeft && !isFling) ||
(isFling && !isVelocityXLeft)) && mCurrentPage > 0) {
inalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
snapToPageWithVelocity(finalPage, velocityX);
} else if (((isSignificantMove && isDeltaXLeft && !isFling) ||
(isFling && isVelocityXLeft)) &&
mCurrentPage < getChildCount() - 1) {
finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
snapToPageWithVelocity(finalPage, velocityX);
} else {
snapToDestination();
}
} else {
...
mScroller.fling(initialScrollX,
getScrollY(), vX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
invalidate();
}
onScrollInteractionEnd();
此处判断比较多,我解释一下,我们在左右滑动时,有个有效值,也就是手指滑动距离超过了该值,则认为是有效的,到你超过这个值然后抬起手指,则认为你滑动了一屏,剩下的距离根据惯性自动完成,如果你滑动没有超过这个值,则认为你切换屏幕是无效的,抬起手指后屏幕会返回到初始的屏幕位置。
(2)TOUCH_STATE_PREV_PAGE事件:
如果不是第一屏,滑动到前一屏,代码很简单,不再贴代码
(3)TOUCH_STATE_NEXT_PAGE事件:
如果不是最后一屏,滑动到下一屏
(4)TOUCH_STATE_REORDERING:
排序,也就是调用updateDragViewTranslationDuringDrag方法,移动拖拽的View到相应的位置。
(四)滑动方法:
(1)scrollBy方法:这个方法其实很简单最终调用的是scrollTo方法,也就是移动到相应的位置,最后调用View的scrollTo方法;
(2)snapToPage方法:这个方法最终调用mScroller.startScroll(),计算出最终位置,然后滑动到相应位置即可。
最后
Github地址:https://github.com/yuchuangu85/Launcher3_mx
微信公众账号:Code-MX
注:本文原创,转载请注明出处,多谢。