Android 4.0 Launcher2原始碼分析—桌面快捷圖示的拖拽
通過上一篇文章Android4.0Launcher2原始碼分析(五)——Workspace的滑動中,已經瞭解了Launcher的ViewTree中各層所負責的工作,在DragLayer中就負責對快捷圖示和AppWidget等元件的拖拽工作。桌面的滑動和圖示的拖拽是兩項獨立的工作,正常情況下我們用手指滑動桌面會觸發滑動操作,而當長按一個圖示時,則會觸發圖示的拖拽操作,此時再滑動則會拖拽圖示移動而桌面不會滑動。那麼這裡就分兩大部分來探討:1、拖拽操作的啟動。2、拖拽。
一、拖拽操作的啟動
那麼首先進入Launcher.onCreate()中來探究下如何啟用拖拽的狀態。
- protected void onCreate(Bundle savedInstanceState) {
- ......
- setupViews();
- ......
- }
接著進入setupViews();
- private void setupViews() {
- ......
- mWorkspace.setOnLongClickListener(this);
- ......
- }
從這裡我們可以看到對Workspace設定了OnLongClickListener,而Launcher又實現了這個介面。接著進入Launcher.onLongClick()
- public boolean onLongClick(View v) {
- ......
- if (!(v instanceof CellLayout)) {
- v = (View) v.getParent().getParent();
- }
- resetAddInfo();
- CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag();
- ......
- // The hotseat touch handling does not go through Workspace, and we always allow long press
- // on hotseat items.
- final View itemUnderLongClick = longClickCellInfo.cell;
- boolean allowLongPress = isHotseatLayout(v) || mWorkspace.allowLongPress();
- if (allowLongPress && !mDragController.isDragging()) {
- if (itemUnderLongClick == null) {
- ......
- } else {
- if (!(itemUnderLongClick instanceof Folder)) {
- // User long pressed on an item
- mWorkspace.startDrag(longClickCellInfo);
- }
- }
- }
- return true;
- }
當用戶在一個item上長按時,則itemUnderLongClick != null,再通過呼叫Workspace.startDrag()來啟用item的拖拽。下面先通過時序圖來看下拖拽狀態啟用所經歷的過程:
圖示拖拽功能的啟用大概可以分為六步,下面就一步一步的探究下其中的實現:
Step1:Workspace.startDrag(CellLayout.CellInfocellInfo)
- void startDrag(CellLayout.CellInfo cellInfo) {
- View child = cellInfo.cell;
- ......
- mDragInfo = cellInfo;
- //使圖示從桌面上消失,給人一種被“拖到空中”的感覺
- child.setVisibility(GONE);
- ......
- final Canvas canvas = new Canvas();
- // We need to add extra padding to the bitmap to make room for the glow effect
- final int bitmapPadding = HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS;
- // The outline is used to visualize where the item will land if dropped
- //圖示的輪廓,在桌面上的對應的位置繪製圖標的輪廓,顯示當手鬆開圖示時它在桌面上的落點
- mDragOutline = createDragOutline(child, canvas, bitmapPadding);
- beginDragShared(child, this);
- }
在這個方法中,主要的工作就是讓圖示從桌面上消失,並且顯示一個圖示的外部輪廓,以表明它將要放置的位置,其顯示的效果如下
顯示圖示的輪廓可以從視覺上給使用者更加好的體驗。接著,進入beginDragShared()
Step2:Workspace.beginDragShared(Viewchild,DragSource source)
- public void beginDragShared(View child, DragSource source) {
- ......
- // The drag bitmap follows the touch point around on the screen
- final Bitmap b = createDragBitmap(child, new Canvas(), bitmapPadding);
- final int bmpWidth = b.getWidth();
- //我們將在DragLayer中繪製“拖拽後”的圖示,通過DragLayer.getLoactionInDragLayer()
- //獲取在DragLayer中的座標,並存放在mTempXY中。
- mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY);
- final int dragLayerX = (int) mTempXY[0] + (child.getWidth() - bmpWidth) / 2;
- int dragLayerY = mTempXY[1] - bitmapPadding / 2;
- Point dragVisualizeOffset = null;
- Rect dragRect = null;
- //無論child是BubbleTextView或者PagedViewIncon或者FolderIcon的例項
- //定點陣圖標的位置與大小
- if (child instanceof BubbleTextView || child instanceof PagedViewIcon) {
- int iconSize = r.getDimensionPixelSize(R.dimen.app_icon_size);
- int iconPaddingTop = r.getDimensionPixelSize(R.dimen.app_icon_padding_top);
- int top = child.getPaddingTop();
- int left = (bmpWidth - iconSize) / 2;
- int right = left + iconSize;
- int bottom = top + iconSize;
- dragLayerY += top;
- // Note: The drag region is used to calculate drag layer offsets, but the
- // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
- dragVisualizeOffset = new Point(-bitmapPadding / 2, iconPaddingTop - bitmapPadding / 2);
- dragRect = new Rect(left, top, right, bottom);
- } else if (child instanceof FolderIcon) {
- int previewSize = r.getDimensionPixelSize(R.dimen.folder_preview_size);
- dragRect = new Rect(0, 0, child.getWidth(), previewSize);
- }
- ......
- mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
- DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect);
- b.recycle();
- }
Workspace.beginSharedDrag()中主要所做的工作就是計算拖拽目標位於DragLayer中的座標和尺寸大小,接著又呼叫DragController.startDrag()
Step3:DragController.startDrag(Bitmapb ,int dragLayerX, int dragLayerY,DragSource source, ObjectdragInfo, int dragAction, Point dragOffset, RectdragRegion)
- public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,
- DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion) {
- ......
- for (DragListener listener : mListeners) {
- listener.onDragStart(source, dragInfo, dragAction);
- }
- 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;
- //設定mDragging=true,表示拖拽已經開始
- //在DragLayer的onInterceptTouchEvent()中根據這個值判斷是否攔截MotionEvent
- mDragging = true;
- //例項化DragObject,表示拖拽的物件
- //封裝了拖拽物件的資訊
- mDragObject = new DropTarget.DragObject();
- mDragObject.dragComplete = false;
- 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());
- ......
- //將拖拽的圖示顯示在DragLayer中
- dragView.show(mMotionDownX, mMotionDownY);
- handleMoveEvent(mMotionDownX, mMotionDownY);
- }
程式碼中顯示通過一個for語句呼叫了DragListener.onDragStart()方法,通知它們已經開始拖拽了,其中由於Workspace實現了DragListener並且新增到了mListeners中。所以Workspace.onDragStart()被呼叫。然後又封裝了一個DragObject物件,封裝DragSource、DragInfo和DragView等資訊。接著,將呼叫DragView.show()將DragView顯示在DragLayer中。
Step4:DragView.show(int touchX,inttouchY)
- public void show(int touchX, int touchY) {
- //將DragView新增到DragLayer中
- mDragLayer.addView(this);
- //設定位置、尺寸等資訊
- DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0);
- lp.width = mBitmap.getWidth();
- lp.height = mBitmap.getHeight();
- lp.x = touchX - mRegistrationX;
- lp.y = touchY - mRegistrationY;
- lp.customPosition = true;
- setLayoutParams(lp);
- mLayoutParams = lp;
- mAnim.start();
- }
其中的內容很簡單易懂,就是在將DragView新增到了DragLayer中,並且在合適的位置顯示了出來。接著應該呼叫在DragController.startDrag()中呼叫handleMoveEvent(),這個將在後文將拖拽過程分析時在看。到這一步,拖拽操作的啟動過程就完成了。接著就可以拖拽圖示了。
二、拖拽
通過了前面文章的分析,已經知道了拖拽過程的實現在DragLayer中,當進行圖示的拖拽時,DragLayer.onInterceptTouchEvent()就會對MotionEvent進行攔截。並且
在自身的onTouchEvent()方法中進行操作,從而實現圖示的移動。由於onInterceptTouchEvent()攔截了MotionEvent,因此Workspace等UI控制元件不會接收到事件,從而不會產生
干擾。那麼首先進入DragLayer.onInterceptTouchEvent():
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- ......
- return mDragController.onInterceptTouchEvent(ev);
- }
程式碼中省略了與其他功能的不部分程式碼,最後呼叫了DragController.onInterceptTouchEvent() ,並取其返回值作為自身方法的返回值。進入DragController.onInterceptTouchEvent()。
- public boolean onInterceptTouchEvent(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_MOVE:
- break;
- case MotionEvent.ACTION_DOWN:
- // Remember location of down touch
- mMotionDownX = dragLayerX;
- mMotionDownY = dragLayerY;
- mLastDropTarget = null;
- break;
- case MotionEvent.ACTION_UP:
- if (mDragging) {
- drop(dragLayerX, dragLayerY);
- }
- endDrag();
- break;
- case MotionEvent.ACTION_CANCEL:
- cancelDrag();
- break;
- }
- return mDragging;
- }
這裡我們關心的是它的返回值。可以看到方法將mDragging作為返回值。當觸發了拖拽狀態,在的DragController.startDrag()中將mDragging的值改為true。所以這裡也將返回true。DragLayer將攔截MotionEvent,並傳給自身的onTouchEvent()方法,在onTouchEvent()中對圖示進行移動,重新整理介面。
- 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:
- // Remember where the motion event started
- mMotionDownX = dragLayerX;
- mMotionDownY = dragLayerY;
- //判斷當前的觸點是否處於螢幕邊緣的ScrollZone,當處於這個區域時
- //狀態mScrollState將轉變為SCROLL,並且在一定時間的停留之後,螢幕滑動到另一屏。
- if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) {
- mScrollState = SCROLL_WAITING_IN_ZONE;
- mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
- } else {
- mScrollState = SCROLL_OUTSIDE_ZONE;
- }
- break;
- case MotionEvent.ACTION_MOVE:
- //呼叫handleMoveEvent()處理圖示移動
- 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) {
- "WHITE-SPACE: pre"> //根據目前相對DragLayer的座標,將圖示“降落”到指定的DropTarget上。
- drop(dragLayerX, dragLayerY);
- }
- endDrag();
- break;
- case MotionEvent.ACTION_CANCEL:
- cancelDrag();
- break;
- }
- return true;
- }
onTouchEvent()中處理的事件涉及到不同狀態之間的轉換,以及每種狀態之下對相應的MotionEvent的對策。這裡同樣,從簡單的情況入手:圖示拖拽起來後,移動一段距離,在螢幕的另一個位置放下。
首先,當拖拽起圖示時,拖拽圖示的狀態被啟動,這就是第一部分所探討的內容。
然後,移動拖拽的圖示。此時觸發了MotionEvent.ACTION_MOVE事件,緊接著呼叫handleMoveEvent()來處理移動。進入handleMoveEvent()來看看圖示移動是怎麼實現的。
- private void handleMoveEvent(int x, int y) {
- //更新在DragLayer中的位置
- mDragObject.dragView.move(x, y);
- // Drop on someone?
- final int[] coordinates = mCoordinatesTemp;
- //根據當前的位置尋找DropTarget物件來放置圖示
- DropTarget dropTarget = findDropTarget(x, y, coordinates);
- mDragObject.x = coordinates[0];
- mDragObject.y = coordinates[1];
- if (dropTarget != null) {
- DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject);
- if (delegate != null) {
- dropTarget = delegate;
- }
- if (mLastDropTarget != dropTarget) {
- if (mLastDropTarget != null) {
- //從最後一次記錄的DropTarget中退出
- mLastDropTarget.onDragExit(mDragObject);
- }
- //進入到當前尋找到的DropTarget
- dropTarget.onDragEnter(mDragObject);
- }
- dropTarget.onDragOver(mDragObject);
- } else {
- if (mLastDropTarget != null) {
- mLastDropTarget.onDragExit(mDragObject);
- }
- }
- mLastDropTarget = dropTarget;
- // Scroll, maybe, but not if we're in the delete region.
- boolean inDeleteRegion = false;
- if (mDeleteRegion != null) {
- inDeleteRegion = mDeleteRegion.contains(x, y);
- }
- // After a scroll, the touch point will still be in the scroll region.
- // Rather than scrolling immediately, require a bit of twiddling to scroll again
- final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();
- mDistanceSinceScroll +=
- Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2));
- mLastTouch[0] = x;
- mLastTouch[1] = y;
- //判斷當前拖拽的圖示是否處於ScrollZone即滑動區域。
- //並且根據在哪個一個ScrollZone來處理螢幕滑動的方向。
- if (!inDeleteRegion && x < mScrollZone) {
- if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) {
- mScrollState = SCROLL_WAITING_IN_ZONE;
- if (mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT)) {
- mScrollRunnable.setDirection(SCROLL_LEFT);
- mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
- }
- }
- } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) {
- if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) {
- mScrollState = SCROLL_WAITING_IN_ZONE;
- if (mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT)) {
- mScrollRunnable.setDirection(SCROLL_RIGHT);
- mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
- }
- }
- } else {
- if (mScrollState == SCROLL_WAITING_IN_ZONE) {
- mScrollState = SCROLL_OUTSIDE_ZONE;
- mScrollRunnable.setDirection(SCROLL_RIGHT);
- mHandler.removeCallbacks(mScrollRunnable);
- mDragScroller.onExitScrollArea();
- }
- }
- }
handleMoveEvent()主要處理拖拽過程中需要處理的事務。包括:1、在更新圖示在螢幕中的位置,並重新整理UI。2、判斷圖示當前所處的位置。包括SCROLL_OUTSIDE_ZONE和SCROLL_WAITING_IN_ZONE,對處於SCROLL_WAITING_IN_ZONE位置時,需要根據具體的位置,向前或向後切換顯示的螢幕。再回到上面假設的情況中。則此時只是簡單的重新整理了位置資訊,並重新繪製圖標。
最後,當鬆開拖拽的物件時,觸發了MotionEvent.ACTION_UP事件。則進入下面一段程式碼: