1. 程式人生 > >Android 4.0 Launcher2原始碼分析—桌面快捷圖示的拖拽

Android 4.0 Launcher2原始碼分析—桌面快捷圖示的拖拽

通過上一篇文章Android4.0Launcher2原始碼分析(五)——Workspace的滑動中,已經瞭解了LauncherViewTree中各層所負責的工作,在DragLayer中就負責對快捷圖示和AppWidget等元件的拖拽工作。桌面的滑動和圖示的拖拽是兩項獨立的工作,正常情況下我們用手指滑動桌面會觸發滑動操作,而當長按一個圖示時,則會觸發圖示的拖拽操作,此時再滑動則會拖拽圖示移動而桌面不會滑動。那麼這裡就分兩大部分來探討:1、拖拽操作的啟動。2、拖拽。

一、拖拽操作的啟動

那麼首先進入Launcher.onCreate()中來探究下如何啟用拖拽的狀態。

  1. protected void onCreate(Bundle savedInstanceState) {  
  2.         ......  
  3.         setupViews();  
  4.         ......  
  5.     }  

接著進入setupViews();

  1. private void setupViews() {  
  2.         ......  
  3.         mWorkspace.setOnLongClickListener(this);  
  4.         ......  
  5.     }  

從這裡我們可以看到對Workspace設定了OnLongClickListener,而Launcher又實現了這個介面。接著進入Launcher.onLongClick()

  1. public boolean onLongClick(View v) {  
  2.         ......  
  3.         if (!(v instanceof CellLayout)) {  
  4.             v = (View) v.getParent().getParent();  
  5.         }  
  6.         resetAddInfo();  
  7.         CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag();  
  8.         ......  
  9.         // The hotseat touch handling does not go through Workspace, and we always allow long press   
  10.         // on hotseat items.   
  11.         final View itemUnderLongClick = longClickCellInfo.cell;  
  12.         boolean allowLongPress = isHotseatLayout(v) || mWorkspace.allowLongPress();  
  13.         if (allowLongPress && !mDragController.isDragging()) {  
  14.             if (itemUnderLongClick == null) {  
  15.                 ......  
  16.             } else {  
  17.                 if (!(itemUnderLongClick instanceof Folder)) {  
  18.                     // User long pressed on an item   
  19.                     mWorkspace.startDrag(longClickCellInfo);  
  20.                 }  
  21.             }  
  22.         }  
  23.         return true;  
  24.     }  

當用戶在一個item上長按時,則itemUnderLongClick != null,再通過呼叫Workspace.startDrag()來啟用item的拖拽。下面先通過時序圖來看下拖拽狀態啟用所經歷的過程:

Android <wbr>4.0 <wbr>Launcher2原始碼分析—桌面快捷圖示的拖拽

圖示拖拽功能的啟用大概可以分為六步,下面就一步一步的探究下其中的實現:

Step1:Workspace.startDrag(CellLayout.CellInfocellInfo)

  1. void startDrag(CellLayout.CellInfo cellInfo) {  
  2.         View child = cellInfo.cell;  
  3.         ......  
  4.         mDragInfo = cellInfo;  
  5.         //使圖示從桌面上消失,給人一種被“拖到空中”的感覺   
  6.         child.setVisibility(GONE);  
  7.         ......  
  8.         final Canvas canvas = new Canvas();  
  9.         // We need to add extra padding to the bitmap to make room for the glow effect   
  10.         final int bitmapPadding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS;  
  11.         // The outline is used to visualize where the item will land if dropped   
  12.         //圖示的輪廓,在桌面上的對應的位置繪製圖標的輪廓,顯示當手鬆開圖示時它在桌面上的落點   
  13.         mDragOutline = createDragOutline(child, canvas, bitmapPadding);  
  14.         beginDragShared(child, this);  
  15.     }  

在這個方法中,主要的工作就是讓圖示從桌面上消失,並且顯示一個圖示的外部輪廓,以表明它將要放置的位置,其顯示的效果如下

Android <wbr>4.0 <wbr>Launcher2原始碼分析—桌面快捷圖示的拖拽

顯示圖示的輪廓可以從視覺上給使用者更加好的體驗。接著,進入beginDragShared()

