上一章墨香带你学Launcher之(四)- 应用安装、更新、卸载时的数据加载 介绍了应用的安装、更新、卸载时的数据加载和图标绘制流程,本章我们来介绍承载图标、小部件等的Workspace的布局和滑动操作。
在第一章墨香带你学Launcher之(一)- 概述 中我们讲过Workspace包含多个CellLayout,每个CellLayout是一个页面,多个CellLayout可以通过滑动切换,这样就可以找到不同的图标,那么Workspace中的CellLayout是如何布局到Workspace中的,Workspace中滑动又是如何处理的,我们按照这两个步骤进行分析。
1.Workspace布局:
首先我们先看一下Workspace的继承逻辑:
Workspace继承PagedView,而PagedView又继承ViewGroup,由名字我们可以猜出,PagedView是分页的自定义View,谈到自定义View,我们应该比较熟悉自定义View的原理,此处不再详细讲解,不熟的可以看看我的这篇博客中的详解Android知识梳理 。我们直接看Workspace是如何布局的,其实,workspace的布局是在PagedView里面处理的,首先是onMeasure方法,我们看下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { if (getChildCount() == 0 ) { super .onMeasure(widthMeasureSpec, heightMeasureSpec); return ; } ... 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函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 protected void onLayout (boolean changed, int left, int top, int right, int bottom) { if (getChildCount() == 0 ) { return ; } ... 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中实现的,我们看一下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public boolean onTouchEvent (MotionEvent ev) { super .onTouchEvent(ev); 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有五种状态:
1 2 3 4 5 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事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 if (!mScroller.isFinished()) { abortScrollerAnimation(false ); } 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状态: ```java 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(排序)事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 mLastMotionX = ev.getX(); mLastMotionY = ev.getY(); ... updateDragViewTranslationDuringDrag(); final int dragViewIndex = indexOfChild(mDragView);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 () { snapToPage(pageUnderPointIndex); 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); ... } removeView(mDragView); 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事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 ... 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/tree/launcher3_6.0
Android开发群:192508518
微信公众账号:Code-MX
注:本文原创,转载请注明出处,多谢。