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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容