前言
在Android手機桌面,我們經常會把一個應用的圖標從菜單裏面,拖拽到桌面。或者把一個應用的圖標移到自己更加喜歡的位置。拖拽能夠讓用戶方便的把應用放到用戶可記得易操作的位置,從而能夠讓用戶快捷的打開高頻使用的應用。同時,拖拽也可以讓用戶能夠佈置自己的桌面,能夠把應用進行分類的存放。因此,Launcher拖拽讓用戶可自定義桌面。
拖拽的內容:
- 主屏幕(Workspace)上的圖標和小部件
- 文件夾中的圖標
- 抽屜中的圖標和小部件
拖拽過程是怎樣的,就像大象裝冰箱是一樣的,分三步長按(把冰箱門打開)拖拽(把大象放進冰箱)放開(帶上冰箱門)。
第一步:長按處理及準備工作
長按就要有監聽器,首先看監聽器是如何設置的。
在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