本文主要分析下Launcher3的拖拽流程。Launcher3的点击事件比较好分析,大都都是打开某个应用或者进入文件夹app界面。而拖拽流程就比较复杂,拖拽的触发是View的长按事件,而长按事件的处理是在Launcher3.java的onLongClick()方法中,我们一起来看下。
@Override
public boolean onLongClick(View v) {
...
if (v instanceof Workspace) {
Log.i("Tim_C","v is a workspace");
if (!mWorkspace.isInOverviewMode()) {
Log.i("Tim_C","v is in isInOverviewMode");
if (!mWorkspace.isTouchActive()) {
Log.i("Tim_C","v is workspace enter overview mode");
showOverviewMode(true);--------1
mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
return true;
} else {
return false;
}
} else {
return false;
}
}
...
// The hotseat touch handling does not go through Workspace, and we always allow long press
// on hotseat items.
if (!mDragController.isDragging()) {
if (itemUnderLongClick == null) {
Log.i("Tim_C","User long pressed on empty space");
// User long pressed on empty space
mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);--------3
if (mWorkspace.isInOverviewMode()) {
mWorkspace.startReordering(v);
} else {
showOverviewMode(true);------2
}
} else {
....
mWorkspace.startDrag(longClickCellInfo, dragOptions);
}
}
}
return true;
}
在调试代码的时候,发现在长按workspace后不会进入“----1”处的逻辑,而是直接进入"-------2"处的逻辑。原来在布局的时候CellLayout处于workspace的上面,事件先被CellLayout拦截,所v是一个CellLayout对象而不是Workspace对象。代码中有一个performHapticFeedback方法调用,主要作用是开启Workspace的触感反馈效果。函数经过一系列的判断函数最终会调用Workspace的startDrag()方法进入拖拽流程。startDrag()的具体实现在beginDragShared()函数中
packages\apps\Launcher3\src\com\android\launcher3\Workspace.java
public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
DragPreviewProvider previewProvider, DragOptions dragOptions) {
child.clearFocus();
child.setPressed(false);
mOutlineProvider = previewProvider;
// The drag bitmap follows the touch point around on the screen
final Bitmap b = previewProvider.createDragBitmap(mCanvas);
...
DeviceProfile grid = mLauncher.getDeviceProfile();
Point dragVisualizeOffset = null;
Rect dragRect = null;
if (child instanceof BubbleTextView) {
int iconSize = grid.iconSizePx;
int top = child.getPaddingTop();
int left = (b.getWidth() - 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(- halfPadding, halfPadding);
dragRect = new Rect(left, top, right, bottom);
} 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);
}
...
DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source,
dragObject, dragVisualizeOffset, dragRect, scale, dragOptions);
dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
b.recycle();
return dv;
}
注意函数中有对mOutlineProvider赋值,DragPreviewProvider的作用有两点,一是通过调用createDragBitmap()方法生成随手指移动的应用图标,二是生成outline图标(显示在手指下面的那个白框)。函数最后会调用DragController的startDrag()函数。
packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragController.java
public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
float initialDragViewScale, DragOptions options) {
...
mDragObject = new DropTarget.DragObject();
mIsDragDeferred = !mOptions.deferDragCondition.shouldStartDeferredDrag(0);
final Resources res = mLauncher.getResources();
final float scaleDps = FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND
? res.getDimensionPixelSize(R.dimen.dragViewScale)
: mIsDragDeferred
? res.getDimensionPixelSize(R.dimen.deferred_drag_view_scale)
: 0f;
final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
registrationY, initialDragViewScale, scaleDps);
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);
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.show(mMotionDownX, mMotionDownY);//显示跟随手指移动的应用图标
...
mLastTouch[0] = mMotionDownX;
mLastTouch[1] = mMotionDownY;
handleMoveEvent(mMotionDownX, mMotionDownY);//拖拽的移动处理
mLauncher.getUserEventDispatcher().resetActionDurationMillis();
return dragView;
}
函数创建了一个DropTarget.DragObject对象,之后对其实参数进行赋值,其中创建的DragView是对Bitmap的封装,并调用其show()方法来显示应用图标,最后调用handleMoveEvent()方法来处理拖拽的移动。
packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragController.java
private void handleMoveEvent(int x, int y) {
mDragObject.dragView.move(x, y);
// Drop on someone?
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;
checkScrollState(x, y);
if (mIsDragDeferred && mOptions.deferDragCondition.shouldStartDeferredDrag(
Math.hypot(x - mMotionDownX, y - mMotionDownY))) {
startDeferredDrag();
}
}
代码中调用dragView的move()来绘制更新位置,调用checkTouchMove()来检测进入某个DropTarget,DropTarget有实现onDragExit,onDragEnter,onDragOver方法需要处理。比如说当拖动到文件夹上面会打开显示文件夹,拖动到删除区域改变颜色等,checkScrollState则是用来检测是否应当滑动到下一页。可以想到这个函数是需要不断调用,以更新显示拖拽图标的位置。其实跟踪下代码可以看到DragLayer的onTouchEvent被DragController接管了。主要代码如下:
packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragLayer.java
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
if (mActiveController != null) {
return mActiveController.onTouchEvent(ev);
}
return false;
}
packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragController.java
(implements DragDriver.EventListener)
public boolean onTouchEvent(MotionEvent ev) {
...
return mDragDriver.onTouchEvent(ev);
}
@Override
public void onDriverDragMove(float x, float y) {
final int[] dragLayerPos = getClampedDragLayerPos(x, y);
handleMoveEvent(dragLayerPos[0], dragLayerPos[1]);
}
packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragDriver.java
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(), null);
break;
case MotionEvent.ACTION_CANCEL:
mEventListener.onDriverDragCancel();
break;
}
return true;
}
好了,我们已经快要分析完了。当然当我们停下来手指离开屏幕就会触发ACTION_UP事件,从而调用DragController的onDriverDragEnd()方法,这个方法简单只是找到DropTarget对象并调用drop()函数和endDrag()函数。drop函数代码如下:
packages\apps\Launcher3\src\com\android\launcher3\dragndrop\DragController.java
void drop(DropTarget dropTarget, float x, float y, PointF flingVel) {
...
// Drop onto the target.
boolean accepted = false;
if (dropTarget != null) {
dropTarget.onDragExit(mDragObject);
if (dropTarget.acceptDrop(mDragObject)) {
if (flingVel != null) {
dropTarget.onFlingToDelete(mDragObject, flingVel);
} else {
dropTarget.onDrop(mDragObject);
}
accepted = true;
}
}
final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
mDragObject.dragSource.onDropCompleted(
dropTargetAsView, mDragObject, flingVel != null, accepted);
mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView);
if (mIsDragDeferred) {
mOptions.deferDragCondition.onDropBeforeDeferredDrag();
}
}
可以看到drop()函数会调onFlingToDelete和onDrop函数,当一种情况是当我们快速地向上滑动的时候调用,第二种是普通的拖拽调用。之后会调用onDropCompleted函数。第一种情况的具体实现是在DeleteDropTarget中,我们来看下最后调用的代码:
packages\apps\Launcher3\src\com\android\launcher3\DeleteDropTarget.java
/**
* Removes the item from the workspace. If the view is not null, it also removes the view.
*/
public static void removeWorkspaceOrFolderItem(Launcher launcher, ItemInfo item, View view) {
// Remove the item from launcher and the db, we can ignore the containerInfo in this call
// because we already remove the drag view from the folder (if the drag originated from
// a folder) in Folder.beginDrag()
launcher.removeItem(view, item, true /* deleteFromDb */);
launcher.getWorkspace().stripEmptyScreens();
launcher.getDragLayer().announceForAccessibility(launcher.getString(R.string.item_removed));
}
代码很简单,直接删除掉拖拽对象,并且删除空页面。对于第二种情况,因为大多数的拖拽都是在Workspace上面,所以我们来看Workspace的onDrop的实现。
packages\apps\Launcher3\src\com\android\launcher3\Workspace.java
public void onDrop(final DragObject d) {
...
if (d.dragSource != this) {
final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1] };
onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d);
} else if (mDragInfo != null) {
final View cell = mDragInfo.cell;
if (dropTargetLayout != null && !d.cancelled) {
...
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 (!mInScrollArea && createUserFolderIfNecessary(cell, container,
dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) {
return;
}
if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
distance, d, false)) {
return;
}
int[] resultSpan = new int[2];
mTargetCell = dropTargetLayout.performReorder((int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY, cell,
mTargetCell, resultSpan, CellLayout.MODE_ON_DROP);
if (foundCell) {
final ItemInfo info = (ItemInfo) cell.getTag();
if (hasMovedLayouts) {
// Reparent the view
CellLayout parentCell = getParentCellLayoutForView(cell);
if (parentCell != null) {
parentCell.removeView(cell);
} else if (ProviderConfig.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;
...
LauncherModel.modifyItemInDatabase(mLauncher, info, container, screenId, lp.cellX,
lp.cellY, item.spanX, item.spanY);
} else {
// 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();
// Prepare it to be animated into its new position
// This must be called after the view has been re-parented
final Runnable onCompleteRunnable = new Runnable() {
@Override
public void run() {
mAnimatingViewIntoPlace = false;
updateChildrenLayersEnabled(false);
}
};
mAnimatingViewIntoPlace = true;
if (d.dragView.hasDrawn()) {
...
if (isWidget) {
int animationType = resizeOnDrop ? ANIMATE_INTO_POSITION_AND_RESIZE :
ANIMATE_INTO_POSITION_AND_DISAPPEAR;
animateWidgetDrop(info, parent, d.dragView,
onCompleteRunnable, animationType, cell, false);
} else {
int duration = snapScreen < 0 ? -1 : ADJACENT_SCREEN_DROP_DURATION;
mLauncher.getDragLayer().animateViewIntoPosition(d.dragView, cell, duration,
onCompleteRunnable, this);
}
} else {
d.deferDragViewCleanupPostAnimation = false;
cell.setVisibility(VISIBLE);
}
parent.onDropChild(cell);
}
if (d.stateAnnouncer != null) {
d.stateAnnouncer.completeAction(R.string.item_moved);
}
}
onDrop的代码很长,上述代码只是把主要的功能代码贴上去了。首先计算mTargetCell也就要drop的位置,然后判断是不是在另一个图标上面,如果是则创建一个文件夹;第二就是直接放入一个已经存在的文件夹里,但是Folder也是实现了onDrop的,所以这部份代码是不会执行的。第三就是拖拽对象是不是在另一页,如果是则调用addInScreen()方法加入到该页面,如果没有在其它页就更新位置,最后再更新数据库;第四如果找不到位置就放置到原位。onDrop执行完后会执行onDragEnd()函数,请允许我把最后一步的代码贴下:
packages\apps\Launcher3\src\com\android\launcher3\Workspace.java
@Override
public void onDragEnd() {
if (ENFORCE_DRAG_EVENT_ORDER) {
enfoceDragParity("onDragEnd", 0, 0);
}
if (!mDeferRemoveExtraEmptyScreen) {
removeExtraEmptyScreen(true, mDragSourceInternal != null);
}
updateChildrenLayersEnabled(false);
mLauncher.unlockScreenOrientation(false);
// Re-enable any Un/InstallShortcutReceiver and now process any queued items
InstallShortcutReceiver.disableAndFlushInstallQueue(getContext());
mDragSourceInternal = null;
mLauncher.onInteractionEnd();
}
代码比较好理解,删除掉空页面,取消锁定方向,允许安装应用程序。至此拖拽的流程就分析完了,由于知识水平有限,本文难免有写的不对的地方,还望大家指正。