美文网首页移动开发狂热者(299402133)Android知识移动开发
墨香带你学Launcher之(五)-Workspace滑动

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

作者: 翰墨飘香 | 来源:发表于2016-10-18 17:08 被阅读877次

    上一章墨香带你学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

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

    相关文章

      网友评论

      本文标题:墨香带你学Launcher之(五)-Workspace滑动

      本文链接:https://www.haomeiwen.com/subject/jljtuttx.html