Step2:Workspace.beginDragShared(Viewchild,DragSource source)

  1. public void beginDragShared(View child, DragSource source) {  
  2.         ......  
  3.         // The drag bitmap follows the touch point around on the screen   
  4.         final Bitmap b = createDragBitmap(child, new Canvas(), bitmapPadding);  
  5.         final int bmpWidth = b.getWidth();  
  6.         //我們將在DragLayer中繪製“拖拽後”的圖示,通過DragLayer.getLoactionInDragLayer()   
  7.         //獲取在DragLayer中的座標,並存放在mTempXY中。   
  8.         mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY);  
  9.         final int dragLayerX = (int) mTempXY[0] + (child.getWidth() - bmpWidth) / 2;  
  10.         int dragLayerY = mTempXY[1] - bitmapPadding / 2;  
  11.         Point dragVisualizeOffset = null;  
  12.         Rect dragRect = null;  
  13.         //無論child是BubbleTextView或者PagedViewIncon或者FolderIcon的例項   
  14.         //定點陣圖標的位置與大小   
  15.         if (child instanceof BubbleTextView || child instanceof PagedViewIcon) {  
  16.             int iconSize = r.getDimensionPixelSize(R.dimen.app_icon_size);  
  17.             int iconPaddingTop = r.getDimensionPixelSize(R.dimen.app_icon_padding_top);  
  18.             int top = child.getPaddingTop();  
  19.             int left = (bmpWidth - iconSize) / 2;  
  20.             int right = left + iconSize;  
  21.             int bottom = top + iconSize;  
  22.             dragLayerY += top;  
  23.             // Note: The drag region is used to calculate drag layer offsets, but the   
  24.             // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.   
  25.             dragVisualizeOffset = new Point(-bitmapPadding / 2, iconPaddingTop - bitmapPadding / 2);  
  26.             dragRect = new Rect(left, top, right, bottom);  
  27.         } else if (child instanceof FolderIcon) {  
  28.             int previewSize = r.getDimensionPixelSize(R.dimen.folder_preview_size);  
  29.             dragRect = new Rect(0, 0, child.getWidth(), previewSize);  
  30.         }  
  31.         ......  
  32.         mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),  
  33.                 DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect);  
  34.         b.recycle();  
  35.     }  

Workspace.beginSharedDrag()中主要所做的工作就是計算拖拽目標位於DragLayer中的座標和尺寸大小,接著又呼叫DragController.startDrag()

Step3:DragController.startDrag(Bitmapb ,int dragLayerX, int dragLayerY,DragSource source, ObjectdragInfo, int dragAction, Point dragOffset, RectdragRegion)

  1. public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,  
  2.         DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion) {  
  3.     ......  
  4.     for (DragListener listener : mListeners) {  
  5.         listener.onDragStart(source, dragInfo, dragAction);  
  6.     }  
  7.     final int registrationX = mMotionDownX - dragLayerX;  
  8.     final int registrationY = mMotionDownY - dragLayerY;  
  9.     final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;  
  10.     final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;  
  11.     //設定mDragging=true,表示拖拽已經開始   
  12.     //在DragLayer的onInterceptTouchEvent()中根據這個值判斷是否攔截MotionEvent   
  13.     mDragging = true;  
  14.     //例項化DragObject,表示拖拽的物件   
  15.     //封裝了拖拽物件的資訊   
  16.     mDragObject = new DropTarget.DragObject();  
  17.     mDragObject.dragComplete = false;  
  18.     mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);  
  19.     mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);  
  20.     mDragObject.dragSource = source;  
  21.     mDragObject.dragInfo = dragInfo;  
  22.     ......  
  23.     final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,  
  24.             registrationY, 0, 0, b.getWidth(), b.getHeight());  
  25.     ......  
  26.     //將拖拽的圖示顯示在DragLayer中   
  27.     dragView.show(mMotionDownX, mMotionDownY);  
  28.     handleMoveEvent(mMotionDownX, mMotionDownY);  
  29. }  


程式碼中顯示通過一個for語句呼叫了DragListener.onDragStart()方法,通知它們已經開始拖拽了,其中由於Workspace實現了DragListener並且新增到了mListeners中。所以Workspace.onDragStart()被呼叫。然後又封裝了一個DragObject物件,封裝DragSourceDragInfoDragView等資訊。接著,將呼叫DragView.show()DragView顯示在DragLayer中。

Step4:DragView.show(int touchX,inttouchY)

  1.     public void show(int touchX, int touchY) {  
  2.         //將DragView新增到DragLayer中   
  3.         mDragLayer.addView(this);  
  4.         //設定位置、尺寸等資訊   
  5.         DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0);  
  6.         lp.width = mBitmap.getWidth();  
  7.         lp.height = mBitmap.getHeight();  
  8.         lp.x = touchX - mRegistrationX;  
  9.         lp.y = touchY - mRegistrationY;  
  10.         lp.customPosition = true;  
  11.         setLayoutParams(lp);  
  12.         mLayoutParams = lp;  
  13.         mAnim.start();  
  14.     }  

其中的內容很簡單易懂,就是在將DragView新增到了DragLayer中,並且在合適的位置顯示了出來。接著應該呼叫在DragController.startDrag()中呼叫handleMoveEvent(),這個將在後文將拖拽過程分析時在看。到這一步,拖拽操作的啟動過程就完成了。接著就可以拖拽圖示了。

二、拖拽

通過了前面文章的分析,已經知道了拖拽過程的實現在DragLayer中,當進行圖示的拖拽時,DragLayer.onInterceptTouchEvent()就會對MotionEvent進行攔截。並且
在自身的onTouchEvent()方法中進行操作,從而實現圖示的移動。由於onInterceptTouchEvent()攔截了MotionEvent,因此Workspace等UI控制元件不會接收到事件,從而不會產生
干擾。那麼首先進入DragLayer.onInterceptTouchEvent():

  1. public boolean onInterceptTouchEvent(MotionEvent ev) {  
  2.         ......  
  3.         return mDragController.onInterceptTouchEvent(ev);  
  4.     }  

程式碼中省略了與其他功能的不部分程式碼,最後呼叫了DragController.onInterceptTouchEvent() ,並取其返回值作為自身方法的返回值。進入DragController.onInterceptTouchEvent()。

  1.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  2.         ......  
  3.         final int action = ev.getAction();  
  4.         final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());  
  5.         final int dragLayerX = dragLayerPos[0];  
  6.         final int dragLayerY = dragLayerPos[1];  
  7.         switch (action) {  
  8.             case MotionEvent.ACTION_MOVE:  
  9.                 break;  
  10.             case MotionEvent.ACTION_DOWN:  
  11.                 // Remember location of down touch   
  12.                 mMotionDownX = dragLayerX;  
  13.                 mMotionDownY = dragLayerY;  
  14.                 mLastDropTarget = null;  
  15.                 break;  
  16.             case MotionEvent.ACTION_UP:  
  17.                 if (mDragging) {  
  18.                     drop(dragLayerX, dragLayerY);  
  19.                 }  
  20.                 endDrag();  
  21.                 break;  
  22.             case MotionEvent.ACTION_CANCEL:  
  23.                 cancelDrag();  
  24.                 break;  
  25.         }  
  26.         return mDragging;  
  27.     }  

這裡我們關心的是它的返回值。可以看到方法將mDragging作為返回值。當觸發了拖拽狀態,在的DragController.startDrag()中將mDragging的值改為true。所以這裡也將返回trueDragLayer將攔截MotionEvent,並傳給自身的onTouchEvent()方法,在onTouchEvent()中對圖示進行移動,重新整理介面。

  1. public boolean onTouchEvent(MotionEvent ev) {  
  2.     ......  
  3.     final int action = ev.getAction();  
  4.     final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());  
  5.     final int dragLayerX = dragLayerPos[0];  
  6.     final int dragLayerY = dragLayerPos[1];  
  7.     switch (action) {  
  8.     case MotionEvent.ACTION_DOWN:  
  9.         // Remember where the motion event started   
  10.         mMotionDownX = dragLayerX;  
  11.         mMotionDownY = dragLayerY;  
  12.         //判斷當前的觸點是否處於螢幕邊緣的ScrollZone,當處於這個區域時   
  13.         //狀態mScrollState將轉變為SCROLL,並且在一定時間的停留之後,螢幕滑動到另一屏。   
  14.         if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) {  
  15.             mScrollState = SCROLL_WAITING_IN_ZONE;  
  16.             mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);  
  17.         } else {  
  18.             mScrollState = SCROLL_OUTSIDE_ZONE;  
  19.         }  
  20.         break;  
  21.     case MotionEvent.ACTION_MOVE:  
  22.         //呼叫handleMoveEvent()處理圖示移動   
  23.         handleMoveEvent(dragLayerX, dragLayerY);  
  24.         break;  
  25.     case MotionEvent.ACTION_UP:  
  26.         // Ensure that we've processed a move event at the current pointer location.   
  27.         handleMoveEvent(dragLayerX, dragLayerY);  
  28.         mHandler.removeCallbacks(mScrollRunnable);  
  29.         if (mDragging) {  
  30.           "WHITE-SPACE: pre">   //根據目前相對DragLayer的座標,將圖示“降落”到指定的DropTarget上。   
  31.             drop(dragLayerX, dragLayerY);  
  32.         }  
  33.         endDrag();  
  34.         break;  
  35.     case MotionEvent.ACTION_CANCEL:  
  36.         cancelDrag();  
  37.         break;  
  38.     }  
  39.     return true;  
  40. }  

