前言
在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。
ItemLongClickListener.java
private static boolean onWorkspaceItemLongClick(View v) {
Launcher launcher = Launcher.getLauncher(v.getContext());
if (!canStartDrag(launcher)) return false;
if (!launcher.isInState(NORMAL) && !launcher.isInState(OVERVIEW)) return false;
if (!(v.getTag() instanceof ItemInfo)) return false;
launcher.setWaitingForResult(null);
beginDrag(v, launcher, (ItemInfo) v.getTag(), new DragOptions());
return true;
}
public static void beginDrag(View v, Launcher launcher, ItemInfo info,
DragOptions dragOptions) {
if (info.container >= 0) {
Folder folder = Folder.getOpen(launcher);
if (folder != null) {
if (!folder.getItemsInReadingOrder().contains(v)) {
folder.close(true);
} else {
folder.startDrag(v, dragOptions);
return;
}
}
}
CellLayout.CellInfo longClickCellInfo = new CellLayout.CellInfo(v, info);
launcher.getWorkspace().startDrag(longClickCellInfo, dragOptions);
}
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);
}
//判断是哪一类touchcontroller
protected boolean findActiveController(MotionEvent ev) {
mActiveController = null;
//浮动按钮
AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
if (topView != null && topView.onControllerInterceptTouchEvent(ev)) {
mActiveController = topView;
return true;
}
//mController是DragControler()还是AllAppsSwipeController()
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后 ,判断是否快速甩向屏幕上方触发删除的事件。isFlingingToDelete检查你是否在执行甩的动作是通过VelocityTracker来计算的,他会记录每次事件,然后计算出速度,包括X方向和Y方向的。如果不是在“扔”,就会调用放下图标最重要的一个方法, drop方法(flingAnimation=null),不管哪种删除最后都是通过completeDrop(...)删除);
进行不同的处理(如下代码)
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(); //fling效果并删除
} 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 folder buttonDropTarget等都实现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);
}
/**
* Called at the end of a drag which originated on the workspace.
*/
public void onDropCompleted(final View target, final DragObject d,
final boolean success) {
if (success) {
if (target != this && mDragInfo != null) {
removeWorkspaceItem(mDragInfo.cell);
}
} else if (mDragInfo != null) {
final CellLayout cellLayout = mLauncher.getCellLayout(
mDragInfo.container, mDragInfo.screenId);
if (cellLayout != null) {
cellLayout.onDropChild(mDragInfo.cell);
} else if (FeatureFlags.IS_DOGFOOD_BUILD) {
throw new RuntimeException("Invalid state: cellLayout == null in "
+ "Workspace#onDropCompleted. Please file a bug. ");
}
}
View cell = getHomescreenIconByItemId(d.originalDragInfo.id);
if (d.cancelled && cell != null) {
cell.setVisibility(VISIBLE);
}
mDragInfo = null;
}
大结局
回到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)到另外一个位置为例
拖拽时序图
参考: