【Launcher开发】拖拽过程分析(上)

    不知你是否还记得桌面布局分析一文中的launcher.xml布局文件中根布局下嵌套的一个看似没啥用的DragLayer布局,它既不像Workspace那样作为PagedView容器占据大半江山,也不似HotSeat那样别有洞天。如果仅以布局的层面看,它更像是冗余存在。但它作为包裹布局,却实在有深层次的意义,今天我们就一起来揭下其神秘面纱。

    我们知道,在安卓桌面长按某个应用图标时可以拖拽进行重新排序。那么整个拖拽过程是怎么发生的呢,它和我们上文说的DragLayer又有什么关系呢。显然,我们应该先从应用图标的长按监听开始看。

    在Launcher.java也就是启动类中的setUpViews方法中我们可以看到workspace设置了长按事件监听,并且回调Listener就是Launcher本身this。

 private void setupViews() {
        final DragController dragController = mDragController;
        //最外层FrameLayout
        mLauncherView = findViewById(R.id.launcher);
        //用于拖拽的拖拽布局
        mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
        //存放CellLayout,是一个PageView
        mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
        //存在于Land屏幕中
        mQsbDivider = findViewById(R.id.qsb_divider);
        //WorkSpace与HotSeat之间的分界线,当进入All App模式时,会隐藏
        mDockDivider = findViewById(R.id.dock_divider);
        //设置launcher全屏显示
        /**
         * 1.View.SYSTEM_UI_FLAG_VISIBLE :状态栏和Activity共存,Activity不全屏显示。也就是应用平常的显示画面
         * 2.View.SYSTEM_UI_FLAG_FULLSCREEN :Activity全屏显示,且状态栏被覆盖掉
         * 3.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN :Activity全屏显示,但是状态栏不会被覆盖掉,而是正常显示,只是Activity顶端布   局会被覆盖住
         * 4.View.INVISIBLE : Activity全屏显示,隐藏状态栏
         */
        mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
        mWorkspaceBackgroundDrawable = getResources().getDrawable(R.drawable.workspace_bg);

        // Setup the drag layer
        mDragLayer.setup(this, dragController);

        // 初始化底部热区控件
        mHotseat = (Hotseat) findViewById(R.id.hotseat);
        if (mHotseat != null) {
            //传递launcher引用
            mHotseat.setup(this);
        }

        // Setup the workspace
        mWorkspace.setHapticFeedbackEnabled(false);
        //处于长按事件,包括壁纸选择,长按拖拽等
        mWorkspace.setOnLongClickListener(this);
        mWorkspace.setup(dragController);
        //让WorkSpace成为拖拽开始事件与结束事件的CallBack
        dragController.addDragListener(mWorkspace);

        // Get the search/delete bar
        mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.qsb_bar);

        // Setup AppsCustomize
        mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane);
        mAppsCustomizeContent = (AppsCustomizePagedView)
                mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content);
        mAppsCustomizeContent.setup(this, dragController);

        // Setup the drag controller (drop targets have to be added in reverse order in priority)
        dragController.setDragScoller(mWorkspace);
        dragController.setScrollView(mDragLayer);
        dragController.setMoveTarget(mWorkspace);
        dragController.addDropTarget(mWorkspace);
        if (mSearchDropTargetBar != null) {
            mSearchDropTargetBar.setup(this, dragController);
        }
    }

    我们可以猜一猜,workspace的setOnLongClickListener最后一定会把listener传递给BubbleTextView即应用图标。接着我们点进去。代码如下:

@Override
    public void setOnLongClickListener(OnLongClickListener l) {
        mLongClickListener = l;
        final int count = getPageCount();
        for (int i = 0; i < count; i++) {
            getPageAt(i).setOnLongClickListener(l);
        }
    }

    不出所料,workspace继续把listener继续传给子控件page(其实就是CellLayout),那么接下来继续往CellLayout追溯...

    等等!!!

    为啥在CellLayout没有重写setOnLongClickListener相关的表述呢?

    原来关于OnLongClickListener的添加没有由CellLayout自己来控制(其实我认为由自己控制更好),BubbleTextView的添加始于bindItems等launcher的绑定控件回调,我们进入到bindXXX()中可以看见,无论是绑定时创建的BubbleTextView、AppWidgetHostView还是FoldIcon,最终都通过workspace.addInScreen添加到布局中。以下是addInScreen方法:

void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY,
            boolean insert) {
        if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
            if (screen < 0 || screen >= getChildCount()) {
                Log.e(TAG, "The screen must be >= 0 and < " + getChildCount()
                    + " (was " + screen + "); skipping child");
                return;
            }
        }
        final CellLayout layout;
        if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
            layout = mLauncher.getHotseat().getLayout();
            child.setOnKeyListener(null);
            if (child instanceof FolderIcon) {
                ((FolderIcon) child).setTextVisible(false);
            }

            if (screen < 0) {
                screen = mLauncher.getHotseat().getOrderInHotseat(x, y);
            } else {
                // Note: We do this to ensure that the hotseat is always laid out in the orientation
                // of the hotseat in order regardless of which orientation they were added
                x = mLauncher.getHotseat().getCellXFromOrder(screen);
                y = mLauncher.getHotseat().getCellYFromOrder(screen);
            }
        } else {
            // Show folder title if not in the hotseat
            if (child instanceof FolderIcon) {
                ((FolderIcon) child).setTextVisible(true);
            }

            layout = (CellLayout) getChildAt(screen);
            child.setOnKeyListener(new IconKeyEventListener());
        }

        LayoutParams genericLp = child.getLayoutParams();
        CellLayout.LayoutParams lp;
        if (genericLp == null || !(genericLp instanceof CellLayout.LayoutParams)) {
            lp = new CellLayout.LayoutParams(x, y, spanX, spanY);
        } else {
            lp = (CellLayout.LayoutParams) genericLp;
            lp.cellX = x;
            lp.cellY = y;
            lp.cellHSpan = spanX;
            lp.cellVSpan = spanY;
        }

        if (spanX < 0 && spanY < 0) {
            lp.isLockedToGrid = false;
        }

        // Get the canonical child id to uniquely represent this view in this screen
        int childId = LauncherModel.getCellLayoutChildId(container, screen, x, y, spanX, spanY);
        boolean markCellsAsOccupied = !(child instanceof Folder);
        if (!layout.addViewToCellLayout(child, insert ? 0 : -1, childId, lp, markCellsAsOccupied)) {
            // TODO: This branch occurs when the workspace is adding views
            // outside of the defined grid
            // maybe we should be deleting these items from the LauncherModel?
            Log.w(TAG, "Failed to add to item at (" + lp.cellX + "," + lp.cellY + ") to CellLayout");
        }

        if (!(child instanceof Folder)) {
            child.setHapticFeedbackEnabled(false);
            child.setOnLongClickListener(mLongClickListener);
        }
        if (child instanceof DropTarget) {
            mDragController.addDropTarget((DropTarget) child);
        }
    }

    可以看到,在64行执行了setOnLongClickListener方法,而这个child则正是BubbleTextView、AppWidgetHostView、FolderIcon之一。也就是说CellLayout中的子控件cell都设置了长按点击事件的listener为mLongClickListener,而mLongClickListener正是在workspace的setOnLongClickListener(上一个代码段中可见)保存的。如此,所有的长按事件都应该在Launcher.java的onLongClick中回调。即如下代码:

public boolean onLongClick(View v) {
        if (!isDraggingEnabled()) return false;//能否拖拽,内部判断startLoader操作是否完成
        if (isWorkspaceLocked()) return false;
        if (mState != State.WORKSPACE) return false;//是否处于workSpace中
        //长按分为两种:1.长按ItemInfo,2.长按空白处
        if (!(v instanceof CellLayout)) {//主要目的是为了获取CellLayout
            v = (View) v.getParent().getParent();
        }//判断当前长按控件是否是CellLayout,如果当前的v是shortcut平级的view,那么getParent就是ShortcutAndWidgetContainer,再次getParent即CellLayout

        resetAddInfo();//pendingAddInfo重置
        CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag();
        // This happens when long clicking an item with the dpad/trackball
        if (longClickCellInfo == null) {
            return true;
        }

        //cell信息是在CellLayout的onInterceptTouchEvent中控制的,
        // 当WorkSpace不处于小屏模式时会在onInterceptTouchEvent中记录touchX,Y值以及点击的BubbleTextView
        final View itemUnderLongClick = longClickCellInfo.cell;
        //是否允许长按事件
        boolean allowLongPress = isHotseatLayout(v) || mWorkspace.allowLongPress();
        //当前处于允许长按状态,且没有控件正在被拖拽
        if (allowLongPress && !mDragController.isDragging()) {
            //如果CellLayout没有BubbleTextView被点击,则说明是长按进入壁纸选择事件
            if (itemUnderLongClick == null) {
                //长按震动反馈
                mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
                startWallpaper();//如果长按区域中没有ItemInfo,那么就处理为长按设置壁纸
            } else {
                //Folder打开后的的长按拖拽事件不在这里处理
                if (!(itemUnderLongClick instanceof Folder)) {
                    //在startDrag中让CellLayout中被长按的控件隐藏,并且调用DragController开始在DragLayer中处理拖拽事件
                    mWorkspace.startDrag(longClickCellInfo);
                }
            }
        }
        return true;
    }

    在上面代码段中有个Tag判断至关重要,它就是从何而来呢?

    熟悉事件分发机制的朋友们应该知道View的事件分发是向内传递事件,向外消费事件。父控件ViewGroup先判断是否打断(Intercept)事件即onInterceptTouchEvent方法,然后在根据Intercept的返回值决定是否继续往下dispatch,如果onInterceptTouchEvent返回值为true,那么触摸事件就由控件自身的onTouchEvent处理,如果为false,则调用dispatchTouchEvent方法继续往内部子控件分发。如果子控件接收消费该事件,那么则进行listener消费或者onTouch消费。我们进入到CellLayout的onInterceptTouchEvent中:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // First we clear the tag to ensure that on every touch down we start with a fresh slate,
        // even in the case where we return early. Not clearing here was causing bugs whereby on
        // long-press we'd end up picking up an item from a previous drag operation.
        final int action = ev.getAction();
        //清除cellInfo中的信息-reset
        if (action == MotionEvent.ACTION_DOWN) {
            clearTagCellInfo();
        }
        //当listener的onTouch返回true表示小屏模式或者还没完成切换,
        // 小屏模式指代在all apps页面长按应用icon后进入Workspace处于的模式,state==mall or state==spring_load
        //也就是说小模式下CellLayout内部的BubbleTextView不会响应事件
        if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) {
            return true;
        }
        //在down事件时设置cellInfo标识当前touch的位置已经touch位置上的cell
        if (action == MotionEvent.ACTION_DOWN) {
            setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY());
        }
        //默认不拦截事件
        return false;
    }

    首先onInterceptTouchEvent一定是先于子控件的onLongClick消费事件执行的,那么我们据以上可以知道,在onInterceptTouchEvent判断时,CellLayout就记录了当前的downX,Y值,不信可以看setTagToCellInfoForPoint方法:

public void setTagToCellInfoForPoint(int touchX, int touchY) {
        final CellInfo cellInfo = mCellInfo;
        Rect frame = mRect;
        final int x = touchX + getScrollX();
        final int y = touchY + getScrollY();
        final int count = mShortcutsAndWidgets.getChildCount();

        boolean found = false;
        for (int i = count - 1; i >= 0; i--) {
            final View child = mShortcutsAndWidgets.getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            if ((child.getVisibility() == VISIBLE || child.getAnimation() != null) &&
                    lp.isLockedToGrid) {
                child.getHitRect(frame);

                float scale = child.getScaleX();
                frame = new Rect(child.getLeft(), child.getTop(), child.getRight(),
                        child.getBottom());
                //在new出来时,frame的宽高是为child的宽高-相对应parent控件ShortcutWidgetContainer的距离,
                //距离cellLayout的距离加上ShortcutWidgetContainer与CellLayout的距离,也就是padding
                //总结:此处计算->child位置=child与父控件(ShortcutWidgetContainer距离)+父控件到CellLayout距离
                frame.offset(getPaddingLeft(), getPaddingTop());
                //此处当child有scale时需要对frame进行缩放。
                frame.inset((int) (frame.width() * (1f - scale) / 2),
                        (int) (frame.height() * (1f - scale) / 2));
                //碰撞检测,如果点击的xy在child上,那么就使用CellInfo记录点击信息
                if (frame.contains(x, y)) {
                    cellInfo.cell = child;
                    cellInfo.cellX = lp.cellX;
                    cellInfo.cellY = lp.cellY;
                    cellInfo.spanX = lp.cellHSpan;
                    cellInfo.spanY = lp.cellVSpan;
                    found = true;
                    break;
                }
            }
        }

        mLastDownOnOccupiedCell = found;
        //
        if (!found) {
            final int cellXY[] = mTmpXY;
            pointToCellExact(x, y, cellXY);

            cellInfo.cell = null;
            cellInfo.cellX = cellXY[0];
            cellInfo.cellY = cellXY[1];
            cellInfo.spanX = 1;
            cellInfo.spanY = 1;
        }
        //使用tag记录CellInfo即当前的touch信息
        setTag(cellInfo);
    }

    由此可知,CellLayout的Tag记录了当前down事件的位置信息以及child(点击位置的子控件)信息。啰嗦一句,上面代码ShortcutWidgetContainer控件是CellLayout的下级容器控件,我们添加到CellLayout的应用图标,其实都是添加到CellLayout子容器ShortcutWidgetContainer中了。

    此时再回到onLongClick中就可以理解为什么itemUnderLongClick下的cell为空时会执行壁纸选择了,因为在onInterceptTouchEvent中没有在点击的downX,Y处找到child控件。当然,本文是关于拖拽分析的,自然更关心cell非空的else语句。在其中调用了Workspace的startDrag方法。接下来我们进入到startDrag中:

 void startDrag(CellLayout.CellInfo cellInfo) {
        View child = cellInfo.cell;

        // Make sure the drag was started by a long press as opposed to a long click.
        if (!child.isInTouchMode()) {
            return;
        }
        //隐藏被长按的控件,并清除掉控件中的状态
        mDragInfo = cellInfo;
        child.setVisibility(INVISIBLE);
        CellLayout layout = (CellLayout) child.getParent().getParent();
        layout.prepareChildForDrag(child);
        child.clearFocus();
        child.setPressed(false);

        final Canvas canvas = new Canvas();

        //在此处画出outline,即child的轮廓
        mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING);
        beginDragShared(child, this);
    }

    以上代码步骤可以分解为:

        1.隐藏掉我们点击的应用图标,并清除其focus和press效果。为什么要隐藏控件,这里先留坑,下面会解答。

        2.画出当前被点击child的outline即轮廓,此轮廓当dragOver某个Cell时,会在这个Cell上显示child边廓。

        3.调用beginDragShared方法,其内部矫正了downX,Y后把拖拽交由我们拖拽主角DragController来实现真正拖拽效果。

     beginDragShared方法如下,主要是:

public void beginDragShared(View child, DragSource source) {
        Resources r = getResources();

        //此方法内部主要通过((TextView) v).getCompoundDrawables()[1]获取bitmap
        final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING);

        final int bmpWidth = b.getWidth();
        final int bmpHeight = b.getHeight();
        //scale时child相对于DragLayer的缩放比,mTempXY则记录了child相对于DragLayer的的x,y坐标
        float scale = mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY);
        //如果scale比例为1,那么dragLayerX即是mTempXY[0]
        int dragLayerX =
                Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2);
        //如果scale比例为1,那么dragLayerY即是mTempXY[1]
        int dragLayerY =
                Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2
                        - DRAG_BITMAP_PADDING / 2);

        Point dragVisualizeOffset = null;
        Rect dragRect = null;
        if (child instanceof BubbleTextView || child instanceof PagedViewIcon) {
            int iconSize = r.getDimensionPixelSize(R.dimen.app_icon_size);
            int iconPaddingTop = r.getDimensionPixelSize(R.dimen.app_icon_padding_top);
            int top = child.getPaddingTop();
            int left = (bmpWidth - iconSize) / 2;
            int right = left + iconSize;
            int bottom = top + iconSize;
            dragLayerY += top;//如果child还有paddingTop,那么还需要矫正paddingTop值
            // Note: The drag region is used to calculate drag layer offsets, but the
            // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
            dragVisualizeOffset = new Point(-DRAG_BITMAP_PADDING / 2,
                    iconPaddingTop - DRAG_BITMAP_PADDING / 2);
            dragRect = new Rect(left, top, right, bottom);
        } else if (child instanceof FolderIcon) {
            int previewSize = r.getDimensionPixelSize(R.dimen.folder_preview_size);
            dragRect = new Rect(0, 0, child.getWidth(), previewSize);
        }

        //此处再次清除焦点和press等,其实在其上已经清除过
        if (child instanceof BubbleTextView) {
            BubbleTextView icon = (BubbleTextView) child;
            icon.clearPressedOrFocusedBackground();
        }
        //把拖拽控制权交给mDragController即DragController类
        mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
        //用完回收,好习惯
        b.recycle();

        // 不显示指示器
        showScrollingIndicator(false);
    }

    上面方法详解:

        1.获取child(也就是CellLayout中被点击的应用图标)的bitmap。此bitmap在之后伪造child时会用到。

        2.child相对于DragLayer的位置,DragLayer即文章开头我们提到的看似没用的控件,从此处开始它将登上舞台。位置信息通过getLocationInDragLayer存放在mTempXY中,scale代表child相对于DragLayer的缩放比例,为啥会用到scale呢,因为getLeft、getWidth等方法得到的值时未缩放平移的值(可以参考tween动画原理,这里不赘述)。我们也可以进入getLocationInDragLayer,其内部其实最终调用到此处:

public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) {
        float scale = 1.0f;
        float[] pt = {coord[0], coord[1]};
        descendant.getMatrix().mapPoints(pt);
        scale *= descendant.getScaleX();
        pt[0] += descendant.getLeft();
        pt[1] += descendant.getTop();
        ViewParent viewParent = descendant.getParent();
        while (viewParent instanceof View && viewParent != this) {
            final View view = (View)viewParent;
            //mapPoints方法,getLeft等是不会计算scale、translate、skew等后的view实际位置的,
            // 而,使用matrix的mapPoints可以矫正view的实际位置
            view.getMatrix().mapPoints(pt);
            scale *= view.getScaleX();
            pt[0] += view.getLeft() - view.getScrollX();
            pt[1] += view.getTop() - view.getScrollY();
            viewParent = view.getParent();
        }
        coord[0] = (int) Math.round(pt[0]);
        coord[1] = (int) Math.round(pt[1]);
        return scale;
    }

    3.因为拖拽主要需要确定child位置,但实际主要是child中图片位置,所以dragLayerY会加上child的paddingTop值。

    4.拖拽控制权转交给DragController执行。那么DragController是啥呢,它其实是外层布局DragLayer(这家伙又出来了)的TouchEvent事件接管者。不信?我们接着贴代码,以下代码都是DragLayer中的方法。

    onInterceptTouchEvent方法

/**
     * 如果当前down事件处于打开的folder中,那么应该让folder处理此事件
     * 否则则由DragController接手事件处理
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            if (handleTouchDown(ev, true)) {
                return true;
            }
        }
        clearAllResizeFrames();
        return mDragController.onInterceptTouchEvent(ev);
    }

    onTouchEvent方法    

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        int action = ev.getAction();

        int x = (int) ev.getX();
        int y = (int) ev.getY();

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                if (handleTouchDown(ev, false)) {
                    return true;
                }
            }
        }

        if (mCurrentResizeFrame != null) {
            handled = true;
            switch (action) {
                case MotionEvent.ACTION_MOVE:
                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
                    mCurrentResizeFrame.onTouchUp();
                    mCurrentResizeFrame = null;
            }
        }
        if (handled) return true;
        return mDragController.onTouchEvent(ev);
    }

    如果你对这两个方法中的handleTouchDown有疑惑,其实它内部是处理Folder的Touch处理的。handleTouchDown方法如下:

/**
     * 处理down事件,重点是down事件发生时是否有folder正处于打开状态
     *
     * @param ev
     * @param intercept
     * @return
     */
    private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
        Rect hitRect = new Rect();
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        for (AppWidgetResizeFrame child: mResizeFrames) {
            child.getHitRect(hitRect);
            if (hitRect.contains(x, y)) {
                if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) {
                    mCurrentResizeFrame = child;
                    mXDown = x;
                    mYDown = y;
                    //阻止父级控件拦截touch事件
                    requestDisallowInterceptTouchEvent(true);
                    return true;
                }
            }
        }
        /**
         *
         *如果当前down事件是在打开的folder中,那么先看down的位置是否是在可编辑的EditView中,
         * 如果不是,那么让EditView回到unfocus状态,并且隐藏掉软键盘
         */
        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
        if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) {
            if (currentFolder.isEditingName()) {
                if (!isEventOverFolderTextRegion(currentFolder, ev)) {
                    currentFolder.dismissEditingName();
                    return true;
                }
            }
            /**
             * 如果down没有发生在正在打开的folder中,那么久隐藏此holder,执行close操作
             */
            getDescendantRectRelativeToSelf(currentFolder, hitRect);
            if (!isEventOverFolder(currentFolder, ev)) {
                mLauncher.closeFolder();
                return true;
            }
        }
        return false;
    }

    其实也就是说,如果当前WorkSpace中没有被打开的Folder需要处理,那么DragLayer的事件就由DragController全权接手处理。

    OK!让我们回到主题,之前说到beginDragShared方法最后会调用DragController的startDrag方法来让DragController接手长按事件的处理。那么我们有必要分析DragController类的方法结构。

public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,
            DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
            float initialDragViewScale) {
        if (PROFILE_DRAWING_DURING_DRAG) {
            android.os.Debug.startMethodTracing("Launcher");
        }

        // Hide soft keyboard, if visible
        if (mInputMethodManager == null) {
            mInputMethodManager = (InputMethodManager)
                    mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE);
        }
        mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);

        for (DragListener listener : mListeners) {
            listener.onDragStart(source, dragInfo, dragAction);
        }
        //mMotionDownX,mMotionDownY皆是由onInterceptTouchEvent获取
        final int registrationX = mMotionDownX - dragLayerX;
        final int registrationY = mMotionDownY - dragLayerY;

        final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
        final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
        //此处把mDragging置为true会导致DragLayer的onInterceptTouchEvent被DragController接收,最终DragLayer会拦截掉Touch事件
        //让DragLayer自己处理onTouchEvent,然后会传到DragController自身的onTouchEvent身上来
        mDragging = true;

        mDragObject = new DropTarget.DragObject();

        mDragObject.dragComplete = false;

        mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
        mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
        mDragObject.dragSource = source;//即workspace
        mDragObject.dragInfo = dragInfo;//bubbleTextView的Tag,即ShortcutInfo

        mVibrator.vibrate(VIBRATE_DURATION);

        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
                registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);

        if (dragOffset != null) {
            dragView.setDragVisualizeOffset(new Point(dragOffset));
        }
        if (dragRegion != null) {
            dragView.setDragRegion(new Rect(dragRegion));
        }

        dragView.show(mMotionDownX, mMotionDownY);
        handleMoveEvent(mMotionDownX, mMotionDownY);
    }

    此处做了如下操作:

        1.隐藏软键盘。

        2.通知监听者listener拖拽开始即onDragStart。

        3.使mDragging等于true。这一步很关键,正是因为mDragging的值决定了DragLayer能否拦截事件自己处理。

        4.拖拽震动反馈。

        5.使用成员变量mDragObject保存拖拽信息。包括拖拽位置、拖拽源、拖拽itemInfo(通过child.getTag()获取),并且重新创建了一个DragView,此DragView主要用于显示之前我们通过child的createDragBitmap创造的bitmap并是实际的被拖动者。还记得上文提到过的应用图标已经被隐藏了吗?其实,真正被拖动的控件式被伪造出来的DragView(可以理解为一个ImageView,当然他是直接继承的View控件,只不过在onDraw方法中绘制了bitmap)。

        6.调用dragView的show方法,其内部一方面添加当前DragView到DragLayer(再次说明,这货很有用),另一方面执行了dragView的缩放动画(这里主要是对DragView放大显示,这也是为啥我们长按应用图标后图标变大的地方)。    

        7.调用handleMoveEvent方法。其内部调用DragView的move(X,Y)方法,达到真正的移动效果。

     此处再跑下题,为什么说mDragging这个值在startDrag中被置为true很重要呢?

    我们知道DragController的startDrag方法被调用是追根溯源是源自应用图标的onLongClick方法。此时可以确定的是DragLayer还没有开始拦截事件不让往下传递。但是!!!当DragController的startDrag方法把mDragging置为true时呢。我们进入到DragController的onInterceptTouchEvent中(其实本该进入到DragLayer的onInterceptTouchEvent中去看,但是其事件已经委托给DragController了,所以直接来DragController看):

public boolean onInterceptTouchEvent(MotionEvent ev) {
        @SuppressWarnings("all") // suppress dead code warning
        final boolean debug = false;
        if (debug) {
            Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging="
                    + mDragging);
        }

        // Update the velocity tracker
        acquireVelocityTrackerAndAddMovement(ev);

        final int action = ev.getAction();
        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
        final int dragLayerX = dragLayerPos[0];
        final int dragLayerY = dragLayerPos[1];

        switch (action) {
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_DOWN:
                // Remember location of down touch
                mMotionDownX = dragLayerX;
                mMotionDownY = dragLayerY;
                mLastDropTarget = null;
                break;
            case MotionEvent.ACTION_UP:
                mLastTouchUpTime = System.currentTimeMillis();
                if (mDragging) {
                    PointF vec = isFlingingToDelete(mDragObject.dragSource);
                    if (vec != null) {
                        dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
                    } else {
                        drop(dragLayerX, dragLayerY);
                    }
                }
                endDrag();
                break;
            case MotionEvent.ACTION_CANCEL:
                cancelDrag();
                break;
        }

        return mDragging;
    }

    可以看到Drag Layer是否拦截Touch事件正式由mDragging决定的。所以在startDrag中把mDragging置为true,也就可以理解为当用户长按应用图标时DragLayer就拦截事件自己处理了。

   回到正题。我们从DragControlller的startDrag及onTouchEvent中可以知道。拖拽的过程其实就是在不断地调用handleMoveEvent()方法。那么我们有必要进入到此方法中去看一看。以下是此方法的代码段:

private void handleMoveEvent(int x, int y) {
        mDragObject.dragView.move(x, y);

        // Drop on someone?
        final int[] coordinates = mCoordinatesTemp;
        //此处的coordinates要经过修正,得出的相对于workspace的x,y值
        DropTarget dropTarget = findDropTarget(x, y, coordinates);
        mDragObject.x = coordinates[0];
        mDragObject.y = coordinates[1];
        //通知workspace当前dragView的xy值(此x,y值已经不是相对于DragLayer,而是经过修正,相对于WorkSpace)等信息,
        // 方便Workspace在onDragOver回调处理像显示轮廓等信息
        checkTouchMove(dropTarget);

        // Check if we are hovering over the scroll areas
        //Math.pow是进行次方运算,Math.sqrt进行开方运算
        mDistanceSinceScroll +=
            Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2));
        mLastTouch[0] = x;
        mLastTouch[1] = y;
        //检查当前的x,y值是否进入WorkSpace的页面scroll边界,边界值为20dp,
        //也就是说当前dragView进入到距离WorkSpace的左右边界20dp范围内的话就触发scroll页面
        checkScrollState(x, y);
    }

    此方法在矫正完x,y的值后,检查当前的拖拽状态,及滑动状态(拖拽滑屏)。

    我们先进入checkTouchMove中。

private void checkTouchMove(DropTarget dropTarget) {
        if (dropTarget != null) {
            DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject);
            if (delegate != null) {
                dropTarget = delegate;
            }
            //mLastDropTarget在down事件触发时会被置null,所以当每次重新长按时一定会走onDragEnter
            if (mLastDropTarget != dropTarget) {
                if (mLastDropTarget != null) {
                    mLastDropTarget.onDragExit(mDragObject);
                }
                //首次时调用onDragEnter
                dropTarget.onDragEnter(mDragObject);
            }
            //以后每次移动时都会调用onDragOver
            dropTarget.onDragOver(mDragObject);
        } else {
            if (mLastDropTarget != null) {
                mLastDropTarget.onDragExit(mDragObject);
            }
        }
        mLastDropTarget = dropTarget;
    }

    其实不难理解就是在其中调用了DropTarget即Workspace的onDragEnter及onDragOver方法。onDragEnter内部主要是做了一些基本的清理工作,没啥可说的。主要是onDragOver方法,其内部主要处理了拖动的逻辑,下面一起来看一看onDragOver方法:

public void onDragOver(DragObject d) {

        //
        if (mInScrollArea || mIsSwitchingState || mState == State.SMALL) return;

        Rect r = new Rect();
        CellLayout layout = null;
        ItemInfo item = (ItemInfo) d.dragInfo;

        //spanX,Y表示当前子控件占据的横向Cell个数和纵向Cell个数
        if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found");
        mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset,
            d.dragView, mDragViewVisualCenter);

        final View child = (mDragInfo == null) ? null : mDragInfo.cell;
        // Identify whether we have dragged over a side page
        if (isSmall()) {//当从all apps长按拖动icon到workspace时调用时会进入
            if (mLauncher.getHotseat() != null && !isExternalDragWidget(d)) {
                mLauncher.getHotseat().getHitRect(r);
                if (r.contains(d.x, d.y)) {
                    layout = mLauncher.getHotseat().getLayout();
                }
            }
            if (layout == null) {
                layout = findMatchingPageForDragOver(d.dragView, d.x, d.y, false);
            }
            if (layout != mDragTargetLayout) {

                setCurrentDropLayout(layout);
                setCurrentDragOverlappingLayout(layout);

                boolean isInSpringLoadedMode = (mState == State.SPRING_LOADED);
                //如果处于小屏模式(从all apps页面长按进入的wrokspace,这种状态叫SPRING_MODE),那么
                if (isInSpringLoadedMode) {
                    if (mLauncher.isHotseatLayout(layout)) {
                        mSpringLoadedDragController.cancel();
                    } else {
                        //此处其实内部进行了snapToPage及页面切换到mDragTargetLayout页面
                        mSpringLoadedDragController.setAlarm(mDragTargetLayout);
                    }
                }
            }
        } else {

            //此处当处于WorkSpace内部拖动时
            // Test to see if we are over the hotseat otherwise just use the current page
            if (mLauncher.getHotseat() != null && !isDragWidget(d)) {
                mLauncher.getHotseat().getHitRect(r);
                if (r.contains(d.x, d.y)) {//首先判断当前的touchX,Y是否属于HotSeat
                    layout = mLauncher.getHotseat().getLayout();
                }
            }
            if (layout == null) {
                //如果layout为空这说明touch位置不在HotSeat中
                layout = getCurrentDropLayout();
            }

            Log.i(TAG, "onDragOver: "+(layout != mDragTargetLayout));
            //不相等则通知以前并清除状态,此处存疑,判断都是false,待分析
            if (layout != mDragTargetLayout) {
                //Log.i(TAG, "onDragOver: layout != mDragTargetLayout");
                setCurrentDropLayout(layout);
                setCurrentDragOverlappingLayout(layout);
            }

        }

        // Handle the drag over
        if (mDragTargetLayout != null) {
            //经过下面if else 计算mDragViewVisualCenter即为在mDragTargetLayout中的TouchX,Y
            if (mLauncher.isHotseatLayout(mDragTargetLayout)) {
                //mDragViewVisualCenter坐标相对于矫正,内部使用invert逆向+mapPoints
                mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
            } else {
                mapPointFromSelfToChild(mDragTargetLayout, mDragViewVisualCenter, null);
            }

            ItemInfo info = (ItemInfo) d.dragInfo;
            /**
             * 计算出当前在mDragViewVisualCenter最近的Cell位置
             */
            mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1], item.spanX, item.spanY,
                    mDragTargetLayout, mTargetCell);
            //清理addToFolder、createFolder、reOrder
            setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]);
            //此处计算出mDragViewVisualCenter距离最近的Cell也就是mTargetCell的距离
            float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
                    mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
            //获取在mTargetCell位置上的应用图标控件,可能为空
            final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0],
                    mTargetCell[1]);
            //mDragViewVisualCenter与mTargetCell的distance是否小于特定值
            // 且mDragView所代表的控件是application或者shortcut,
            // 那么就通过Alarm机制,创建FolderRing,进入到DRAG_MODE_CREATE_FOLDER模式。
            manageFolderFeedback(info, mDragTargetLayout, mTargetCell,
                    targetCellDistance, dragOverView);

            int minSpanX = item.spanX;
            int minSpanY = item.spanY;
            if (item.minSpanX > 0 && item.minSpanY > 0) {
                minSpanX = item.minSpanX;
                minSpanY = item.minSpanY;
            }
            //判断距离mDragViewVisualCenter最近的mTargetCell是否有应用图标在。
            boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
                    mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
                    item.spanY, child, mTargetCell);

            //如果没有应用图标在
            if (!nearestDropOccupied) {
                //在onDragOver中不断被调用,下面就是显示轮廓的具体方法
                mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
                        (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
                        mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false,
                        d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion());
            }
            //如果当前有应用图标在,而且之前的manageFolderFeedback没成立,那么就需要让mTargetCell挪位置
            else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
                    && !mReorderAlarm.alarmPending() && (mLastReorderX != mTargetCell[0] ||
                    mLastReorderY != mTargetCell[1])) {

                // Otherwise, if we aren't adding to or creating a folder and there's no pending
                // reorder, then we schedule a reorder
                ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
                        minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child);
                mReorderAlarm.setOnAlarmListener(listener);
                mReorderAlarm.setAlarm(REORDER_TIMEOUT);
            }
            //此处按道理不会走,因为如果是创建folder或者加入folder的地方,是不可能没有被占用的
            if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER ||
                    !nearestDropOccupied) {
                if (mDragTargetLayout != null) {
                    mDragTargetLayout.revertTempState();
                }
            }
        }
    }

    方法解释:

        1.获取DragView的中心点mDragViewVisualCenter。

        2.针对SPRING_LOADED模式和NORMAL模式分别进行了一些状态回置。

        3.对DragView的中心点相对于mDragTargetLayout做一些矫正。

        4.计算离DragView中心最近的Cell及它两的距离,此Cell上的应用图标控件(可能在最近Cell出没有图标)。

        5.manageFolderFeedback方法,主要用于创建Folder(显示创建Folder时的圆环ring)。原理是计算DragView与最近Cell的距离是否小于特定值,且DragView所代表的控件是application或者shortcut,那么久显示出创建Folder时的圆环。

        6.判断最近的mTargetCell是否已经被应用图标占用,如果未被占用,则在此位置显示拖拽图标的轮廓(此轮廓在Workspace的startDrag中被创建出来)。如果已经被占用,那么就使用Alarm进行图标重新排序。这里介绍下Alarm,他是个定时器,当时间到时就会执行Listener中的onAlarm方法。

    接下来我们需要进入到应用重新排序的这个AlarmListener中去看看,他是怎么重排的序。

public void onAlarm(Alarm alarm) {
            int[] resultSpan = new int[2];
            mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1], spanX, spanY, mDragTargetLayout, mTargetCell);
            mLastReorderX = mTargetCell[0];
            mLastReorderY = mTargetCell[1];
            //此处就是做动画,让出位置
            mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0],
                (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
                child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);

            if (mTargetCell[0] < 0 || mTargetCell[1] < 0) {
                mDragTargetLayout.revertTempState();
            } else {
                setDragMode(DRAG_MODE_REORDER);
            }

            boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
            //
            mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
                (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
                mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize,
                dragView.getDragVisualizeOffset(), dragView.getDragRegion());
        }

    关于拖拽轮廓显示的方法mDragTargetLayout.visualizeDropLocation()解析可参见我的文章拖拽轮廓显示。关于拖拽过程中的重新排序过程mDragTargetLayout.createArea()的解析可参见我的文章拖拽过程中的排序。这里就不铺开讲了。

     在讲述玩checkTouchState后,我们在来看DragController的handleMoveEvent方法中的checkScrollState,即检查是否需要滑动页面。

private void checkScrollState(int x, int y) {
        final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();
        final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY;
        final DragLayer dragLayer = mLauncher.getDragLayer();
        final boolean isRtl = (dragLayer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
        final int forwardDirection = isRtl ? SCROLL_RIGHT : SCROLL_LEFT;
        final int backwardsDirection = isRtl ? SCROLL_LEFT : SCROLL_RIGHT;

        if (x < mScrollZone) {
            if (mScrollState == SCROLL_OUTSIDE_ZONE) {
                mScrollState = SCROLL_WAITING_IN_ZONE;
                if (mDragScroller.onEnterScrollArea(x, y, forwardDirection)) {
                    dragLayer.onEnterScrollArea(forwardDirection);
                    mScrollRunnable.setDirection(forwardDirection);
                    mHandler.postDelayed(mScrollRunnable, delay);//此处会一直检查scroll状态
                }
            }
        } else if (x > mScrollView.getWidth() - mScrollZone) {
            if (mScrollState == SCROLL_OUTSIDE_ZONE) {
                mScrollState = SCROLL_WAITING_IN_ZONE;
                if (mDragScroller.onEnterScrollArea(x, y, backwardsDirection)) {
                    dragLayer.onEnterScrollArea(backwardsDirection);
                    mScrollRunnable.setDirection(backwardsDirection);
                    mHandler.postDelayed(mScrollRunnable, delay);
                }
            }
        } else {
            clearScrollRunnable();
        }
    }

    可以看出,checkScrollState就是判断当前DragView的x值是否小于mScrollZone(系统默认是20dp范围) ,或者x值是否大于WorkSpace的getWidth()-mScrollZone。通俗的讲,就是判断就是DragView距离左边或右边的距离值是否小于mScrollZone(20dp),如果小于此值,说明当前需要进入滑动页面操作。检查可分为如下几步:

        1.具体来看,滑动页面需要判断mDragScroller即WorkSpace的onEnterScrollArea方法是否成立。

@Override
    public boolean onEnterScrollArea(int x, int y, int direction) {
        // Ignore the scroll area if we are dragging over the hot seat
        boolean isPortrait = !LauncherApplication.isScreenLandscape(getContext());
        //如果边界检查发现x值边界位于HotSeat内,那么就不进行页面切换
        if (mLauncher.getHotseat() != null && isPortrait) {
            Rect r = new Rect();
            mLauncher.getHotseat().getHitRect(r);
            if (r.contains(x, y)) {
                return false;
            }
        }

        boolean result = false;
        //非小屏模式
        if (!isSmall() && !mIsSwitchingState) {
            mInScrollArea = true;
            //页面滑动方向判断。
            final int page = getNextPage() +
                       (direction == DragController.SCROLL_LEFT ? -1 : 1);

            // 清理状态
            setCurrentDropLayout(null);
            //判断page是否符合workspace数量要求
            if (0 <= page && page < getChildCount()) {
                CellLayout layout = (CellLayout) getChildAt(page);
                //对page进行重绘。
                setCurrentDragOverlappingLayout(layout);

                // Workspace is responsible for drawing the edge glow on adjacent pages,
                // so we need to redraw the workspace when this may have changed.
                invalidate();
                result = true;
            }
        }

        //如果小屏模式则返回false
        return result;
    }

        2.成立的话就调用DragLayer的onEnterScrollArea进行DragLayer重绘,以下DragLayer是具体调用。

void onEnterScrollArea(int direction) {
        mInScrollArea = true;
        invalidate();
    }

        3.执行mScrollRunnable进行具体的页面切换。其内部调用WokSpace的scrollLeft、scrollRight执行滑动切换页面,此Runnable具体如下:

private class ScrollRunnable implements Runnable {
        private int mDirection;

        ScrollRunnable() {
        }

        public void run() {
            if (mDragScroller != null) {
                //根据滑动方向具体执行滑动流程
                if (mDirection == SCROLL_LEFT) {
                    mDragScroller.scrollLeft();
                } else {
                    mDragScroller.scrollRight();
                }
                mScrollState = SCROLL_OUTSIDE_ZONE;
                mDistanceSinceScroll = 0;
                //重绘Workspace和DragLayer
                mDragScroller.onExitScrollArea();
                mLauncher.getDragLayer().onExitScrollArea();
                //如果当前还处于拖拽过程中,那么持续进行滑动检查
                if (isDragging()) {
                    // Check the scroll again so that we can requeue the scroller if necessary
                    checkScrollState(mLastTouch[0], mLastTouch[1]);
                }
            }
        }

        void setDirection(int direction) {
            mDirection = direction;
        }
    }

    以下是WorkSpace的页面切换方法。可以看到,内部判断如果不是小屏模式就执行父类PagedView的scrollLeft或scrollRight(父类方法内部调用snapToPage,这里不再展开表述),同时如果当前页面有处于open状态的文件夹,则执行文件夹的关闭操作。

@Override
    public void scrollLeft() {
        //判断是否是小屏模式,如果不是最终会调用snapToPage方法
        if (!isSmall() && !mIsSwitchingState) {
            super.scrollLeft();
        }
        //如果当前页面有打开的Folder,则需要关闭folder
        Folder openFolder = getOpenFolder();
        if (openFolder != null) {
            openFolder.completeDragExit();
        }
    }

    @Override
    public void scrollRight() {
        if (!isSmall() && !mIsSwitchingState) {
            super.scrollRight();
        }
        Folder openFolder = getOpenFolder();
        if (openFolder != null) {
            openFolder.completeDragExit();
        }
    }

    至此handleTouchMove方法即DragView的Move事件我们已经了解完了,下面再来看看up事件,显然TouchUp中我们应该根据DragView的拖拽位置来确定应用的重新排序。以下是DragController的onTouchEvent方法:

public boolean onTouchEvent(MotionEvent ev) {
        if (!mDragging) {
            return false;
        }

        // Update the velocity tracker
        acquireVelocityTrackerAndAddMovement(ev);

        final int action = ev.getAction();
        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
        final int dragLayerX = dragLayerPos[0];
        final int dragLayerY = dragLayerPos[1];

        switch (action) {
        case MotionEvent.ACTION_DOWN:
            // Remember where the motion event started
            mMotionDownX = dragLayerX;
            mMotionDownY = dragLayerY;

            if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) {
                mScrollState = SCROLL_WAITING_IN_ZONE;
                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
            } else {
                mScrollState = SCROLL_OUTSIDE_ZONE;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            handleMoveEvent(dragLayerX, dragLayerY);
            break;
        case MotionEvent.ACTION_UP:
            // Ensure that we've processed a move event at the current pointer location.
            handleMoveEvent(dragLayerX, dragLayerY);
            mHandler.removeCallbacks(mScrollRunnable);

            if (mDragging) {
                //是否是快速拖拽删除如果有返回值,则是快速拖拽删除,如果不是则进入drop
                PointF vec = isFlingingToDelete(mDragObject.dragSource);
                if (vec != null) {
                    dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
                } else {
                    drop(dragLayerX, dragLayerY);
                }
            }
            endDrag();
            break;
        case MotionEvent.ACTION_CANCEL:
            mHandler.removeCallbacks(mScrollRunnable);
            cancelDrag();
            break;
        }

        return true;
    }

    可以看见在up事件中:

        1.DragView移动到upX,upY坐标。

        2.且移除了WorkSpace的页面滑动监听。

      3.并且根据mDragging状态(此时一般为true),及mVelocityTracker所获取的拖拽速度决定当前是删除拖动应用图标(dropOnFlingToDeleteTarget()方法),还是按照DragView的拖拽位置重新对应用图标进行排序(onDrop()方法)。

    在onDrop方法中先进行判断当前DragView所处在的DragTarget能否accept当前的拖拽排序。如果能接手,则进而执行WorkSpace的onDrop方法。以下即是DragController的drop方法:

private void drop(float x, float y) {
        final int[] coordinates = mCoordinatesTemp;
        final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);

        mDragObject.x = coordinates[0];
        mDragObject.y = coordinates[1];
        boolean accepted = false;
        if (dropTarget != null) {
            mDragObject.dragComplete = true;
            dropTarget.onDragExit(mDragObject);
            if (dropTarget.acceptDrop(mDragObject)) {
                dropTarget.onDrop(mDragObject);
                accepted = true;
            }
        }
        mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted);
    }

    那么WorkSpace有是如何判断能否执行onDrop排序呢,我们走进acceptDrop方法瞧瞧:

public boolean acceptDrop(DragObject d) {
        // If it's an external drop (e.g. from All Apps), check if it should be accepted
        CellLayout dropTargetLayout = mDropToLayout;
        if (d.dragSource != this) {
            // Don't accept the drop if we're not over a screen at time of drop
            if (dropTargetLayout == null) {
                return false;
            }
            if (!transitionStateShouldAllowDrop()) return false;

            mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset,
                    d.dragView, mDragViewVisualCenter);

            // We want the point to be mapped to the dragTarget.
            if (mLauncher.isHotseatLayout(dropTargetLayout)) {
                mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
            } else {
                mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null);
            }

            int spanX = 1;
            int spanY = 1;
            if (mDragInfo != null) {
                final CellLayout.CellInfo dragCellInfo = mDragInfo;
                spanX = dragCellInfo.spanX;
                spanY = dragCellInfo.spanY;
            } else {
                final ItemInfo dragInfo = (ItemInfo) d.dragInfo;
                spanX = dragInfo.spanX;
                spanY = dragInfo.spanY;
            }

            int minSpanX = spanX;
            int minSpanY = spanY;
            if (d.dragInfo instanceof PendingAddWidgetInfo) {
                minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX;
                minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY;
            }

            mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1], minSpanX, minSpanY, dropTargetLayout,
                    mTargetCell);
            float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                    mDragViewVisualCenter[1], mTargetCell);
            //创建Folder
            if (willCreateUserFolder((ItemInfo) d.dragInfo, dropTargetLayout,
                    mTargetCell, distance, true)) {
                return true;
            }
            //加入已存在的Folder
            if (willAddToExistingUserFolder((ItemInfo) d.dragInfo, dropTargetLayout,
                    mTargetCell, distance)) {
                return true;
            }

            int[] resultSpan = new int[2];
            //腾出区域
            mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
                    null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP);
            //大于0表示可以腾出区域
            boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;

            //如果没有mTargetCell不可腾出区域,那么就返回false。
            if (!foundCell) {
                // Don't show the message if we are dropping on the AllApps button and the hotseat
                // is full
                boolean isHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
                if (mTargetCell != null && isHotseat) {
                    Hotseat hotseat = mLauncher.getHotseat();
                    if (hotseat.isAllAppsButtonRank(
                            hotseat.getOrderInHotseat(mTargetCell[0], mTargetCell[1]))) {
                        return false;
                    }
                }

                mLauncher.showOutOfSpaceMessage(isHotseat);
                return false;
            }
        }
        return true;
    }

    可以看到,方法内部重新获取了DragView的中心点,矫正之后获取距离DragView最近的mTargetCell位置及距离。进而进行了以下三种判断。

      1.如果当前mTargetCell上已经有应用图标且mTargetCell与DragView所代表的的控件内容属于ShortcutInfo或者ApplicationInfo类型。那么就创建可以Folder。

        2.如果当前mTargetCell是FolderIcon且FolderIcon内部child的数量没有达到mMaxNumItems(16)那么就可以加入已存在的Folder。

        3.调用createaArea查看是否能找到策略腾出mTargetCell(如果数组中的CellX,CellY大于0,表示能够腾出)。

    如果以上三种判断任意一种能够成立,那么就表示WorkSpace能够接手当前DragView的应用排序。执行WorkSpace的onDrop方法:

public void onDrop(final DragObject d) {
        mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView,
                mDragViewVisualCenter);

        CellLayout dropTargetLayout = mDropToLayout;

        // We want the point to be mapped to the dragTarget.
        if (dropTargetLayout != null) {
            if (mLauncher.isHotseatLayout(dropTargetLayout)) {
                mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
            } else {
                mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null);
            }
        }

        int snapScreen = -1;
        boolean resizeOnDrop = false;
        if (d.dragSource != this) {
            final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1] };
            //表示从all apps页面拖拽过来,或者FolderIcon拖拽过来。
            onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d);
        } else if (mDragInfo != null) {
            final View cell = mDragInfo.cell;

            Runnable resizeRunnable = null;
            if (dropTargetLayout != null) {
                // Move internally
                boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout);
                boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
                long container = hasMovedIntoHotseat ?
                        LauncherSettings.Favorites.CONTAINER_HOTSEAT :
                        LauncherSettings.Favorites.CONTAINER_DESKTOP;
                int screen = (mTargetCell[0] < 0) ?
                        mDragInfo.screen : indexOfChild(dropTargetLayout);
                int spanX = mDragInfo != null ? mDragInfo.spanX : 1;
                int spanY = mDragInfo != null ? mDragInfo.spanY : 1;
                // First we find the cell nearest to point at which the item is
                // dropped, without any consideration to whether there is an item there.

                mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int)
                        mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell);
                float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                        mDragViewVisualCenter[1], mTargetCell);

                //创造Folder,主要判断流程还会根据acceptDrop走,最终调用Launcher.java的addFolder方法添加FolderIcon到WorkSpace(在addFolder中又使用addInScreen方法)中
                if (!mInScrollArea && createUserFolderIfNecessary(cell, container,
                        dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) {
                    return;
                }
                //添加到已存在的Folder中,主要是添加到FoldeInfo的contents中
                if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
                        distance, d, false)) {
                    return;
                }

                // Aside from the special case where we're dropping a shortcut onto a shortcut,
                // we need to find the nearest cell location that is vacant
                ItemInfo item = (ItemInfo) d.dragInfo;
                int minSpanX = item.spanX;
                int minSpanY = item.spanY;
                if (item.minSpanX > 0 && item.minSpanY > 0) {
                    minSpanX = item.minSpanX;
                    minSpanY = item.minSpanY;
                }

                int[] resultSpan = new int[2];
                //能否在最近mTargetCell位置找出策略来腾出位置
                mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0],
                        (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell,
                        mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);

                boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;

                // if the widget resizes on drop
                if (foundCell && (cell instanceof AppWidgetHostView) &&
                        (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) {
                    resizeOnDrop = true;
                    item.spanX = resultSpan[0];
                    item.spanY = resultSpan[1];
                    AppWidgetHostView awhv = (AppWidgetHostView) cell;
                    AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
                            resultSpan[1]);
                }

                if (mCurrentPage != screen && !hasMovedIntoHotseat) {
                    snapScreen = screen;
                    snapToPage(screen);
                }

                if (foundCell) {
                    final ItemInfo info = (ItemInfo) cell.getTag();
                    if (hasMovedLayouts) {
                        // Reparent the view
                        //移除原来的DragView所代表的的应用图标,并重新添加添加到WorkSpace的mTargetCell位置
                        getParentCellLayoutForView(cell).removeView(cell);
                        addInScreen(cell, container, screen, mTargetCell[0], mTargetCell[1],
                                info.spanX, info.spanY);
                    }

                    // update the item's position after drop
                    CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
                    lp.cellX = lp.tmpCellX = mTargetCell[0];
                    lp.cellY = lp.tmpCellY = mTargetCell[1];
                    lp.cellHSpan = item.spanX;
                    lp.cellVSpan = item.spanY;
                    lp.isLockedToGrid = true;
                    cell.setId(LauncherModel.getCellLayoutChildId(container, mDragInfo.screen,
                            mTargetCell[0], mTargetCell[1], mDragInfo.spanX, mDragInfo.spanY));

                    if (container != LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
                            cell instanceof LauncherAppWidgetHostView) {
                        final CellLayout cellLayout = dropTargetLayout;
                        // We post this call so that the widget has a chance to be placed
                        // in its final location

                        final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
                        AppWidgetProviderInfo pinfo = hostView.getAppWidgetInfo();
                        if (pinfo != null &&
                                pinfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) {
                            final Runnable addResizeFrame = new Runnable() {
                                public void run() {
                                    DragLayer dragLayer = mLauncher.getDragLayer();
                                    dragLayer.addResizeFrame(info, hostView, cellLayout);
                                }
                            };
                            resizeRunnable = (new Runnable() {
                                public void run() {
                                    if (!isPageMoving()) {
                                        addResizeFrame.run();
                                    } else {
                                        mDelayedResizeRunnable = addResizeFrame;
                                    }
                                }
                            });
                        }
                    }

                    LauncherModel.moveItemInDatabase(mLauncher, info, container, screen, lp.cellX,
                            lp.cellY);
                } else {
                    // If we can't find a drop location, we return the item to its original position
                    CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
                    mTargetCell[0] = lp.cellX;
                    mTargetCell[1] = lp.cellY;
                    CellLayout layout = (CellLayout) cell.getParent().getParent();
                    layout.markCellsAsOccupiedForView(cell);
                }
            }

            final CellLayout parent = (CellLayout) cell.getParent().getParent();
            final Runnable finalResizeRunnable = resizeRunnable;
            // Prepare it to be animated into its new position
            // This must be called after the view has been re-parented
            final Runnable onCompleteRunnable = new Runnable() {
                @Override
                public void run() {
                    mAnimatingViewIntoPlace = false;
                    updateChildrenLayersEnabled(false);
                    if (finalResizeRunnable != null) {
                        finalResizeRunnable.run();
                    }
                }
            };
            mAnimatingViewIntoPlace = true;
            if (d.dragView.hasDrawn()) {
                final ItemInfo info = (ItemInfo) cell.getTag();
                if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) {
                    int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE :
                            ANIMATE_INTO_POSITION_AND_DISAPPEAR;
                    animateWidgetDrop(info, parent, d.dragView,
                            onCompleteRunnable, animationType, cell, false);
                } else {
                    int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION;
                    mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
                            onCompleteRunnable, this);
                }
            } else {
                d.deferDragViewCleanupPostAnimation = false;
                cell.setVisibility(VISIBLE);//最终把隐藏掉的cell显示出来了
            }
            parent.onDropChild(cell);
        }
    }

       上边的流程其实和acceptDrop中的判断流程类似,只不过判断成功之后执行了添加操作。

    至此,算是分析完了WorkSpace页面的应用图标长按拖拽的分析,接下来的下篇我们将分析FolderIcon和AppsCustomizePagedView页面的长按拖拽功能。

   

    

    

猜你喜欢

转载自blog.csdn.net/qq_33859911/article/details/80658022