onTouchEvent()中處理的事件涉及到不同狀態之間的轉換,以及每種狀態之下對相應的MotionEvent的對策。這裡同樣,從簡單的情況入手:圖示拖拽起來後,移動一段距離,在螢幕的另一個位置放下。

首先,當拖拽起圖示時,拖拽圖示的狀態被啟動,這就是第一部分所探討的內容。

然後,移動拖拽的圖示。此時觸發了MotionEvent.ACTION_MOVE事件,緊接著呼叫handleMoveEvent()來處理移動。進入handleMoveEvent()來看看圖示移動是怎麼實現的。

  1. private void handleMoveEvent(int x, int y) {  
  2.         //更新在DragLayer中的位置   
  3.         mDragObject.dragView.move(x, y);  
  4.         // Drop on someone?   
  5.         final int[] coordinates = mCoordinatesTemp;  
  6.         //根據當前的位置尋找DropTarget物件來放置圖示   
  7.         DropTarget dropTarget = findDropTarget(x, y, coordinates);  
  8.         mDragObject.x = coordinates[0];  
  9.         mDragObject.y = coordinates[1];  
  10.         if (dropTarget != null) {  
  11.             DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject);  
  12.             if (delegate != null) {  
  13.                 dropTarget = delegate;  
  14.             }  
  15.             if (mLastDropTarget != dropTarget) {  
  16.                 if (mLastDropTarget != null) {  
  17.                     //從最後一次記錄的DropTarget中退出   
  18.                     mLastDropTarget.onDragExit(mDragObject);  
  19.                 }  
  20.                 //進入到當前尋找到的DropTarget   
  21.                 dropTarget.onDragEnter(mDragObject);  
  22.             }  
  23.             dropTarget.onDragOver(mDragObject);  
  24.         } else {  
  25.             if (mLastDropTarget != null) {  
  26.                 mLastDropTarget.onDragExit(mDragObject);  
  27.             }  
  28.         }  
  29.         mLastDropTarget = dropTarget;  
  30.         // Scroll, maybe, but not if we're in the delete region.   
  31.         boolean inDeleteRegion = false;  
  32.         if (mDeleteRegion != null) {  
  33.             inDeleteRegion = mDeleteRegion.contains(x, y);  
  34.         }  
  35.         // After a scroll, the touch point will still be in the scroll region.   
  36.         // Rather than scrolling immediately, require a bit of twiddling to scroll again   
  37.         final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();  
  38.         mDistanceSinceScroll +=  
  39.             Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2));  
  40.         mLastTouch[0] = x;  
  41.         mLastTouch[1] = y;  
  42.         //判斷當前拖拽的圖示是否處於ScrollZone即滑動區域。   
  43.         //並且根據在哪個一個ScrollZone來處理螢幕滑動的方向。   
  44.         if (!inDeleteRegion && x < mScrollZone) {  
  45.             if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) {  
  46.                 mScrollState = SCROLL_WAITING_IN_ZONE;  
  47.                 if (mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT)) {  
  48.                     mScrollRunnable.setDirection(SCROLL_LEFT);  
  49.                     mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);  
  50.                 }  
  51.             }  
  52.         } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) {  
  53.             if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) {  
  54.                 mScrollState = SCROLL_WAITING_IN_ZONE;  
  55.                 if (mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT)) {  
  56.                     mScrollRunnable.setDirection(SCROLL_RIGHT);  
  57.                     mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);  
  58.                 }  
  59.             }  
  60.         } else {  
  61.             if (mScrollState == SCROLL_WAITING_IN_ZONE) {  
  62.                 mScrollState = SCROLL_OUTSIDE_ZONE;  
  63.                 mScrollRunnable.setDirection(SCROLL_RIGHT);  
  64.                 mHandler.removeCallbacks(mScrollRunnable);  
  65.                 mDragScroller.onExitScrollArea();  
  66.             }  
  67.         }  
  68.     }  

handleMoveEvent()主要處理拖拽過程中需要處理的事務。包括:1、在更新圖示在螢幕中的位置,並重新整理UI2、判斷圖示當前所處的位置。包括SCROLL_OUTSIDE_ZONESCROLL_WAITING_IN_ZONE,對處於SCROLL_WAITING_IN_ZONE位置時,需要根據具體的位置,向前或向後切換顯示的螢幕。再回到上面假設的情況中。則此時只是簡單的重新整理了位置資訊,並重新繪製圖標。

最後,當鬆開拖拽的物件時,觸發了MotionEvent.ACTION_UP事件。則進入下面一段程式碼: