墨香帶你學(xué)Launcher之(六)--拖拽

上一章墨香帶你學(xué)Launcher之(五)-Workspace滑動介紹了workspace的形成以及滑動過程處理滨彻,本章我們介紹桌面圖標(biāo)的拖拽過程弥鹦,這個拖拽過程設(shè)計非常巧妙祖娘,設(shè)計的東西很多,所以我盡量詳細(xì)講解缚柏。

由于十一回來一直上火辩蛋,到最近才漸好呻畸,工作相對也較忙,所以一直拖到現(xiàn)在才開始寫這篇文章悼院,在留言里也看到很多人關(guān)注我的博客文章伤为,非常感激,也有朋友一直期待,所以在此說聲抱歉绞愚,來的有點(diǎn)晚叙甸,所以趁著今天有空補(bǔ)上這篇文章。

對于雙層桌面位衩,拖拽主要有幾個事件裆蒸,一個是從二級菜單的所有應(yīng)用界面中的圖標(biāo)(或者小插件)拖拽到桌面上,另外一個是在桌面上或者文件夾中的圖標(biāo)拖拽到別的桌面或者文件夾中糖驴,還有就是拖拽桌面上的CellLayout進(jìn)行排序僚祷,這個內(nèi)容在前一章已經(jīng)講過了,想看的可以看前一章贮缕,剩下的這兩種我們分開來講辙谜。拖拽過程中還有些名詞,比如DropTarget跷睦、DragObject筷弦、DragView、DragSource等抑诸,我會在講解過程中在解釋烂琴。下面我們開始看第一個過程:

桌面上的圖標(biāo)拖拽


我們知道圖標(biāo)拖拽的觸發(fā)條件是長按事件,因此我們要找到長按事件的過程蜕乡,長按事件的代碼在Launcher.java這個類中奸绷,代碼如下:

 public boolean onLongClick(View v) {
        // 如果不允許拖拽則返回
        if (!isDraggingEnabled()) return false;
        // 如果桌面鎖定返回
        if (isWorkspaceLocked()) return false;
        // 如果沒有在桌面顯示狀態(tài)返回
        if (mState != State.WORKSPACE) return false;
        
        // 顯示所有圖標(biāo)的按鈕,顯示所有圖標(biāo)界面
        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語句中层玲,如果長按的是桌面的空白區(qū)域号醉,則調(diào)用showOverviewMode(true)來顯示桌面預(yù)覽狀態(tài),也就是桌面縮小辛块,顯示多個CellLayout的狀態(tài)畔派,這時再長按單個CellLayout可以進(jìn)行拖拽排序。接著是if (v.getTag() instanceof ItemInfo) 這個判斷润绵,這個就是說你當(dāng)前長按的是app的圖標(biāo)或者文件夾线椰,這時會創(chuàng)建一個CellLayout.CellInfo對象,這個對象是對你要拖拽的View包含對象的信息存儲也就相當(dāng)于復(fù)制了一份尘盼,然后創(chuàng)建引用itemUnderLongClick憨愉,這個是你正在長按的圖標(biāo),再往下卿捎,判斷如果沒有拖拽事件執(zhí)行就開始判斷執(zhí)行拖拽事件配紫,下面的判斷也是如果CellLayout.CellInfo這個對象為空,則執(zhí)行桌面預(yù)覽或者排序事件午阵,如果不為空躺孝,則說明長按的是圖標(biāo),那么此時要判斷你拖動的不是文件夾或者不是顯示所有圖標(biāo)的那個按鈕,開始執(zhí)行
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方法中調(diào)用的是下面的那個startDrag方法伪很,在這個方法中調(diào)用了對于你長按的圖標(biāo)進(jìn)行了隱藏,那么隱藏怎么拖拽圖標(biāo)呢奋单,這里先不解釋,然后調(diào)用layout.prepareChildForDrag(child)方法猫十,這個方法其實(shí)就是對于你剛才長按的那個View的位置進(jìn)行儲存览濒,也就是他占用的位置,這個位置雖然圖標(biāo)隱藏了但是在它被放到其他地方前還是被它占用的拖云,然后調(diào)用
beginDragShared方法贷笛,這個方法中傳入了一個this,點(diǎn)擊進(jìn)入會看到是參數(shù)DragSource宙项,也就是workspace乏苦,接著看最終調(diào)用下面方法:

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的邊框尤筐,當(dāng)你拖動View時汇荐,這個邊框會在你拖動的附近允許你防止該View的位置顯示出來,以表示你可以將View放置的地方盆繁,創(chuàng)建代碼比較簡單掀淘,自己看一下就好了,我們往下看油昂,緊接著就是調(diào)用createDragBitmap這個方法創(chuàng)建你上面隱藏的那個View的Bitmap革娄,過程自己看一下似枕,很簡單涡上,然后開始計算拖拽的起始位置,然后調(diào)用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循環(huán)中的回調(diào)函數(shù)安寺,實(shí)現(xiàn)回調(diào)函數(shù)的有下面幾個類厕妖,

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()創(chuàng)建DragObject對象井仰,儲存相關(guān)的信息,然后生成DragView對象破加,通過dragView.show(mMotionDownX, mMotionDownY)方法將DragView對象添加到你手放置的位置俱恶,此時可以知道你拖拽的原來是DragView對象,最后調(diào)用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移動到相應(yīng)的位置合是,然后查找DropTarget了罪,這里不再貼代碼,我簡單說一下就可以了聪全,這里的DropTarget是你拖動你的View要放入的地方泊藕,比如folder,workspace等难礼,在DragController中存在一個DropTarget的列表娃圆,然后查找到對應(yīng)的DropTarget對象,接著調(diào)用checkTouchMove方法蛾茉,來處理相應(yīng)的結(jié)果讼呢,我們看一下代碼:

    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是將圖標(biāo)拖拽到的目的低對象,這個做了判斷谦炬,如果DropTarget不為空悦屏,也就是找到了目標(biāo),那么判斷是不是與上次的DropTarget相同键思,這個怎么解釋础爬,我們看最后一行代碼,mLastDropTarget在這里賦值稚机,也就是第一次為null幕帆,那么肯定不等,此時判斷mLastDropTarget為null赖条,直接走dropTarget.onDragEnter失乾,也就是進(jìn)入動作,當(dāng)?shù)诙螘r會先執(zhí)行mLastDropTarget.onDragExit動作然后執(zhí)行dropTarget.onDragOver動作纬乍,也就是先退出上一個目標(biāo)碱茁,然后進(jìn)入下一個目標(biāo),當(dāng)DropTarget為null時仿贬,會判斷mLastDropTarget是否為null纽竣,如果為null,那么就執(zhí)行mLastDropTarget.onDragExit動作茧泪,最后執(zhí)行賦值蜓氨,那么onDragEnter、onDragOver队伟、onDragExit都是一個怎么作用呢穴吹,我們通過log分析一下,如下圖:

launcher02.png
launcher03.png
launcher04.png

這個是我從桌面拖動一個圖標(biāo)進(jìn)入文件夾嗜侮,直到文件夾打開港令,然后直接再拖拽到桌面的過程啥容,首先起始位置是DrogController的startDrag方法,然后調(diào)用Workspace的onDragStart,然后是onDragEnter顷霹,也就是計入Workspace咪惠,進(jìn)入CellLayout,然后調(diào)用Workspace的onDragOver淋淀,也就是在Workspace中拖拽移動的過程遥昧,中間過程比較多,我省略了一部分朵纷,然后看第二張圖渠鸽,當(dāng)我進(jìn)入Folder的時候,先退出Workspace柴罐,然后退出CellLayout,然后進(jìn)入Folder憨奸,然后執(zhí)行onDragOver,在Folder中拖動革屠,接著看第三張圖,當(dāng)我離開Folder的時候排宰,先執(zhí)行Folder的onDragExit似芝,然后進(jìn)入Workspace,然后進(jìn)入CellLayout板甘,然后在Workspace中拖拽党瓮,最后放置到Workspace中,先執(zhí)行DrogController的drop函數(shù)盐类,然后退出Workspace寞奸,退出CellLayout,然后Workspace接受拖拽的View在跳,然后釋放枪萄,然后調(diào)用Workspace的onDragEnd然后執(zhí)行Folder的onDragEnd函數(shù),最后結(jié)束拖拽過程猫妙。

從上面的整個流程可以看到上面三個函數(shù)的具體過程瓷翻,這樣就很好理解了。我們接著分析三個函數(shù)分別作了什么割坠,首先是onDragEnter:

由下圖可以onDragEnter有三個地方實(shí)現(xiàn)齐帚,Workspace、Folder和ButtonDropTarget彼哼,前兩個很熟对妄,最后一個是什么呢,這個是你在桌面長按圖標(biāo)時候在桌面頂部出現(xiàn)的刪除卸載那個按鈕沪羔,那么在這三個地方如何實(shí)現(xiàn)的饥伊,下面我們分別看一下象浑,

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

首先獲取當(dāng)前圖標(biāo)所在的CellLayout,然后調(diào)用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,只在此方法中賦值茫因,因此第一次進(jìn)入為null蚪拦,所以會執(zhí)行CellLayout的onDragEnter方法,這是就我們上面看到的那個順序冻押,在workspace的onDragEnter方法后執(zhí)行CellLayout的相應(yīng)方法驰贷,然后是一些清理工作,這里不再詳細(xì)講解洛巢。

接著是Workspace的onDragOver括袒,代碼如下:

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

        // 獲取拖拽View的中心點(diǎn)坐標(biāo)
        mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);

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

            //獲取當(dāng)前的CellLayout并處理
            
        } else {
            //獲取當(dāng)前的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];
                
                // 如果沒有位置稿茉,并且是排序狀態(tài)锹锰,則進(jìn)行排序處理
                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);
            }

            // 如果是創(chuàng)建文件夾或者放置到文件夾狀態(tài)
            if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER ||
                    !nearestDropOccupied) {
                if (mDragTargetLayout != null) {
                    mDragTargetLayout.revertTempState();
                }
            }
        }
    }

上面代碼比較多,不在詳細(xì)解釋漓库,里面添加了相關(guān)注釋恃慧,主要是說一下拖拽過程,在拖拽的時候渺蒿,要時時計算拖拽圖標(biāo)和最近位置的距離痢士,并且判斷最近位置是否被占用,如果沒有被占用茂装,則顯示圖標(biāo)的輪廓框怠蹂,如果被占用了,就要判斷狀體训唱,如果是排序狀態(tài)褥蚯,也就是會把當(dāng)前被占用位置的圖標(biāo)擠跑,如果是創(chuàng)建文件夾狀態(tài)况增,則會創(chuàng)建文件夾赞庶,不會擠跑圖標(biāo),如果是文件夾并且是添加文件夾狀態(tài)澳骤,則顯示添加文件夾的效果歧强。

最后是Workspace的onDragExit方法,這個方法內(nèi)容不多为肮,其實(shí)沒太多操作摊册,主要是清楚一些狀態(tài),代碼就不貼了颊艳,自己看看就知道了茅特。

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

在這里面主要是一個滑動過程肯骇,文件夾中的應(yīng)用超過一定數(shù)量也是分頁的,因此你在拖拽過程中需要判斷是否滑動翻頁祖很。代碼量比較少笛丙,可以自己看看。
在onDragExit的時候要關(guān)閉文件夾假颇,清除相應(yīng)的拖拽監(jiān)聽胚鸯。

最后是ButtonDropTarget,onDragEnter方法主要是改變顏色和效果笨鸡,onDragOver沒有做任何處理蠢琳,onDragExit方法是清除效果,onDragEnd也沒有效果只是重置了一個標(biāo)簽镜豹。

整個Workspace中的拖拽代碼量很大,但是只要抓住邏輯相對也是比較簡單蓝牲,我們在上面分析了所有狀態(tài)操作趟脂,但是還存在一個問題沒有講,就是如果觸發(fā)的拖拽過程例衍,其實(shí)整個是在DragController中的onTouch方法中觸發(fā)的昔期,也是就在這個方法中調(diào)用的handleMoveEvent方法,我們看一下佛玄,而DragController中的onTouch方法是在DragLayer中的onTouch方法中調(diào)用的硼一,因?yàn)檎麄€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;
    }

里面好幾處調(diào)用了handleMoveEvent方法梦抢,就是不斷滑動過程中不斷的處理拖拽事件般贼,所以看到是連續(xù)的,這里面有兩個方法我們看一下奥吩,首先是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);
    }

首先是調(diào)用findDropTarget方法來查找放置拖拽View的目標(biāo)對象,然后判斷目標(biāo)對象是否可以接受該View端衰,如果可以接受調(diào)用ondrop方法叠洗,這個方法有四個地方實(shí)現(xiàn)甘改,如下圖:

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) {
                    
                        ...
                    
                        //添加到相應(yīng)的CellLayout中
                        addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1],
                                info.spanX, info.spanY);
                    }

                    // 放置完成后更新位置
                    
                    ...
                    
                    // 修改數(shù)據(jù)庫
                    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);
        }
    }

在這里面很多操作我們之前都看到了十艾,所以不再詳細(xì)講解,不過還有三個函數(shù)需要看一下名挥,一個是onDropExternal方法疟羹,這個方法是在開始的時候如果不是從Workspace拖拽來的時候調(diào)用,它和onDrop方法差不多禀倔,只是多一個判斷拖入CellLayout的過程榄融,自己看一下就可以了。還要就是createUserFolderIfNecessary方法和addToExistingFolderIfNecessar方法救湖,這連個都是在if條件中調(diào)用的愧杯,所以不能忽略掉,雖然是判斷鞋既,但是也做了相應(yīng)的創(chuàng)建文件夾或者加入文件夾的操作力九。

第二個是UninstallDropTarget中的onDrop這個比較簡單就是調(diào)用卸載功能,不在解釋邑闺。
第三個是ButtonDropTarget中的跌前,這個其實(shí)是刪除和卸載按鈕的的操作,也就是最后調(diào)用刪除或者卸載陡舅。
第四個是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);
           
            // 調(diào)整數(shù)據(jù)庫
            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();

        ...
        
    }

文件夾中的拖拽主要是從外部拖拽或者從文件夾到文件夾或者在文件夾內(nèi)部拖拽,如果是外部要加入文件夾并且排序靶衍,如果是內(nèi)部則直接排序灾炭。

從小部件或者所有應(yīng)用圖標(biāo)界面開始拖拽


從小部件界面或者所有應(yīng)用圖標(biāo)界面拖拽過程其實(shí)是一樣的,我們就只介紹小部件的拖拽颅眶。

小部件的列表界面是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的預(yù)覽圖來作為拖拽對象
        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) {
            
            ...
            
            // 生成預(yù)覽圖片
            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);
        }
    }

接著調(diào)用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();

        // 構(gòu)造拖拽對象及其參數(shù)信息
        ...

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

        ...

        // 跳轉(zhuǎn)到桌面商叹,并且將小部件視圖添加到桌面上
        dragView.show(mMotionDownX, mMotionDownY);
        // 處理拖拽事件
        handleMoveEvent(mMotionDownX, mMotionDownY);
        return dragView;
    }

看到handleMoveEvent整個函數(shù)我們就很熟悉眷蜈,整個事件就和上面的workspace處理方式是一樣的,這里就不再重復(fù)講解沈自,整個流程相對比較復(fù)雜酌儒,本來打算很詳細(xì)的講解,但是還是比價粗糙枯途,但是整個流程是全面的忌怎,所以還希望自己多研究代碼籍滴,里面涉及到一些位置的算法,我這里沒有講榴啸,自己看看也就會了孽惰,沒有那么難。

最后


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

微信公眾賬號:Code-MX

注:本文原創(chuàng)鸥印,轉(zhuǎn)載請注明出處勋功,多謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末库说,一起剝皮案震驚了整個濱河市狂鞋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌潜的,老刑警劉巖骚揍,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異啰挪,居然都是意外死亡信不,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門亡呵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抽活,“玉大人,你說我怎么就攤上這事锰什∽煤荆” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵歇由,是天一觀的道長。 經(jīng)常有香客問我果港,道長沦泌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任辛掠,我火速辦了婚禮谢谦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘萝衩。我一直安慰自己回挽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布猩谊。 她就那樣靜靜地躺著千劈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪牌捷。 梳的紋絲不亂的頭發(fā)上墙牌,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天涡驮,我揣著相機(jī)與錄音,去河邊找鬼喜滨。 笑死捉捅,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的虽风。 我是一名探鬼主播棒口,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辜膝!你這毒婦竟也來了无牵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤内舟,失蹤者是張志新(化名)和其女友劉穎合敦,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體验游,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡充岛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了耕蝉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崔梗。...
    茶點(diǎn)故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖垒在,靈堂內(nèi)的尸體忽然破棺而出蒜魄,到底是詐尸還是另有隱情,我是刑警寧澤场躯,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布谈为,位于F島的核電站,受9級特大地震影響踢关,放射性物質(zhì)發(fā)生泄漏伞鲫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一签舞、第九天 我趴在偏房一處隱蔽的房頂上張望秕脓。 院中可真熱鬧,春花似錦儒搭、人聲如沸吠架。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽傍药。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怔檩,已是汗流浹背褪秀。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留薛训,地道東北人媒吗。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像乙埃,于是被迫代替她去往敵國和親闸英。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容