Android O Launcher3--拖拽

 

前言     

  在Android手機桌面,我們經常會把一個應用的圖標從菜單裏面,拖拽到桌面。或者把一個應用的圖標移到自己更加喜歡的位置。拖拽能夠讓用戶方便的把應用放到用戶可記得易操作的位置,從而能夠讓用戶快捷的打開高頻使用的應用。同時,拖拽也可以讓用戶能夠佈置自己的桌面,能夠把應用進行分類的存放。因此,Launcher拖拽讓用戶可自定義桌面。

拖拽的內容:

  1. 主屏幕(Workspace)上的圖標和小部件
  2. 文件夾中的圖標
  3. 抽屜中的圖標和小部件

拖拽過程是怎樣的,就像大象裝冰箱是一樣的,分三步長按(把冰箱門打開)拖拽(把大象放進冰箱)放開(帶上冰箱門)。

第一步:長按處理及準備工作

 長按就要有監聽器,首先看監聽器是如何設置的。

在Workspace中addInScreen方法的最後,view類設置workspace長按監聽器

    
        child.setHapticFeedbackEnabled(false);
        child.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
        if (child instanceof DropTarget) {
            mDragController.addDropTarget((DropTarget) child);
        }

 ItemLongClickListener 分爲INSTANCE_WORKSPACE和INSTANCE_ALL_APPS兩種;本次只分析workspace長按監聽器,

長按事件傳遞過來的 View 便是我們長按的應用圖標,v的實質便是 BubbleTextView。

Workspace.startDrag

判斷各種狀態後,又回到workspace的startDrag.  其中child.setVisibility(INVISIBLE);  設置view即icon不可見;接着beginDragShared(child, this, options); 

beginDragShared的參數(View child, DragSource source, DragOptions options);

-->beginDragShared(child, source, (ItemInfo) dragObject, new DragPreviewProvider(child), options);

DragSource 表示本次拖拽的應用圖標來自哪裏,在 beginDragShared() 中傳入的參數是 this,也即本次拖拽來自 Workspace;

DragPreviewProvider 預覽位圖和輪廓線相關的類。輪廓線就是用來指示你圖標會被放在哪個位置的,你不斷拖動,這個輪廓線的位置也在不斷變化。
 

    public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
            DragPreviewProvider previewProvider, DragOptions dragOptions) {
         ............
        //放棄焦點,將按下的狀態設置成false,輪廓線等
        child.clearFocus();
        child.setPressed(false);
        mOutlineProvider = previewProvider;

        // The drag bitmap follows the touch point around on the screen
        final Bitmap b = previewProvider.createDragBitmap();
        int halfPadding = previewProvider.previewPadding / 2;
        
        float scale = previewProvider.getScaleAndPosition(b, mTempXY);
        int dragLayerX = mTempXY[0];
        int dragLayerY = mTempXY[1];

        DeviceProfile grid = mLauncher.getDeviceProfile();
        Point dragVisualizeOffset = null;
        Rect dragRect = null;
        if (child instanceof BubbleTextView) {
            dragRect = new Rect();
            ((BubbleTextView) child).getIconBounds(dragRect);
            dragLayerY += dragRect.top;
            // Note: The dragRect is used to calculate drag layer offsets, but the
            // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
        } else if (child instanceof FolderIcon) {
            int previewSize = grid.folderIconSizePx;
            dragVisualizeOffset = new Point(- halfPadding, halfPadding - child.getPaddingTop());
            dragRect = new Rect(0, child.getPaddingTop(), child.getWidth(), previewSize);
        } else if (previewProvider instanceof ShortcutDragPreviewProvider) {
            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
        }

        // Clear the pressed state if necessary
        if (child instanceof BubbleTextView) {
            BubbleTextView icon = (BubbleTextView) child;
            icon.clearPressedBackground();
        }

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

        if (child instanceof BubbleTextView && !dragOptions.isAccessibleDrag) {
            PopupContainerWithArrow popupContainer = PopupContainerWithArrow
                    .showForIcon((BubbleTextView) child);
            if (popupContainer != null) {
                dragOptions.preDragCondition = popupContainer.createPreDragCondition();

                mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("dragging started");
            }
        }

        DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source,
                dragObject, dragVisualizeOffset, dragRect, scale * iconScale, scale, dragOptions);
        dv.setIntrinsicIconScaleFactor(dragOptions.intrinsicIconScaleFactor);
        return dv;
    }
DragController是桌面裏面的拖拽控制器,一旦他的startDrag執行完了,就可以接收滑動的觸摸消息了。

DragController.startDrag

public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
            DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
            float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options) {
        if (PROFILE_DRAWING_DURING_DRAG) {
            android.os.Debug.startMethodTracing("Launcher");
        }

        // Hide soft keyboard, if visible
     // 隱藏軟件盤
        UiThreadHelper.hideKeyboardAsync(mLauncher, mWindowToken);

        mOptions = options;
        if (mOptions.systemDndStartPoint != null) {
            mMotionDownX = mOptions.systemDndStartPoint.x;
            mMotionDownY = mOptions.systemDndStartPoint.y;
        }

        final int registrationX = mMotionDownX - dragLayerX;
        final int registrationY = mMotionDownY - dragLayerY;

        final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
        final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;

        mLastDropTarget = null;

        //非常重要的對象
        mDragObject = new DropTarget.DragObject();

        mIsInPreDrag = mOptions.preDragCondition != null
                && !mOptions.preDragCondition.shouldStartDrag(0);

        final Resources res = mLauncher.getResources();
        final float scaleDps = mIsInPreDrag
                ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;

      // 創建DragView對象,bitmap化身view
        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
                registrationY, initialDragViewScale, dragViewScaleOnDrop, scaleDps);
        dragView.setItemInfo(dragInfo);
        mDragObject.dragComplete = false;
        if (mOptions.isAccessibleDrag) {
            // 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.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);

            //創建DragDriver,後面會用到
            mDragDriver = DragDriver.create(mLauncher, this, mDragObject, mOptions);
        }

        mDragObject.dragSource = source;
        mDragObject.dragInfo = dragInfo;
        mDragObject.originalDragInfo = new ItemInfo();
        mDragObject.originalDragInfo.copyFrom(dragInfo);

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

        // 觸摸反饋
        mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
 // 顯示DragView對象(將該DragView添加到DragLayer上)
        dragView.show(mMotionDownX, mMotionDownY);
        mDistanceSinceScroll = 0;

        //監聽事件
        if (!mIsInPreDrag) {
            callOnDragStart();
        } else if (mOptions.preDragCondition != null) {
            mOptions.preDragCondition.onPreDragStart(mDragObject);
        }

        //處理當前移動事件,是整個過程的一個很重要的方法
        mLastTouch[0] = mMotionDownX;
        mLastTouch[1] = mMotionDownY;
        handleMoveEvent(mMotionDownX, mMotionDownY);
        mLauncher.getUserEventDispatcher().resetActionDurationMillis();
        return dragView;
    }

分析以上代碼內容

DragObject:

 new DropTarget.DragObject() 是整個拖拽框架中最有“實權”的實例對象;

它包含了拖拽的視圖代表 DragView;包含被拖拽的應用 icon(BubbleTextView) 數據 ItemInfo;包含拖拽的源頭 DragSource;在整個拖拽過程中,接受控制中心 DragController 的執行指令,是指令的首要執行者,DragObject 會伴隨整個拖拽過程,對 DragView、icon(BubbleTextView)、DragSource 有絕對“控制權”,如控制 DragView 的顯示、消失和位置等。

DragDriver:

是驅動管理 Drag/Drop操作的基本類;還包含Driver拖拽事件接口;而DragController實現了事件接口,後面會用到  

public interface EventListener {
        void onDriverDragMove(float x, float y);
        void onDriverDragExitWindow();
        void onDriverDragEnd(float x, float y);
        void onDriverDragCancel();
    }


監聽事件:

拖拽開始時,通過回調 onDragStart() 通知監聽者拖拽已經開始,在 Launcher3 中,註冊拖拽監聽者的有 Folder 、Workspace 、ButtonDropTarget.java、 SecondDropTarget.java、 DeleteDropTarget.java、WidgetHostViewLoader??等.

DropTarget接口

  • onDragEnter是當拖動圖標到某一DropTarget(藍色),邊緣,剛剛進入DropTarget範圍內的時候所調用的內容。比如說我們拖動桌面的一個快捷方式,到桌面頂端的刪除區域,“刪除”兩字和手中的圖標會變紅,這些動作都是在onDragEnter回調中完成的
  • onDragOver是在某一DropTarget內部移動的時候會調用的回調,比如我們把手上的圖標移動到兩個圖標中間的時候,會發生擠位的情況(就是桌面已有圖標讓出空位),基本上每個ACTION_MOVE操作都會調用他。
  • onDragExit是從某一DropTarget拖出時候會進行的回調,比如onDragEnter時變紅的“刪除”和圖標會在這個調用中恢復正常。
  • onDrop是鬆手時候發生的調用,做一些放下時候的操作,比如刪除快捷方式的時候會在onDrop裏面開始刪除的操作。

handleMoveEvent:

直接調用 handleMoveEvent() 方法,這個方法很重要,它是整個拖拽過程實現的基本條件,因此,這個方法在拖拽過程中會被調用非常多次數,也就是處理我們拖拽的時候的手指移動事件;後面詳細介紹。

到此,拖拽的開始和準別工作到這裏已經徹底完成,下面將進入真正的拖拽過程。  

第二步:拖拽(觸摸事件的攔截和分發

在上一章節中,bitmap 爲何一個生成一個DragView ?我們知道,一個子 View 是不能越界到它的父控件的外面,而我們的 icon(BubbleTextView) 是要能拖到界面上得任何一個位置的。所以,Launcher 有一個覆蓋整個 Laucher 界面的 DragLayer,而 DragView 便是依附在 DragLayer 作爲它的直接子 View,便能在整個 Launcher 界面上移動。 

onInterceptTouchEvent

    DragLayer繼承自ViewGroup,其onInterceptTouchEvent方法若返回true,說明需要攔截觸屏事件,則後續的一系列事件將傳遞給自身的onTouchEvent方法,而不再向其子控件傳遞。DragController的onInterceptTouchEvent由DragLayer的onInterceptTouchEvent調用,用於攔截觸屏事件的處理。當用戶點擊屏幕時,觸發ACTION_DOWN事件,記錄當前觸摸位置。當擡起時,觸發ACTION_UP事件,結束拖拽。若擡起時處於拖拽中,在當前位置釋放被拖拽物。因此,若此時處於拖拽中,後續的觸屏事件將只傳遞到DragLayer的onTouchEvent。


onTouchEvent

    onTouchEvent處理觸屏事件,若返回true,則表示消費掉該事件,事件不再向父控件的onTouchEvent傳遞。DragController的onTouchEvent由DragLayer的onTouchEvent調用,用於處理被拖拽物的移動。當startDrag執行完畢,DragController設置拖拽狀態爲true,這樣,觸屏事件將最終轉到onTouchEvent中,在此處調用handleMoveEvent進行物體的移動。

      DragLayer是Launcher上所有佈局的父容器,主要進行事件的攔截和分發,但是具體工作都是交由DragController來處理的,我們直接看DragController的onInterceptTouchEvent的攔截處理。

   首先看DragLayer本身沒有onInterceptTouchEvent,查看父類BaseDragLayer的;


public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (mTouchCompleteListener != null) {
                mTouchCompleteListener.onTouchComplete();
            }
            mTouchCompleteListener = null;
        } else if (action == MotionEvent.ACTION_DOWN) {
            mActivity.finishAutoCancelActionMode();
        }
        return findActiveController(ev);
    }

    protected boolean findActiveController(MotionEvent ev) {
        mActiveController = null;

        AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
        if (topView != null && topView.onControllerInterceptTouchEvent(ev)) {
            mActiveController = topView;
            return true;
        }

        for (TouchController controller : mControllers) {
            if (controller.onControllerInterceptTouchEvent(ev)) {
                mActiveController = controller;
                return true;
            }
        }
        return false;
    }

 onControllerInterceptTouchEvent 是TouchController的一個接口;   

 從代碼可以看出首先判斷是否浮動View,然後根據DragController的onControllerInterceptTouchEvent處理結果確定返回;

是否攔截看返回值。


    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
        if (mOptions != null && mOptions.isAccessibleDrag) {
            return false;
        }

        // Update the velocity tracker
        mFlingToDeleteHelper.recordMotionEvent(ev);

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

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // Remember location of down touch
                mMotionDownX = dragLayerX;
                mMotionDownY = dragLayerY;
                break;
            case MotionEvent.ACTION_UP:
                mLastTouchUpTime = System.currentTimeMillis();
                break;
        }

        return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
    } 

DragDriver 

   public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();

        switch (action) {
            case MotionEvent.ACTION_UP:
                mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
                break;
            case MotionEvent.ACTION_CANCEL:
                mEventListener.onDriverDragCancel();
                break;
        }

        return true;
    }

MotionEvent.ACTION_DOWN/MOVE/ATCION_UP/CANCEL 返回true;所以DragLayer對Touch事件進行了攔截在BaseDragController的onTouchEvent中進行處理。

public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (mTouchCompleteListener != null) {
                mTouchCompleteListener.onTouchComplete();
            }
            mTouchCompleteListener = null;
        }

        if (mActiveController != null) {
            return mActiveController.onControllerTouchEvent(ev);
        } else {
            // In case no child view handled the touch event, we may not get onIntercept anymore
            return findActiveController(ev);
        }
    }

DragController

public boolean onControllerTouchEvent(MotionEvent ev) {
        if (mDragDriver == null || mOptions == null || mOptions.isAccessibleDrag) {
            return false;
        }

        // Update the velocity tracker
        mFlingToDeleteHelper.recordMotionEvent(ev);

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

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

        return mDragDriver.onTouchEvent(ev);
    }

 DragDriver 定義了事件接口,DragController實現了事件接口; 前面DragController.startDrag創建DragDriver時傳遞this;所以所有的事件處理又回到DragController實現.

public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();

        switch (action) {
            case MotionEvent.ACTION_MOVE:
                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
                break;
            case MotionEvent.ACTION_UP:
                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
                mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
                break;
            case MotionEvent.ACTION_CANCEL:
                mEventListener.onDriverDragCancel();
                break;
        }

        return true;
    }

  DragController 又回到事件處理

 @Override
    public void onDriverDragMove(float x, float y) {
        final int[] dragLayerPos = getClampedDragLayerPos(x, y);

        handleMoveEvent(dragLayerPos[0], dragLayerPos[1]);
    }

handleMoveEvent:

直接調用 handleMoveEvent() 方法,這個方法很重要,它是整個拖拽過程實現的基本條件,因此,這個方法在拖拽過程中會被調用非常多次數,也就是處理我們拖拽的時候的手指移動事件;

1)移動DragView到手指的位置   

2) 查找拖拽目標DropTarget;DragView 放下的目標,即 icon(BubbleTextView) 移到的新的位置  

      DropTarget是一個可放置(drop)區域的抽象,也就是我們鬆開手的時候想要把圖標放到某個東西上,這個東西就是DropTarget,實現他的都是View,比如說文件夾,Workspace,刪除區等等,你可以通過“ Open Type Hierarchy”來查看哪些類繼承了DropTarget接口。包含比較重要的幾個接口

  • onDragEnter是當拖動圖標到某一DropTarget(藍色),邊緣,剛剛進入DropTarget範圍內的時候所調用的內容。比如說我們拖動桌面的一個快捷方式,到桌面頂端的刪除區域,“刪除”兩字和手中的圖標會變紅,這些動作都是在onDragEnter回調中完成的
  • onDragOver是在某一DropTarget內部移動的時候會調用的回調,比如我們把手上的圖標移動到兩個圖標中間的時候,會發生擠位的情況(就是桌面已有圖標讓出空位),基本上每個ACTION_MOVE操作都會調用他。
  • onDragExit是從某一DropTarget拖出時候會進行的回調,比如onDragEnter時變紅的“刪除”和圖標會在這個調用中恢復正常。
  • onDrop是鬆手時候發生的調用,做一些放下時候的操作,比如刪除快捷方式的時候會在onDrop裏面開始刪除的操作

3)  檢查拖動時Target的狀態- 4、翻頁距離計算  5 滿足條件回調

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

        // Drop non someoe?
        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;

        if (mIsInPreDrag && mOptions.preDragCondition != null
                && mOptions.preDragCondition.shouldStartDrag(mDistanceSinceScroll)) {
            callOnDragStart();
        }
    }

進入findDropTarget,有一個 mDropTargets 的變量列表,保存了所有的 Target,這些 Target 在 Launcher 啓動的時候就通過方法 addDropTarget(DropTarget target) 添加到 DrageController 中來。包括 Workspace、Folder、ButtonDropTarget 等。在這麼多 Target 中,如何判斷當前是移到哪個 Target 呢?通過 target 的實例調用 getHitRectRelativeToDragLayer(r) 方法,獲取到 target 所處的位置 r,通過 r.contains(x, y) 判斷, x, y 是否包含在 target 內,如果是,則手指移到了該 target,同時把 x,y 保存到 target 實例中,返回該 target 對象。未找到DropTarget則交給Workspace 處理,找到合適celllayout 處理邏輯。
 

    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
        mDragObject.x = x;
        mDragObject.y = y;

        final Rect r = mRectTemp;
        final ArrayList<DropTarget> dropTargets = mDropTargets;
        final int count = dropTargets.size();
        for (int i = count - 1; i >= 0; i--) {
            DropTarget target = dropTargets.get(i);
            if (!target.isDropEnabled())
                continue;

            target.getHitRectRelativeToDragLayer(r);
            if (r.contains(x, y)) {
                dropCoordinates[0] = x;
                dropCoordinates[1] = y;
                mLauncher.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates);
                return target;
            }
        }
        // Pass all unhandled drag to workspace. Workspace finds the correct
        // cell layout to drop to in the existing drag/drop logic.
        dropCoordinates[0] = x;
        dropCoordinates[1] = y;
        mLauncher.getDragLayer().mapCoordInSelfToDescendant(mLauncher.getWorkspace(),
                dropCoordinates);
        return mLauncher.getWorkspace();
    }

回到handleMoveEvent繼續,

如果前後兩次的 target 實例不一致,說明 target 發生變化,通過調用 onDragExit() 方法通知上一次的 target 拖拽已經移走;然後通過 onDragEnter() 方法通知當前 target,拖拽已經移動進來。同時,通過 onDragOver() 通知 target 拖拽已經移到你的上面,準確的說,是 DragView 移到了 target 的上面。

在這裏,每個方法的參數都是 mDragObject 實例,在上文中我們知道,mDragObject 在拖拽中是最有“實權”的“人物”,擁有視圖的化身 DragView,保存有 icon(BubbleTextView)的數據,持有拖拽的來源 DragSource,同時,mDragObject 到達了 target,也可以操作 target 對象實例本身,因此,在 target 可以發生一切事情。
 

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

 

拖拽結束

         當用戶將被拖拽物移動到相應位置後,可以將手指從屏幕上移開。此時,將在onInterceptTouchEvent與onTouchEvent中調用drop方法釋放被拖拽物。其主要功能,就是查找拖拽目的對象(DropTarget),若找到且接受釋放,通知該對象被拖拽物的放入。最後,通知拖拽源(被拖拽物最初所在的容器)拖拽結果。

DragDrvier.java
 
  case MotionEvent.ACTION_UP:
                mEventListener.onDriverDragMove(ev.getX(), ev.getY());
                mEventListener.onDriverDragEnd(ev.getX(), ev.getY());

DragController.java

   public void onDriverDragEnd(float x, float y) {
        DropTarget dropTarget;
        
        Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject);
        if (flingAnimation != null) {
            dropTarget = mFlingToDeleteHelper.getDropTarget();
        } else {
            dropTarget = findDropTarget((int) x, (int) y, mCoordinatesTemp);
        }

        drop(dropTarget, flingAnimation);

        endDrag();
    }

     事件處理進入onDriverDrayEnd後 判斷並生成fling runnable ,後面OnDrop根據runnable 是否爲空獲取不同dropTarget;

    private void drop(DropTarget dropTarget, Runnable flingAnimation) {
        final int[] coordinates = mCoordinatesTemp;
        mDragObject.x = coordinates[0];
        mDragObject.y = coordinates[1];

        // Move dragging to the final target.
        if (dropTarget != mLastDropTarget) {
            if (mLastDropTarget != null) {
                mLastDropTarget.onDragExit(mDragObject);
            }
            mLastDropTarget = dropTarget;
            if (dropTarget != null) {
                dropTarget.onDragEnter(mDragObject);
            }
        }

        mDragObject.dragComplete = true;    //設置標誌
        if (mIsInPreDrag) {
            if (dropTarget != null) {
                dropTarget.onDragExit(mDragObject);
            }
            return;
        }

        // Drop onto the target.
        boolean accepted = false;
        if (dropTarget != null) {
            dropTarget.onDragExit(mDragObject);  //通知拖拽目的對象已離開
            if (dropTarget.acceptDrop(mDragObject)) {  //支持放入
                if (flingAnimation != null) {
                    flingAnimation.run();
                } else {
                    dropTarget.onDrop(mDragObject, mOptions);
                }
                accepted = true;
            }
        }
        final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
        mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView);
        dispatchDropComplete(dropTargetAsView, accepted);
    }

   drop 會通過當前的座標找到需要放到的DropTarget,放下的操作首先得保證有DropTarget,然後DropTarget還得需要接受你手上的圖標,比如你把圖標拖到已經放滿的Hotseat上去肯定不能被接受(DropTarget.acceptDrop返回false)。被接受了就可以調用當前DropTarget的onDrop方法了,以Workspace爲例,它做了一下幾件事:

public void onDrop(final DragObject d, DragOptions options) {
        mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);// 計算拖動View的視覺中心
        CellLayout dropTargetLayout = mDropToLayout;// Drop的Celllayout對象;DragOver時設置

        // 判斷當前是否在Hotseat上,求出相對於dropTargetLayout的視覺中心座標
        // 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);
            }
        }

        boolean droppedOnOriginalCell = false;
     
        // 如果DragObject.dragSource!= Worspace,轉而調用onDropExternal(),否則繼續處理onDrop()的內容
        int snapScreen = -1;
        boolean resizeOnDrop = false;
        if (d.dragSource != this || mDragInfo == null) {
            final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
                    (int) mDragViewVisualCenter[1] };
            onDropExternal(touchXY, dropTargetLayout, d); 
        } else {
            final View cell = mDragInfo.cell;
            boolean droppedOnOriginalCellDuringTransition = false;
            Runnable onCompleteRunnable = null;

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

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

                // 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 (createUserFolderIfNecessary(cell, container,
                        dropTargetLayout, mTargetCell, distance, false, d.dragView) ||
                        addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
                                distance, d, false)) {
                    mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
                    return;
                }

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

                droppedOnOriginalCell = item.screenId == screenId && item.container == container
                        && item.cellX == mTargetCell[0] && item.cellY == mTargetCell[1];
                droppedOnOriginalCellDuringTransition = droppedOnOriginalCell && mIsSwitchingState;

                // When quickly moving an item, a user may accidentally rearrange their
                // workspace. So instead we move the icon back safely to its original position.
                boolean returnToOriginalCellToPreventShuffling = !isFinishedSwitchingState()
                        && !droppedOnOriginalCellDuringTransition && !dropTargetLayout
                        .isRegionVacant(mTargetCell[0], mTargetCell[1], spanX, spanY);
                int[] resultSpan = new int[2];
                if (returnToOriginalCellToPreventShuffling) {
                    mTargetCell[0] = mTargetCell[1] = -1;
                } else {
                    //處理拖動圖標時,如果當前落點被佔據時,擠開當前圖標的效果
                    mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
                            (int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell,
                            mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);
                }

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

                // if the widget resizes on drop //AppWidget可能在拖動時發生縮小
                if (foundCell && (cell instanceof AppWidgetHostView) &&
                        (resultSpan[0] != item.spanX || resultSpan[1] != item.spanY)) {
                    resizeOnDrop = true;
                    item.spanX = resultSpan[0];
                    item.spanY = resultSpan[1];
                    AppWidgetHostView awhv = (AppWidgetHostView) cell;
                    AppWidgetResizeFrame.updateWidgetSizeRanges(awhv, mLauncher, resultSpan[0],
                            resultSpan[1]);
                }

                // 如果滿足則更新位置,保存新的位置信息到數據庫中,播放動畫效果,否則彈回原來位置
                if (foundCell) {
                     // 拖動時可能落點在別的頁面,所以還會有頁面滑動的效果
                    if (getScreenIdForPageIndex(mCurrentPage) != screenId && !hasMovedIntoHotseat) {
                        snapScreen = getPageIndexForScreenId(screenId);
                        snapToPage(snapScreen);
                    }

 
                    final ItemInfo info = (ItemInfo) cell.getTag();
                    if (hasMovedLayouts) {
                        // Reparent the view
                        CellLayout parentCell = getParentCellLayoutForView(cell);
                        if (parentCell != null) {
                            parentCell.removeView(cell);
                        } else if (FeatureFlags.IS_DOGFOOD_BUILD) {
                            throw new NullPointerException("mDragInfo.cell has null parent");
                        }
                        addInScreen(cell, container, screenId, mTargetCell[0], mTargetCell[1],
                                info.spanX, info.spanY);
                    }

                    // update the item's position after drop  更新位置
                    CellLayout.LayoutParams lp = (CellLayout.LayoutParams) cell.getLayoutParams();
                    lp.cellX = lp.tmpCellX = mTargetCell[0];
                    lp.cellY = lp.tmpCellY = mTargetCell[1];
                    lp.cellHSpan = item.spanX;
                    lp.cellVSpan = item.spanY;
                    lp.isLockedToGrid = true;

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

                        final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
                        AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
                        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
                                && !d.accessibleDrag) {
                            onCompleteRunnable = new Runnable() {
                                public void run() {
                                    if (!isPageInTransition()) {
                                        AppWidgetResizeFrame.showForWidget(hostView, cellLayout);
                                    }
                                }
                            };
                        }
                    }

                    //修改數據庫
                    mLauncher.getModelWriter().modifyItemInDatabase(info, container, screenId,
                            lp.cellX, lp.cellY, item.spanX, item.spanY);
                } else {
                    //沒有找到cell
                    if (!returnToOriginalCellToPreventShuffling) {
                        onNoCellFound(dropTargetLayout);
                    }

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

            //動畫效果
            final CellLayout parent = (CellLayout) cell.getParent().getParent();
            if (d.dragView.hasDrawn()) {
                if (droppedOnOriginalCellDuringTransition) {
                    // Animate the item to its original position, while simultaneously exiting
                    // spring-loaded mode so the page meets the icon where it was picked up.
                    mLauncher.getDragController().animateDragViewToOriginalPosition(
                            onCompleteRunnable, cell, SPRING_LOADED_TRANSITION_MS);
                    mLauncher.getStateManager().goToState(NORMAL);
                    mLauncher.getDropTargetBar().onDragEnd();
                    parent.onDropChild(cell);
                    return;
                }
                final ItemInfo info = (ItemInfo) cell.getTag();
                boolean isWidget = info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
                        || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
                if (isWidget) {
                    int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE :
                            ANIMATE_INTO_POSITION_AND_DISAPPEAR;
                    animateWidgetDrop(info, parent, d.dragView, null, animationType, cell, false);
                } else {
                    int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION;
                    mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
                            this);
                }
            } else {
                d.deferDragViewCleanupPostAnimation = false;
                cell.setVisibility(VISIBLE);
            }
            parent.onDropChild(cell);
            
            //設置OVERVIEW-->NORMAL狀態
            mLauncher.getStateManager().goToState(
                    NORMAL, SPRING_LOADED_EXIT_DELAY, onCompleteRunnable);
        }

        if (d.stateAnnouncer != null && !droppedOnOriginalCell) {
            d.stateAnnouncer.completeAction(R.string.item_moved);
        }
    }
DropExternal() 功能,有點累了,不翻譯了
 *Drop an item that didn't originate on one of the workspace screens.
* It may have come from Launcher (e.g. from all apps or customize), or it may have
* come from another app altogether.

 調用 onDropCompleted() 並傳送拖拽結果通知拖拽源(被拖拽物最初所在的容器)拖拽結果 

 

    private void dispatchDropComplete(View dropTarget, boolean accepted) {
        if (!accepted) {
            // If it was not accepted, cleanup the state. If it was accepted, it is the
            // responsibility of the drop target to cleanup the state.
            mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY);
            mDragObject.deferDragViewCleanupPostAnimation = false;
        }

        mDragObject.dragSource.onDropCompleted(dropTarget, mDragObject, accepted);
    }

大結局

回到OnDriverDragEnd. 結束拖拽DragController.endDrag(),這個方法會清理dragDriver/dragView,

如果不是 Deferred,立即執行把 DragView 從 DragLayer 中移除,然後通知監聽者 onDragEnd()

調用DragListener的onDragEnd方法。最後釋放VelocityTracker,等待下一輪觸摸事件的來臨。

    private void endDrag() {
        if (isDragging()) {
            mDragDriver = null;
            boolean isDeferred = false;
            if (mDragObject.dragView != null) {
                isDeferred = mDragObject.deferDragViewCleanupPostAnimation;
                if (!isDeferred) {
                    mDragObject.dragView.remove();
                } else if (mIsInPreDrag) {
                    animateDragViewToOriginalPosition(null, null, -1);
                }
                mDragObject.dragView = null;
            }

            // Only end the drag if we are not deferred
            if (!isDeferred) {
                callOnDragEnd();
            }
        }

        mFlingToDeleteHelper.releaseVelocityTracker();
    }

總結      

在 Launcher 的拖拽框架中,由 DragController 擔當指揮中心,用 DragSource 抽象拖拽的來源,用 DragView 描繪拖拽視圖,設置 DragListener 通知拖拽的開始和結束,整個拖拽過程中,由 DragObject 執行拖拽事務,與 DragSource 相對的 DropTarget 描述拖拽的目的地,DragSource、 DropTarget 代表“你從哪裏來,你到哪裏去”,是拖拽框架中核心問題的抽象。當長按應用 icon 觸發後,DragLayer 把觸摸事件攔截,再傳遞給控制中心 DragControlller 處理。拖拽結束後,在 DropTarget 的 onDrop() 方法中處理拖拽的結束的事務。拖拽以 DragListener 的 onStartDrag() 標誌拖拽的開始,以 onDragEnd() 標誌拖拽的完全結束。
 

拖拽控制框架圖

以從 Workspace 拖拽一個 icon(BubbleTextView)到另外一個位置爲例

拖拽時序圖

 

 

參考:

https://blog.csdn.net/long375577908/article/details/79457700

https://blog.csdn.net/metasearch/article/details/78176250

https://blog.csdn.net/dingfengnupt88/article/details/51816030

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章