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

上一章墨香帶你學Launcher之(五)-Workspace滑動介紹了workspace的造成以及滑動過程處理,本章咱們介紹桌面圖標的拖拽過程,這個拖拽過程設計很是巧妙,設計的東西不少,因此我儘可能詳細講解。java

因爲十一回來一直上火,到最近才漸好,工做相對也較忙,因此一直拖到如今纔開始寫這篇文章,在留言裏也看到不少人關注個人博客文章,很是感激,也有朋友一直期待,因此在此說聲抱歉,來的有點晚,因此趁着今天有空補上這篇文章。git

對於雙層桌面,拖拽主要有幾個事件,一個是從二級菜單的全部應用界面中的圖標(或者小插件)拖拽到桌面上,另一個是在桌面上或者文件夾中的圖標拖拽到別的桌面或者文件夾中,還有就是拖拽桌面上的CellLayout進行排序,這個內容在前一章已經講過了,想看的能夠看前一章,剩下的這兩種咱們分開來說。拖拽過程當中還有些名詞,好比DropTarget、DragObject、DragView、DragSource等,我會在講解過程當中在解釋。下面咱們開始看第一個過程:github

桌面上的圖標拖拽


咱們知道圖標拖拽的觸發條件是長按事件,所以咱們要找到長按事件的過程,長按事件的代碼在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)方法,下面咱們看這個方法的代碼:spring

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循環中的回調函數,實現回調函數的有下面幾個類,app

咱們主要是在桌面上拖拽,主要是桌面中的處理,咱們看下代碼:ide

@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分析一下,以下圖:

這個是我從桌面拖動一個圖標進入文件夾,直到文件夾打開,而後直接再拖拽到桌面的過程,首先起始位置是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,前兩個很熟,最後一個是什麼呢,這個是你在桌面長按圖標時候在桌面頂部出現的刪除卸載那個按鈕,那麼在這三個地方如何實現的,下面咱們分別看一下,

首先是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方法,這個方法有四個地方實現,以下圖:

首先是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地址:github.com/yuchuangu85…

微信公衆帳號:Code-MX

注:本文原創,轉載請註明出處,多謝。

相關文章
相關標籤/搜索