墨香带你学Launcher之(五)-Workspace滑动

上一章墨香带你学Launcher之(四)-应用安装、更新、卸载时的数据加载介绍了应用的安装、更新、卸载时的数据加载和图标绘制流程,本章我们来介绍承载图标、小部件等的Workspace的布局和滑动操作。

在第一章墨香带你学Launcher之(一)--概述中我们讲过Workspace包含多个CellLayout,每个CellLayout是一个页面,多个CellLayout可以通过滑动切换,这样就可以找到不同的图标,那么Workspace中的CellLayout是如何布局到Workspace中的,Workspace中滑动又是如何处理的,我们按照这两个步骤进行分析。

1.Workspace布局:


首先我们先看一下Workspace的继承逻辑:

launcher01.png

Workspace继承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

注:本文原创,转载请注明出处,多谢。

推荐阅读更多精彩内容