本文檔是針對Android6.0版本的Launcher進行分析。各個版本的Launcher是有差異的,廠商也對Launcher客製化的比較多,所以本文只對Google原生代碼的拖拽部分進行分析。
2、簡介
拖拽是用戶在操作桌面經常用到的操作也是客製化比較多的其中之一。拖拽可以分爲以下幾類:
1、 主屏幕上的ICON和Wiget
2、 文件夾中的圖標
3、 抽屜中的ICON和Wiget
這三種情形的處理流程是相似的,所以我們只對在主屏幕的ICON和Wiget拖拽進行分析。
在本文檔中,我很多都在代碼中加了備註,方便查看。
3、代碼目錄結構
主要用到的類有: Launcher.java
Workspace.java
DragController.java
4、架構流程分析
主要的流程可以分爲三大步:
1、 點擊開始拖拽;
2、 拖拽過程中;
3、 拖到目標位置完成拖拽;
4.1、模塊主要類
主要用到的類有: Launcher.java
Workspace.java
DragController.java
4.2、流程分析以及流程圖
原生Launcher上的拖拽處理都是通過長按開始的,Workspace的長按是在Launcher進行處理。
4.2.1、第一部分 長按開始拖拽
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; } } |
流程如下:
先判斷是否允許拖拽-----》是否鎖定狀態----》長按空白處-----》是否在縮略圖模式
final boolean inHotseat = isHotseatLayout(v);//是否熱鍵欄 if (!mDragController.isDragging()) {//沒有進行拖拽 if (itemUnderLongClick == null) {//如果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 {//不是抽屜按鈕也不在文件夾中的元素 final boolean isAllAppsButton = inHotseat && isAllAppsButtonRank( mHotseat.getOrderInHotseat( longClickCellInfo.cellX, longClickCellInfo.cellY));//判斷長按是否allapp按鈕 if (!(itemUnderLongClick instanceof Folder || isAllAppsButton)) {//長按的不是allapp按鈕也不在文件夾展開的佈局中 // User long pressed on an item //調用Workspace.startDrag處理拖動 mWorkspace.startDrag(longClickCellInfo); } } } return true; |
如果是非空白處也就是在獲取桌面CellLayout上一個被拖動的對象。處理代碼如下:
接着處理,在非空白處長按,且沒有拖拽,包含單元信息,長按的不是allapp按鈕也不在文件夾就調用Workspace的startDrag方法進行處理。
以上兩處實際上就是對拖拽的條件進行了限定。長按桌面進入縮略圖也是在此進行處理,因爲本文檔是講拖拽,所以不對縮略圖進行過多講解。
public void startDrag(CellLayout.CellInfo cellInfo) { startDrag(cellInfo, false); } |
接下來我們具體分析Workspace的startDrag方法。上代碼:
繼續深入:
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); } |
先判斷拖拽對象是否處於touch狀態,
如果是的就直接返回;然後隱藏拖拽對象;
標記該位置爲未佔用,目的是讓在拖拽擠壓的過程中,
public void (View child, Point relativeTouchPos, DragSource source, boolean accessible) { //取消拖拽的焦點設置不可按 child.clearFocus(); child.setPressed(false);
// The outline is used to visualize where the item will land if dropped //創建拖拽對象投射輪廓 mDragOutline = createDragOutline(child, DRAG_BITMAP_PADDING);
mLauncher.onDragStarted(child); // The drag bitmap follows the touch point around on the screen拖動的位圖跟着觸摸點在屏幕上週圍 AtomicInteger padding = new AtomicInteger(DRAG_BITMAP_PADDING); final Bitmap b = createDragBitmap(child, padding);//創建拖拽圖像 |
可以讓其他圖標占據;最後到beginDragShared方法。
// Clear the pressed state if necessary //如有必要,清除按下狀態 if (child instanceof BubbleTextView) { BubbleTextView icon = (BubbleTextView) child; icon.clearPressedBackground(); } if (child.getTag() == null || !(child.getTag() instanceof ItemInfo)) { String msg = "Drag started with a view that has no tag set. This " + "will cause a crash (issue 11627249) down the line. " + "View: " + child + " tag: " + child.getTag(); throw new IllegalStateException(msg); } if (child.getParent() instanceof ShortcutAndWidgetContainer) { mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent(); } // 創建拖拽視圖 DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible); dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
b.recycle(); |
流程如圖:
其中,經常出錯的就是繪製拖拽對象輪廓,計算較多,容易出錯。要多留意。
public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY, DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion, float initialDragViewScale, boolean accessible) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); }
// Hide soft keyboard, if visible // 隱藏軟件盤 if (mInputMethodManager == null) { mInputMethodManager = (InputMethodManager) mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE); } mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
for (DragListener listener : mListeners) { listener.onDragStart(source, dragInfo, dragAction); } |
除此之外,還有最重要的就是DragController的startDrag()。創建拖拽視圖。
在startDrag中將分爲兩大部分進行處理。
public void onDragStart(final DragSource source, Object info, int dragAction) { if (ENFORCE_DRAG_EVENT_ORDER) { enfoceDragParity("onDragStart", 0, 0); } 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(); } } |
隱藏軟鍵盤。調用各個監聽對象 調用各個監聽對象實現的onDragStart方法 這裏就是在Workspace中的實現。代碼如下:
mLauncher.lockScreenOrientation();// 鎖定屏幕
InstallShortcutReceiver.enableInstallQueue();正在拖拽的時候,防止卸載或安裝導致快捷圖標變化更新數據庫的操作。
addExtraEmptyScreenOnDrag();添加新的空白頁
// 記錄當前的狀態 mDragging = true; mIsAccessibleDrag = accessible; mDragObject = new DropTarget.DragObject(); mDragObject.dragComplete = false; if (mIsAccessibleDrag) { //對於訪問的拖拽,我們假設視圖被從中心拖動。 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; |
接下來回到DragController的startDrag()方法。
dragView.show();顯示DragView對象(將該DragView添加到DragLayer上)
handleMoveEvent();根據當前的位置處理移動事件
先對show進行分析
public void show(int touchX, int touchY) { mDragLayer.addView(this); // Start the pick-up animation 啓動拾取動畫 DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0); lp.width = mBitmap.getWidth(); lp.height = mBitmap.getHeight(); lp.customPosition = true; setLayoutParams(lp); // 設置顯示位置 setTranslationX(touchX - mRegistrationX); setTranslationY(touchY - mRegistrationY); // Post the animation to skip other expensive work happening on the first frame //動畫播放 post(new Runnable() { public void run() { mAnim.start(); } }); } |
如註釋所寫:顯示DragView對象
4.2.2、第二部分 拖拽過程中
private void handleMoveEvent(int x, int y) { mDragObject.dragView.move(x, y); final int[] coordinates = mCoordinatesTemp; // 查找拖拽目標 DropTarget dropTarget = findDropTarget(x, y, coordinates); //更新拖拽對象的位置 mDragObject.x = coordinates[0]; mDragObject.y = coordinates[1]; checkTouchMove(dropTarget);// 檢查拖動時的狀態̬ // 檢查我們是否在滾動區域上空盤旋 mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y); mLastTouch[0] = x; mLastTouch[1] = y; checkScrollState(x, y);// 對拖動時的翻頁進行判斷處理 } |
handleMoveEvent();根據當前的位置處理移動事件
handleMoveEvent()是拖拽的主要方法。當用戶觸發拖拽後,DragController將通過該方法移動被拖拽物視圖。
findDropTarget(x,y, coordinates); 使用了findDropTarget來查找當前位置對應的拖拽目的對象。其基本原理就是遍歷所有已註冊的拖拽目的對象,若其支持放入且當前位置位於該對象的觸發區域內,則匹配成功返回該對象。
// 檢查拖動時的狀態 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; } |
checkTouchMove(dropTarget);// 檢查拖動時的狀態
if (x < mScrollZone) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; if (mDragScroller.onEnterScrollArea(x, y, forwardDirection)) { dragLayer.onEnterScrollArea(forwardDirection); mScrollRunnable.setDirection(forwardDirection); mHandler.postDelayed(mScrollRunnable, delay); } } } else if (x > mScrollView.getWidth() - mScrollZone) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; if (mDragScroller.onEnterScrollArea(x, y, backwardsDirection)) { dragLayer.onEnterScrollArea(backwardsDirection); mScrollRunnable.setDirection(backwardsDirection); mHandler.postDelayed(mScrollRunnable, delay); } } } else { clearScrollRunnable(); } |
checkScrollState(x, y);// 對拖動時的翻頁進行判斷處理
找了一張網上的圖片
4.2.3 第三部分 完成拖拽
public boolean onTouchEvent(MotionEvent ev) { 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; } |
當用戶將控件拖拽到目標位後,將手指從屏幕移開,處理流程如下:上代碼
通過手指在屏幕的向上事件,onTouchEvent的MotionEvent.ACTION_UP處理。
1、 先判斷是否在拖拽中。
2、 再判斷是否到達可刪除的區域
接着判斷vec != null
如果不爲空則拖動到垃圾箱中進行刪除。爲空則是放下Drop()的動作。
說明:onTouchEvent的MotionEvent.ACTION_UP處理和onInterceptTouchEvent的MotionEvent.ACTION_UP處理流程是一樣的。
private void drop(float x, float y) { final int[] coordinates = mCoordinatesTemp; // x,y所在區域是否有合適的目標 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); } |
下面來看一下Drop方法:
findDropTarget();查找當前位置對應的拖拽目的對象,
其基本原理就是遍歷所有已註冊的拖拽目的對象,若其支持放入且當前位置位於該對象的觸發區域內,則匹配成功返回該對象.
接着判斷是否找到有效的拖拽目的對象(dropTarget != null)
mDragObject.dragComplete = true;// 標記拖拽完成
dropTarget.onDragExit(mDragObject);// 通知拖拽目的對象已離開
if判斷 dropTarget.acceptDrop(mDragObject)是否支持放入
dropTarget.onDrop(mDragObject);// 拖拽物被放置到拖拽目的 // 這個方法最終將拖拽對象放置到目標位置,Workspace實現該方法。
以上的流程藉助網上的一張圖片:
下面分析findDropTarget();查找當前位置對應的拖拽目的對象,
private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 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); mDragObject.x = x;// 更新被拖拽物的位置信息 mDragObject.y = y; if (r.contains(x, y)) {// 指定位置是否位於有效出發範圍內 dropCoordinates[0] = x; dropCoordinates[1] = y; mLauncher.getDragLayer().mapCoordInSelfToDescendent((View) target, dropCoordinates); return target; } } return null; } |
其基本原理就是遍歷所有已註冊的拖拽目的對象,若其支持放入且當前位置位於該對象的觸發區域內,則匹配成功返回該對象.上原代碼:
如註釋縮寫,不過多解釋。
public void onDrop(final DragObject d) { mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter); CellLayout dropTargetLayout = mDropToLayout; } |
下面對Workspace中的Ondrop重點代碼拿出來說明
DragObject.getVisualCenter(mDragViewVisualCenter);//計算拖動View的視覺中心
if (dropTargetLayout != null) { if (mLauncher.isHotseatLayout(dropTargetLayout)) { mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter); } else { mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null); } } |
CellLayout dropTargetLayout = mDropToLayout;// Drop的Celllayout對象
if (!mInScrollArea && createUserFolderIfNecessary(cell, container, dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) { return; } |
這個判斷當前是否在Hotseat上,求出相對於dropTargetLayout的視覺中心座標。
以上是:如果拖拽的對象是一個快捷圖標並且最近的位置上也是一個快捷圖標,就創建一個文件夾來防止這兩個圖標。
繼續
if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell, distance, d, false)) { return; } |
添加到已存在的文件夾上
if(getScreenIdForPageIndex(mCurrentPage)!=screenId && !hasMovedIntoHotseat) { snapScreen = getPageIndexForScreenId(screenId); snapToPage(snapScreen); } |
拖動時可能落點在別的頁面,所以還會有頁面滑動的效果
LauncherModel.modifyItemInDatabase(mLauncher, info, container, screenId, lp.cellX, lp.cellY, item.spanX, item.spanY); |
對數據庫進行更新的操作。
這樣整個的拖拽就是這個樣子。