墨香带你学Launcher之(六)--拖拽

上一章墨香带你学Launcher之(五)-Workspace滑动介绍了workspace的形成以及滑动过程处理,本章我们介绍桌面图标的拖拽过程,这个拖拽过程设计非常巧妙,设计的东西很多,所以我尽量详细讲解。

由于十一回来一直上火,到最近才渐好,工作相对也较忙,所以一直拖到现在才开始写这篇文章,在留言里也看到很多人关注我的博客文章,非常感激,也有朋友一直期待,所以在此说声抱歉,来的有点晚,所以趁着今天有空补上这篇文章。

对于双层桌面,拖拽主要有几个事件,一个是从二级菜单的所有应用界面中的图标(或者小插件)拖拽到桌面上,另外一个是在桌面上或者文件夹中的图标拖拽到别的桌面或者文件夹中,还有就是拖拽桌面上的CellLayout进行排序,这个内容在前一章已经讲过了,想看的可以看前一章,剩下的这两种我们分开来讲。拖拽过程中还有些名词,比如DropTarget、DragObject、DragView、DragSource等,我会在讲解过程中在解释。下面我们开始看第一个过程:

桌面上的图标拖拽


我们知道图标拖拽的触发条件是长按事件,因此我们要找到长按事件的过程,长按事件的代码在Launcher.java这个类中,代码如下:

 public boolean onLongClick(View v) {
        // 如果不允许拖拽则返回
        if (!isDraggingEnabled()) return false;
        // 如果桌面锁定返回
        if (isWorkspaceLocked()) return false;
        // 如果没有在桌面显示状态返回
        if (mState != State.WORKSPACE) return false;
        
        // 显示所有图标的按钮,显示所有图标界面
        if (v == mAllAppsButton) {
            onLongClickAllAppsButton(v);
            return true;
        }
        
        if (v instanceof Workspace) {
            if (!mWorkspace.isInOverviewMode()) {
                if (!mWorkspace.isTouchActive()) {
                    showOverviewMode(true);
                    mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                            HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
                    return true;
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }

        CellLayout.CellInfo longClickCellInfo = null;
        View itemUnderLongClick = null;
        if (v.getTag() instanceof ItemInfo) {
            ItemInfo info = (ItemInfo) v.getTag();
            longClickCellInfo = new CellLayout.CellInfo(v, info);
            itemUnderLongClick = longClickCellInfo.cell;
            resetAddInfo();
        }

        // The hotseat touch handling does not go through Workspace, and we always allow long press
        // on hotseat items.
        final boolean inHotseat = isHotseatLayout(v);
        if (!mDragController.isDragging()) {
            if (itemUnderLongClick == null) {
                // User long pressed on empty space
                mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                        HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
                if (mWorkspace.isInOverviewMode()) {
                    mWorkspace.startReordering(v);
                } else {
                    showOverviewMode(true);
                }
            } else {
                    ...
                    mWorkspace.startDrag(longClickCellInfo);
                      
                    ...
            }
        }
        return true;
    }
    

在if(v instanceof Workspace)这个if语句中,如果长按的是桌面的空白区域,则调用showOverviewMode(true)来显示桌面预览状态,也就是桌面缩小,显示多个CellLayout的状态,这时再长按单个CellLayout可以进行拖拽排序。接着是if (v.getTag() instanceof ItemInfo) 这个判断,这个就是说你当前长按的是app的图标或者文件夹,这时会创建一个CellLayout.CellInfo对象,这个对象是对你要拖拽的View包含对象的信息存储也就相当于复制了一份,然后创建引用itemUnderLongClick,这个是你正在长按的图标,再往下,判断如果没有拖拽事件执行就开始判断执行拖拽事件,下面的判断也是如果CellLayout.CellInfo这个对象为空,则执行桌面预览或者排序事件,如果不为空,则说明长按的是图标,那么此时要判断你拖动的不是文件夹或者不是显示所有图标的那个按钮,开始执行
mWorkspace.startDrag(longClickCellInfo)方法,下面我们看这个方法的代码:

public void startDrag(CellLayout.CellInfo cellInfo) {
        startDrag(cellInfo, false);
    }

    @Override
    public void startDrag(CellLayout.CellInfo cellInfo, boolean accessible) {
        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);

        beginDragShared(child, this, accessible);
    }

在第一个startDrag方法中调用的是下面的那个startDrag方法,在这个方法中调用了对于你长按的图标进行了隐藏,那么隐藏怎么拖拽图标呢,这里先不解释,然后调用layout.prepareChildForDrag(child)方法,这个方法其实就是对于你刚才长按的那个View的位置进行储存,也就是他占用的位置,这个位置虽然图标隐藏了但是在它被放到其他地方前还是被它占用的,然后调用
beginDragShared方法,这个方法中传入了一个this,点击进入会看到是参数DragSource,也就是workspace,接着看最终调用下面方法:

public void beginDragShared(View child, Point relativeTouchPos, DragSource source,
                                boolean accessible) {
       
        ...

        // The outline is used to visualize where the item will land if dropped
        mDragOutline = createDragOutline(child, DRAG_BITMAP_PADDING);

        ...
        
        final Bitmap b = createDragBitmap(child, padding);

        ...
        
        if (child instanceof BubbleTextView) {
            // 这里主要是计算拖拽的起始位置
            ...
        }

        ...

        if (child.getParent() instanceof ShortcutAndWidgetContainer) {
            mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
        }

        // start a drag
        DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible);
        dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());

        b.recycle();
    }

首先通过createDragOutline方法生成mDragOutline,这是你要拖动的View的边框,当你拖动View时,这个边框会在你拖动的附近允许你防止该View的位置显示出来,以表示你可以将View放置的地方,创建代码比较简单,自己看一下就好了,我们往下看,紧接着就是调用createDragBitmap这个方法创建你上面隐藏的那个View的Bitmap,过程自己看一下,很简单,然后开始计算拖拽的起始位置,然后调用mDragController.startDrag方法开始拖拽,代码如下:

public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
                              DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
                              float initialDragViewScale, boolean accessible) {
        ...

        for (DragListener listener : mListeners) {
            listener.onDragStart(source, dragInfo, dragAction);
        }

        ...

        mDragging = true;
        mIsAccessibleDrag = accessible;

        mDragObject = new DropTarget.DragObject();

        mDragObject.dragComplete = false;
        if (mIsAccessibleDrag) {
            // For an accessible drag, we assume the view is being dragged from the center.
            mDragObject.xOffset = b.getWidth() / 2;
            mDragObject.yOffset = b.getHeight() / 2;
            mDragObject.accessibleDrag = true;
        } else {
            mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
            mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
        }

        mDragObject.dragSource = source;
        mDragObject.dragInfo = dragInfo;

        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));
        }

        mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        dragView.show(mMotionDownX, mMotionDownY);
        handleMoveEvent(mMotionDownX, mMotionDownY);
        return dragView;
    }

我们首先看for循环中的回调函数,实现回调函数的有下面几个类,

launcher01.png

我们主要是在桌面上拖拽,主要是桌面中的处理,我们看下代码:

@Override
    public void onDragStart(final DragSource source, Object info, int dragAction) {
        mIsDragOccuring = true;
        updateChildrenLayersEnabled(false);
        mLauncher.lockScreenOrientation();
        mLauncher.onInteractionBegin();
        // Prevent any Un/InstallShortcutReceivers from updating the db while we are dragging
        InstallShortcutReceiver.enableInstallQueue();

        if (mAddNewPageOnDrag) {
            mDeferRemoveExtraEmptyScreen = false;
            addExtraEmptyScreenOnDrag();
        }
    }

主要是锁定屏幕,判断是否添加新的空白屏。

我们接着分析上面的代码,通过mDragObject = new DropTarget.DragObject()创建DragObject对象,储存相关的信息,然后生成DragView对象,通过dragView.show(mMotionDownX, mMotionDownY)方法将DragView对象添加到你手放置的位置,此时可以知道你拖拽的原来是DragView对象,最后调用handleMoveEvent(mMotionDownX, mMotionDownY)方法来处理位置移动,我们看一下代码:

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

        // Drop on someone?
        final int[] coordinates = mCoordinatesTemp;
        DropTarget dropTarget = findDropTarget(x, y, coordinates);
        mDragObject.x = coordinates[0];
        mDragObject.y = coordinates[1];
        checkTouchMove(dropTarget);

        // Check if we are hovering over the scroll areas
        mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y);
        mLastTouch[0] = x;
        mLastTouch[1] = y;
        checkScrollState(x, y);
    }

mDragObject.dragView.move(x, y)方法主要是将生成的DragView移动到相应的位置,然后查找DropTarget,这里不再贴代码,我简单说一下就可以了,这里的DropTarget是你拖动你的View要放入的地方,比如folder,workspace等,在DragController中存在一个DropTarget的列表,然后查找到对应的DropTarget对象,接着调用checkTouchMove方法,来处理相应的结果,我们看一下代码:

    private void checkTouchMove(DropTarget dropTarget) {
        if (dropTarget != null) {
            if (mLastDropTarget != dropTarget) {
                if (mLastDropTarget != null) {
                    mLastDropTarget.onDragExit(mDragObject);
                }
                dropTarget.onDragEnter(mDragObject);
            }
            dropTarget.onDragOver(mDragObject);
        } else {
            if (mLastDropTarget != null) {
                mLastDropTarget.onDragExit(mDragObject);
            }
        }
        mLastDropTarget = dropTarget;
    }

我们知道DrapTarget是将图标拖拽到的目的低对象,这个做了判断,如果DropTarget不为空,也就是找到了目标,那么判断是不是与上次的DropTarget相同,这个怎么解释,我们看最后一行代码,mLastDropTarget在这里赋值,也就是第一次为null,那么肯定不等,此时判断mLastDropTarget为null,直接走dropTarget.onDragEnter,也就是进入动作,当第二次时会先执行mLastDropTarget.onDragExit动作然后执行dropTarget.onDragOver动作,也就是先退出上一个目标,然后进入下一个目标,当DropTarget为null时,会判断mLastDropTarget是否为null,如果为null,那么就执行mLastDropTarget.onDragExit动作,最后执行赋值,那么onDragEnter、onDragOver、onDragExit都是一个怎么作用呢,我们通过log分析一下,如下图:

launcher02.png
launcher03.png
launcher04.png

这个是我从桌面拖动一个图标进入文件夹,直到文件夹打开,然后直接再拖拽到桌面的过程,首先起始位置是DrogController的startDrag方法,然后调用Workspace的onDragStart,然后是onDragEnter,也就是计入Workspace,进入CellLayout,然后调用Workspace的onDragOver,也就是在Workspace中拖拽移动的过程,中间过程比较多,我省略了一部分,然后看第二张图,当我进入Folder的时候,先退出Workspace,然后退出CellLayout,然后进入Folder,然后执行onDragOver,在Folder中拖动,接着看第三张图,当我离开Folder的时候,先执行Folder的onDragExit,然后进入Workspace,然后进入CellLayout,然后在Workspace中拖拽,最后放置到Workspace中,先执行DrogController的drop函数,然后退出Workspace,退出CellLayout,然后Workspace接受拖拽的View,然后释放,然后调用Workspace的onDragEnd然后执行Folder的onDragEnd函数,最后结束拖拽过程。

从上面的整个流程可以看到上面三个函数的具体过程,这样就很好理解了。我们接着分析三个函数分别作了什么,首先是onDragEnter:

由下图可以onDragEnter有三个地方实现,Workspace、Folder和ButtonDropTarget,前两个很熟,最后一个是什么呢,这个是你在桌面长按图标时候在桌面顶部出现的删除卸载那个按钮,那么在这三个地方如何实现的,下面我们分别看一下,

launcher05.png

首先是Workspace中:

    @Override
    public void onDragEnter(DragObject d) {
    
        mCreateUserFolderOnDrop = false;
        mAddToExistingFolderOnDrop = false;

        mDropToLayout = null;
        CellLayout layout = getCurrentDropLayout();
        setCurrentDropLayout(layout);
        setCurrentDragOverlappingLayout(layout);

        if (!workspaceInModalState()) {
            mLauncher.getDragLayer().showPageHints();
        }
    }

首先获取当前图标所在的CellLayout,然后调用setCurrentDropLayout方法,代如下:

    void setCurrentDropLayout(CellLayout layout) {
        if (mDragTargetLayout != null) {
            mDragTargetLayout.revertTempState();
            mDragTargetLayout.onDragExit();
        }
        mDragTargetLayout = layout;
        if (mDragTargetLayout != null) {
            mDragTargetLayout.onDragEnter();
        }
        cleanupReorder(true);
        cleanupFolderCreation();
        setCurrentDropOverCell(-1, -1);
    }

我们查看代码可知,mDragTargetLayout是一个CellLayout,只在此方法中赋值,因此第一次进入为null,所以会执行CellLayout的onDragEnter方法,这是就我们上面看到的那个顺序,在workspace的onDragEnter方法后执行CellLayout的相应方法,然后是一些清理工作,这里不再详细讲解。

接着是Workspace的onDragOver,代码如下:

    public void onDragOver(DragObject d) {
       
        ...
        
        CellLayout layout = null;
        ItemInfo item = (ItemInfo) d.dragInfo;

        // 获取拖拽View的中心点坐标
        mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);

        final View child = (mDragInfo == null) ? null : mDragInfo.cell;
        // Identify whether we have dragged over a side page
        if (workspaceInModalState()) {

            //获取当前的CellLayout并处理
            
        } else {
            //获取当前的CellLayout
        }

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

            ItemInfo info = (ItemInfo) d.dragInfo;

            int minSpanX = item.spanX;
            int minSpanY = item.spanY;
            if (item.minSpanX > 0 && item.minSpanY > 0) {
                minSpanX = item.minSpanX;
                minSpanY = item.minSpanY;
            }

            // 查找最近的位置
            mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1], minSpanX, minSpanY,
                    mDragTargetLayout, mTargetCell);
            int reorderX = mTargetCell[0];
            int reorderY = mTargetCell[1];

            setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]);

            // 计算拖拽View中心到最近摆放拖拽view的位置的距离
            float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
                    mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);

            final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0],
                    mTargetCell[1]);

            manageFolderFeedback(info, mDragTargetLayout, mTargetCell,
                    targetCellDistance, dragOverView, d.accessibleDrag);

            // 判断最近的位置是否被占用
            boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
                            mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
                    item.spanY, child, mTargetCell);

            if (!nearestDropOccupied) {
                // 如果没有占用,就限制边框到那里
                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());
            } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
                    && !mReorderAlarm.alarmPending() && (mLastReorderX != reorderX ||
                    mLastReorderY != reorderY)) {

                int[] resultSpan = new int[2];
                
                // 如果没有位置,并且是排序状态,则进行排序处理
                mDragTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                        (int) mDragViewVisualCenter[1], minSpanX, minSpanY, item.spanX, item.spanY,
                        child, mTargetCell, resultSpan, CellLayout.MODE_SHOW_REORDER_HINT);

                // 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);
            }

            // 如果是创建文件夹或者放置到文件夹状态
            if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER ||
                    !nearestDropOccupied) {
                if (mDragTargetLayout != null) {
                    mDragTargetLayout.revertTempState();
                }
            }
        }
    }

上面代码比较多,不在详细解释,里面添加了相关注释,主要是说一下拖拽过程,在拖拽的时候,要时时计算拖拽图标和最近位置的距离,并且判断最近位置是否被占用,如果没有被占用,则显示图标的轮廓框,如果被占用了,就要判断状体,如果是排序状态,也就是会把当前被占用位置的图标挤跑,如果是创建文件夹状态,则会创建文件夹,不会挤跑图标,如果是文件夹并且是添加文件夹状态,则显示添加文件夹的效果。

最后是Workspace的onDragExit方法,这个方法内容不多,其实没太多操作,主要是清楚一些状态,代码就不贴了,自己看看就知道了。

在onDragEnd方法中的主要是退出拖拽,清除添加的空屏幕。

其次,我们看Folder的onDragEnter方法,代码如下:

    @Override
    public void onDragEnter(DragObject d) {
        XLog.e(XLog.getTag(),XLog.TAG_GU);
        mPrevTargetRank = -1;
        mOnExitAlarm.cancelAlarm();
        // Get the area offset such that the folder only closes if half the drag icon width
        // is outside the folder area
        mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset;
    }

代码很简单,没什么操作。

然后是onDragOver和onDragExit方法:

    @Override
    public void onDragOver(DragObject d) {
        XLog.e(XLog.getTag(),XLog.TAG_GU);
        onDragOver(d, REORDER_DELAY);
    }
    
    @Thunk void onDragOver(DragObject d, int reorderDelay) {

        if (mScrollPauseAlarm.alarmPending()) {
            return;
        }
        final float[] r = new float[2];
        mTargetRank = getTargetRank(d, r);

        if (mTargetRank != mPrevTargetRank) {
            mReorderAlarm.cancelAlarm();
            mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
            mReorderAlarm.setAlarm(REORDER_DELAY);
            mPrevTargetRank = mTargetRank;
        }

        float x = r[0];
        int currentPage = mContent.getNextPage();

        float cellOverlap = mContent.getCurrentCellLayout().getCellWidth()
                * ICON_OVERSCROLL_WIDTH_FACTOR;
        boolean isOutsideLeftEdge = x < cellOverlap;
        boolean isOutsideRightEdge = x > (getWidth() - cellOverlap);

        if (currentPage > 0 && (mContent.mIsRtl ? isOutsideRightEdge : isOutsideLeftEdge)) {
            showScrollHint(DragController.SCROLL_LEFT, d);
        } else if (currentPage < (mContent.getPageCount() - 1)
                && (mContent.mIsRtl ? isOutsideLeftEdge : isOutsideRightEdge)) {
            showScrollHint(DragController.SCROLL_RIGHT, d);
        } else {
            mOnScrollHintAlarm.cancelAlarm();
            if (mScrollHintDir != DragController.SCROLL_NONE) {
                mContent.clearScrollHint();
                mScrollHintDir = DragController.SCROLL_NONE;
            }
        }
    }

在这里面主要是一个滑动过程,文件夹中的应用超过一定数量也是分页的,因此你在拖拽过程中需要判断是否滑动翻页。代码量比较少,可以自己看看。
在onDragExit的时候要关闭文件夹,清除相应的拖拽监听。

最后是ButtonDropTarget,onDragEnter方法主要是改变颜色和效果,onDragOver没有做任何处理,onDragExit方法是清除效果,onDragEnd也没有效果只是重置了一个标签。

整个Workspace中的拖拽代码量很大,但是只要抓住逻辑相对也是比较简单,我们在上面分析了所有状态操作,但是还存在一个问题没有讲,就是如果触发的拖拽过程,其实整个是在DragController中的onTouch方法中触发的,也是就在这个方法中调用的handleMoveEvent方法,我们看一下,而DragController中的onTouch方法是在DragLayer中的onTouch方法中调用的,因为整个workspace是存在DragLayer中,我们看一下整个onTouch方法:

    public boolean onTouchEvent(MotionEvent 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:
                
                ...
                
                handleMoveEvent(dragLayerX, dragLayerY);
                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) {
                    PointF vec = isFlingingToDelete(mDragObject.dragSource);
                    if (!DeleteDropTarget.supportsDrop(mDragObject.dragInfo)) {
                        vec = null;
                    }
                    if (vec != null) {
                        dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
                    } else {
                        drop(dragLayerX, dragLayerY);
                    }
                }
                endDrag();
                break;
            case MotionEvent.ACTION_CANCEL:
                mHandler.removeCallbacks(mScrollRunnable);
                cancelDrag();
                break;
        }

        return true;
    }

里面好几处调用了handleMoveEvent方法,就是不断滑动过程中不断的处理拖拽事件,所以看到是连续的,这里面有两个方法我们看一下,首先是dropOnFlingToDeleteTarget方法,整个方法代码也很简单,主要是拖拽到删除按钮时处理过程,还有一个是drop方法,代码如下:

    private void drop(float x, float y) {
        XLog.e(XLog.getTag(),XLog.TAG_GU);
        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);
    }

首先是调用findDropTarget方法来查找放置拖拽View的目标对象,然后判断目标对象是否可以接受该View,如果可以接受调用ondrop方法,这个方法有四个地方实现,如下图:

launcher06.png

首先是Workspace中,代码:

    public void onDrop(final DragObject d) {
        
        mDragViewVisualCenter = d.getVisualCenter(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);
            }
        }

        // 如果拖拽对象不是来自Workspace
        if (d.dragSource != this) {
            final int[] touchXY = new int[]{(int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1]};
            onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d);
        } else if (mDragInfo != null) {
            final View cell = mDragInfo.cell;

            Runnable resizeRunnable = null;
            if (dropTargetLayout != null && !d.cancelled) {
            
                ...
            
                // 查找位置
                mTargetCell = findNearestArea((int) mDragViewVisualCenter[0], (int)
                        mDragViewVisualCenter[1], spanX, spanY, dropTargetLayout, mTargetCell);
                // 计算距离
                float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
                        mDragViewVisualCenter[1], mTargetCell);

                // If the item being dropped is a shortcut and the nearest drop
                // cell also contains a shortcut, then create a folder with the two shortcuts.
                if (!mInScrollArea && createUserFolderIfNecessary(cell, container,
                        dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) {
                    return;
                }

                // 是否需要加入文件夹,如果需要则加入文件夹
                if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
                        distance, d, false)) {
                    return;
                }

                ...
                
                if (getScreenIdForPageIndex(mCurrentPage) != screenId && !hasMovedIntoHotseat) {
                    snapScreen = getPageIndexForScreenId(screenId);
                    snapToPage(snapScreen);
                }

                if (foundCell) {
                    final ItemInfo info = (ItemInfo) cell.getTag();
                    if (hasMovedLayouts) {
                    
                        ...
                    
                        //添加到相应的CellLayout中
                        addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1],
                                info.spanX, info.spanY);
                    }

                    // 放置完成后更新位置
                    
                    ...
                    
                    // 修改数据库
                    LauncherModel.modifyItemInDatabase(mLauncher, info, container, screenId, lp.cellX,
                            lp.cellY, item.spanX, item.spanY);
                } else {
                    // 如果没有位置则还原拖拽的View
                    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()) {
                // 绘制完成后的一些处理
                ...
            } else {
                d.deferDragViewCleanupPostAnimation = false;
                cell.setVisibility(VISIBLE);
            }
            parent.onDropChild(cell);
        }
    }

在这里面很多操作我们之前都看到了,所以不再详细讲解,不过还有三个函数需要看一下,一个是onDropExternal方法,这个方法是在开始的时候如果不是从Workspace拖拽来的时候调用,它和onDrop方法差不多,只是多一个判断拖入CellLayout的过程,自己看一下就可以了。还要就是createUserFolderIfNecessary方法和addToExistingFolderIfNecessar方法,这连个都是在if条件中调用的,所以不能忽略掉,虽然是判断,但是也做了相应的创建文件夹或者加入文件夹的操作。

第二个是UninstallDropTarget中的onDrop这个比较简单就是调用卸载功能,不在解释。
第三个是ButtonDropTarget中的,这个其实是删除和卸载按钮的的操作,也就是最后调用删除或者卸载。
第四个是Folder中的,

public void onDrop(DragObject d) {
        
        ...

        // If the icon was dropped while the page was being scrolled, we need to compute
        // the target location again such that the icon is placed of the final page.
        if (!mContent.rankOnCurrentPage(mEmptyCellRank)) {
            // 再次排序
            mTargetRank = getTargetRank(d, null);

            // Rearrange items immediately.
            mReorderAlarmListener.onAlarm(mReorderAlarm);

            ...
        }
        mContent.completePendingPageChanges();

        View currentDragView;
        ShortcutInfo si = mCurrentDragInfo;
        // 如果是外部拖入的
        if (mIsExternalDrag) {
            // 生成view并且加入
            currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank);
           
            // 调整数据库
            LauncherModel.addOrMoveItemInDatabase(
                    mLauncher, si, mInfo.id, 0, si.cellX, si.cellY);

            // We only need to update the locations if it doesn't get handled in #onDropCompleted.
            if (d.dragSource != this) {
                updateItemLocationsInDatabaseBatch();
            }
            mIsExternalDrag = false;
        } else {
            currentDragView = mCurrentDragView;
            // 如果来自文件夹则加入view并且排序
            mContent.addViewForRank(currentDragView, si, mEmptyCellRank);
        }

        ...
        
        // 重新排序
        rearrangeChildren();

        ...
        
    }

文件夹中的拖拽主要是从外部拖拽或者从文件夹到文件夹或者在文件夹内部拖拽,如果是外部要加入文件夹并且排序,如果是内部则直接排序。

从小部件或者所有应用图标界面开始拖拽


从小部件界面或者所有应用图标界面拖拽过程其实是一样的,我们就只介绍小部件的拖拽。

小部件的列表界面是WidgetsContainerView,因此要从这里的长按事件开始,

@Override
    public boolean onLongClick(View v) {
        
        ...
        
        boolean status = beginDragging(v);
        if (status && v.getTag() instanceof PendingAddWidgetInfo) {
            WidgetHostViewLoader hostLoader = new WidgetHostViewLoader(mLauncher, v);
            boolean preloadStatus = hostLoader.preloadWidget();
            if (DEBUG) {
                Log.d(TAG, String.format("preloading widget [status=%s]", preloadStatus));
            }
            mLauncher.getDragController().addDragListener(hostLoader);
        }
        return status;
    }

开始拖拽方法是beginDragging方法,代码:

    private boolean beginDragging(View v) {
        if (v instanceof WidgetCell) {
            if (!beginDraggingWidget((WidgetCell) v)) {
                return false;
            }
        } else {
            Log.e(TAG, "Unexpected dragging view: " + v);
        }

        // We don't enter spring-loaded mode if the drag has been cancelled
        if (mLauncher.getDragController().isDragging()) {
            // Go into spring loaded mode (must happen before we startDrag())
            mLauncher.enterSpringLoadedDragMode();
        }

        return true;
    }

在这里通过if语句中的beginDraggingWidget方法开始拖拽,代码如下:

    private boolean beginDraggingWidget(WidgetCell v) {
        // 获取Widget的预览图来作为拖拽对象
        WidgetImageView image = (WidgetImageView) v.findViewById(R.id.widget_preview);
        PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag();

        ...

        // Compose the drag image
        Bitmap preview;
        float scale = 1f;
        final Rect bounds = image.getBitmapBounds();

        if (createItemInfo instanceof PendingAddWidgetInfo) {
            
            ...
            
            // 生成预览图片
            preview = getWidgetPreviewLoader().generateWidgetPreview(mLauncher,
                    createWidgetInfo.info, maxWidth, null, previewSizeBeforeScale);

            ...
            
        } else {
            PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag();
            Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo);
            preview = Utilities.createIconBitmap(icon, mLauncher);
            createItemInfo.spanX = createItemInfo.spanY = 1;
            scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / preview.getWidth();
        }

        ...
        
        // 开始拖拽
        mDragController.startDrag(image, preview, this, createItemInfo,
                bounds, DragController.DRAG_ACTION_COPY, scale);

        preview.recycle();
        return true;
    }

DragController中的startDrag方法:

    public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo,
                          Rect viewImageBounds, int dragAction, float initialDragViewScale) {
        
        // 获取拖拽的位置
        ...

        // 开始拖拽
        startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null,
                null, initialDragViewScale, false);

        if (dragAction == DRAG_ACTION_MOVE) {
            v.setVisibility(View.GONE);
        }
    }

接着调用startDrag方法,代码:

    public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
                              DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
                              float initialDragViewScale, boolean accessible) {
        
        ...

        // 这个和workspace中的处理一样
        for (DragListener listener : mListeners) {
            listener.onDragStart(source, dragInfo, dragAction);
        }

        // 获取位置并且判断是否可以接受拖拽
        ...

        mDragObject = new DropTarget.DragObject();

        // 构造拖拽对象及其参数信息
        ...

        // 生成托追视图
        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
                registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);

        ...

        // 跳转到桌面,并且将小部件视图添加到桌面上
        dragView.show(mMotionDownX, mMotionDownY);
        // 处理拖拽事件
        handleMoveEvent(mMotionDownX, mMotionDownY);
        return dragView;
    }

看到handleMoveEvent整个函数我们就很熟悉,整个事件就和上面的workspace处理方式是一样的,这里就不再重复讲解,整个流程相对比较复杂,本来打算很详细的讲解,但是还是比价粗糙,但是整个流程是全面的,所以还希望自己多研究代码,里面涉及到一些位置的算法,我这里没有讲,自己看看也就会了,没有那么难。

最后


Github地址:https://github.com/yuchuangu85/Launcher3_mx

微信公众账号:Code-MX

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

推荐阅读更多精彩内容