不知你是否还记得桌面布局分析一文中的launcher.xml布局文件中根布局下嵌套的一个看似没啥用的DragLayer布局,它既不像Workspace那样作为PagedView容器占据大半江山,也不似HotSeat那样别有洞天。如果仅以布局的层面看,它更像是冗余存在。但它作为包裹布局,却实在有深层次的意义,今天我们就一起来揭下其神秘面纱。
我们知道,在安卓桌面长按某个应用图标时可以拖拽进行重新排序。那么整个拖拽过程是怎么发生的呢,它和我们上文说的DragLayer又有什么关系呢。显然,我们应该先从应用图标的长按监听开始看。
在Launcher.java也就是启动类中的setUpViews方法中我们可以看到workspace设置了长按事件监听,并且回调Listener就是Launcher本身this。
private void setupViews() {
final DragController dragController = mDragController;
//最外层FrameLayout
mLauncherView = findViewById(R.id.launcher);
//用于拖拽的拖拽布局
mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
//存放CellLayout,是一个PageView
mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
//存在于Land屏幕中
mQsbDivider = findViewById(R.id.qsb_divider);
//WorkSpace与HotSeat之间的分界线,当进入All App模式时,会隐藏
mDockDivider = findViewById(R.id.dock_divider);
//设置launcher全屏显示
/**
* 1.View.SYSTEM_UI_FLAG_VISIBLE :状态栏和Activity共存,Activity不全屏显示。也就是应用平常的显示画面
* 2.View.SYSTEM_UI_FLAG_FULLSCREEN :Activity全屏显示,且状态栏被覆盖掉
* 3.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN :Activity全屏显示,但是状态栏不会被覆盖掉,而是正常显示,只是Activity顶端布 局会被覆盖住
* 4.View.INVISIBLE : Activity全屏显示,隐藏状态栏
*/
mLauncherView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
mWorkspaceBackgroundDrawable = getResources().getDrawable(R.drawable.workspace_bg);
// Setup the drag layer
mDragLayer.setup(this, dragController);
// 初始化底部热区控件
mHotseat = (Hotseat) findViewById(R.id.hotseat);
if (mHotseat != null) {
//传递launcher引用
mHotseat.setup(this);
}
// Setup the workspace
mWorkspace.setHapticFeedbackEnabled(false);
//处于长按事件,包括壁纸选择,长按拖拽等
mWorkspace.setOnLongClickListener(this);
mWorkspace.setup(dragController);
//让WorkSpace成为拖拽开始事件与结束事件的CallBack
dragController.addDragListener(mWorkspace);
// Get the search/delete bar
mSearchDropTargetBar = (SearchDropTargetBar) mDragLayer.findViewById(R.id.qsb_bar);
// Setup AppsCustomize
mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane);
mAppsCustomizeContent = (AppsCustomizePagedView)
mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content);
mAppsCustomizeContent.setup(this, dragController);
// Setup the drag controller (drop targets have to be added in reverse order in priority)
dragController.setDragScoller(mWorkspace);
dragController.setScrollView(mDragLayer);
dragController.setMoveTarget(mWorkspace);
dragController.addDropTarget(mWorkspace);
if (mSearchDropTargetBar != null) {
mSearchDropTargetBar.setup(this, dragController);
}
}
我们可以猜一猜,workspace的setOnLongClickListener最后一定会把listener传递给BubbleTextView即应用图标。接着我们点进去。代码如下:
@Override
public void setOnLongClickListener(OnLongClickListener l) {
mLongClickListener = l;
final int count = getPageCount();
for (int i = 0; i < count; i++) {
getPageAt(i).setOnLongClickListener(l);
}
}
不出所料,workspace继续把listener继续传给子控件page(其实就是CellLayout),那么接下来继续往CellLayout追溯...
等等!!!
为啥在CellLayout没有重写setOnLongClickListener相关的表述呢?
原来关于OnLongClickListener的添加没有由CellLayout自己来控制(其实我认为由自己控制更好),BubbleTextView的添加始于bindItems等launcher的绑定控件回调,我们进入到bindXXX()中可以看见,无论是绑定时创建的BubbleTextView、AppWidgetHostView还是FoldIcon,最终都通过workspace.addInScreen添加到布局中。以下是addInScreen方法:
void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY,
boolean insert) {
if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
if (screen < 0 || screen >= getChildCount()) {
Log.e(TAG, "The screen must be >= 0 and < " + getChildCount()
+ " (was " + screen + "); skipping child");
return;
}
}
final CellLayout layout;
if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
layout = mLauncher.getHotseat().getLayout();
child.setOnKeyListener(null);
if (child instanceof FolderIcon) {
((FolderIcon) child).setTextVisible(false);
}
if (screen < 0) {
screen = mLauncher.getHotseat().getOrderInHotseat(x, y);
} else {
// Note: We do this to ensure that the hotseat is always laid out in the orientation
// of the hotseat in order regardless of which orientation they were added
x = mLauncher.getHotseat().getCellXFromOrder(screen);
y = mLauncher.getHotseat().getCellYFromOrder(screen);
}
} else {
// Show folder title if not in the hotseat
if (child instanceof FolderIcon) {
((FolderIcon) child).setTextVisible(true);
}
layout = (CellLayout) getChildAt(screen);
child.setOnKeyListener(new IconKeyEventListener());
}
LayoutParams genericLp = child.getLayoutParams();
CellLayout.LayoutParams lp;
if (genericLp == null || !(genericLp instanceof CellLayout.LayoutParams)) {
lp = new CellLayout.LayoutParams(x, y, spanX, spanY);
} else {
lp = (CellLayout.LayoutParams) genericLp;
lp.cellX = x;
lp.cellY = y;
lp.cellHSpan = spanX;
lp.cellVSpan = spanY;
}
if (spanX < 0 && spanY < 0) {
lp.isLockedToGrid = false;
}
// Get the canonical child id to uniquely represent this view in this screen
int childId = LauncherModel.getCellLayoutChildId(container, screen, x, y, spanX, spanY);
boolean markCellsAsOccupied = !(child instanceof Folder);
if (!layout.addViewToCellLayout(child, insert ? 0 : -1, childId, lp, markCellsAsOccupied)) {
// TODO: This branch occurs when the workspace is adding views
// outside of the defined grid
// maybe we should be deleting these items from the LauncherModel?
Log.w(TAG, "Failed to add to item at (" + lp.cellX + "," + lp.cellY + ") to CellLayout");
}
if (!(child instanceof Folder)) {
child.setHapticFeedbackEnabled(false);
child.setOnLongClickListener(mLongClickListener);
}
if (child instanceof DropTarget) {
mDragController.addDropTarget((DropTarget) child);
}
}
可以看到,在64行执行了setOnLongClickListener方法,而这个child则正是BubbleTextView、AppWidgetHostView、FolderIcon之一。也就是说CellLayout中的子控件cell都设置了长按点击事件的listener为mLongClickListener,而mLongClickListener正是在workspace的setOnLongClickListener(上一个代码段中可见)保存的。如此,所有的长按事件都应该在Launcher.java的onLongClick中回调。即如下代码:
public boolean onLongClick(View v) {
if (!isDraggingEnabled()) return false;//能否拖拽,内部判断startLoader操作是否完成
if (isWorkspaceLocked()) return false;
if (mState != State.WORKSPACE) return false;//是否处于workSpace中
//长按分为两种:1.长按ItemInfo,2.长按空白处
if (!(v instanceof CellLayout)) {//主要目的是为了获取CellLayout
v = (View) v.getParent().getParent();
}//判断当前长按控件是否是CellLayout,如果当前的v是shortcut平级的view,那么getParent就是ShortcutAndWidgetContainer,再次getParent即CellLayout
resetAddInfo();//pendingAddInfo重置
CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag();
// This happens when long clicking an item with the dpad/trackball
if (longClickCellInfo == null) {
return true;
}
//cell信息是在CellLayout的onInterceptTouchEvent中控制的,
// 当WorkSpace不处于小屏模式时会在onInterceptTouchEvent中记录touchX,Y值以及点击的BubbleTextView
final View itemUnderLongClick = longClickCellInfo.cell;
//是否允许长按事件
boolean allowLongPress = isHotseatLayout(v) || mWorkspace.allowLongPress();
//当前处于允许长按状态,且没有控件正在被拖拽
if (allowLongPress && !mDragController.isDragging()) {
//如果CellLayout没有BubbleTextView被点击,则说明是长按进入壁纸选择事件
if (itemUnderLongClick == null) {
//长按震动反馈
mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
startWallpaper();//如果长按区域中没有ItemInfo,那么就处理为长按设置壁纸
} else {
//Folder打开后的的长按拖拽事件不在这里处理
if (!(itemUnderLongClick instanceof Folder)) {
//在startDrag中让CellLayout中被长按的控件隐藏,并且调用DragController开始在DragLayer中处理拖拽事件
mWorkspace.startDrag(longClickCellInfo);
}
}
}
return true;
}
在上面代码段中有个Tag判断至关重要,它就是从何而来呢?
熟悉事件分发机制的朋友们应该知道View的事件分发是向内传递事件,向外消费事件。父控件ViewGroup先判断是否打断(Intercept)事件即onInterceptTouchEvent方法,然后在根据Intercept的返回值决定是否继续往下dispatch,如果onInterceptTouchEvent返回值为true,那么触摸事件就由控件自身的onTouchEvent处理,如果为false,则调用dispatchTouchEvent方法继续往内部子控件分发。如果子控件接收消费该事件,那么则进行listener消费或者onTouch消费。我们进入到CellLayout的onInterceptTouchEvent中:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// First we clear the tag to ensure that on every touch down we start with a fresh slate,
// even in the case where we return early. Not clearing here was causing bugs whereby on
// long-press we'd end up picking up an item from a previous drag operation.
final int action = ev.getAction();
//清除cellInfo中的信息-reset
if (action == MotionEvent.ACTION_DOWN) {
clearTagCellInfo();
}
//当listener的onTouch返回true表示小屏模式或者还没完成切换,
// 小屏模式指代在all apps页面长按应用icon后进入Workspace处于的模式,state==mall or state==spring_load
//也就是说小模式下CellLayout内部的BubbleTextView不会响应事件
if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) {
return true;
}
//在down事件时设置cellInfo标识当前touch的位置已经touch位置上的cell
if (action == MotionEvent.ACTION_DOWN) {
setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY());
}
//默认不拦截事件
return false;
}
首先onInterceptTouchEvent一定是先于子控件的onLongClick消费事件执行的,那么我们据以上可以知道,在onInterceptTouchEvent判断时,CellLayout就记录了当前的downX,Y值,不信可以看setTagToCellInfoForPoint方法:
public void setTagToCellInfoForPoint(int touchX, int touchY) {
final CellInfo cellInfo = mCellInfo;
Rect frame = mRect;
final int x = touchX + getScrollX();
final int y = touchY + getScrollY();
final int count = mShortcutsAndWidgets.getChildCount();
boolean found = false;
for (int i = count - 1; i >= 0; i--) {
final View child = mShortcutsAndWidgets.getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if ((child.getVisibility() == VISIBLE || child.getAnimation() != null) &&
lp.isLockedToGrid) {
child.getHitRect(frame);
float scale = child.getScaleX();
frame = new Rect(child.getLeft(), child.getTop(), child.getRight(),
child.getBottom());
//在new出来时,frame的宽高是为child的宽高-相对应parent控件ShortcutWidgetContainer的距离,
//距离cellLayout的距离加上ShortcutWidgetContainer与CellLayout的距离,也就是padding
//总结:此处计算->child位置=child与父控件(ShortcutWidgetContainer距离)+父控件到CellLayout距离
frame.offset(getPaddingLeft(), getPaddingTop());
//此处当child有scale时需要对frame进行缩放。
frame.inset((int) (frame.width() * (1f - scale) / 2),
(int) (frame.height() * (1f - scale) / 2));
//碰撞检测,如果点击的xy在child上,那么就使用CellInfo记录点击信息
if (frame.contains(x, y)) {
cellInfo.cell = child;
cellInfo.cellX = lp.cellX;
cellInfo.cellY = lp.cellY;
cellInfo.spanX = lp.cellHSpan;
cellInfo.spanY = lp.cellVSpan;
found = true;
break;
}
}
}
mLastDownOnOccupiedCell = found;
//
if (!found) {
final int cellXY[] = mTmpXY;
pointToCellExact(x, y, cellXY);
cellInfo.cell = null;
cellInfo.cellX = cellXY[0];
cellInfo.cellY = cellXY[1];
cellInfo.spanX = 1;
cellInfo.spanY = 1;
}
//使用tag记录CellInfo即当前的touch信息
setTag(cellInfo);
}
由此可知,CellLayout的Tag记录了当前down事件的位置信息以及child(点击位置的子控件)信息。啰嗦一句,上面代码ShortcutWidgetContainer控件是CellLayout的下级容器控件,我们添加到CellLayout的应用图标,其实都是添加到CellLayout子容器ShortcutWidgetContainer中了。
此时再回到onLongClick中就可以理解为什么itemUnderLongClick下的cell为空时会执行壁纸选择了,因为在onInterceptTouchEvent中没有在点击的downX,Y处找到child控件。当然,本文是关于拖拽分析的,自然更关心cell非空的else语句。在其中调用了Workspace的startDrag方法。接下来我们进入到startDrag中:
void startDrag(CellLayout.CellInfo cellInfo) {
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);
child.clearFocus();
child.setPressed(false);
final Canvas canvas = new Canvas();
//在此处画出outline,即child的轮廓
mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING);
beginDragShared(child, this);
}
以上代码步骤可以分解为:
1.隐藏掉我们点击的应用图标,并清除其focus和press效果。为什么要隐藏控件,这里先留坑,下面会解答。
2.画出当前被点击child的outline即轮廓,此轮廓当dragOver某个Cell时,会在这个Cell上显示child边廓。
3.调用beginDragShared方法,其内部矫正了downX,Y后把拖拽交由我们拖拽主角DragController来实现真正拖拽效果。
beginDragShared方法如下,主要是:
public void beginDragShared(View child, DragSource source) {
Resources r = getResources();
//此方法内部主要通过((TextView) v).getCompoundDrawables()[1]获取bitmap
final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING);
final int bmpWidth = b.getWidth();
final int bmpHeight = b.getHeight();
//scale时child相对于DragLayer的缩放比,mTempXY则记录了child相对于DragLayer的的x,y坐标
float scale = mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY);
//如果scale比例为1,那么dragLayerX即是mTempXY[0]
int dragLayerX =
Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2);
//如果scale比例为1,那么dragLayerY即是mTempXY[1]
int dragLayerY =
Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2
- DRAG_BITMAP_PADDING / 2);
Point dragVisualizeOffset = null;
Rect dragRect = null;
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;//如果child还有paddingTop,那么还需要矫正paddingTop值
// 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(-DRAG_BITMAP_PADDING / 2,
iconPaddingTop - DRAG_BITMAP_PADDING / 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);
}
//此处再次清除焦点和press等,其实在其上已经清除过
if (child instanceof BubbleTextView) {
BubbleTextView icon = (BubbleTextView) child;
icon.clearPressedOrFocusedBackground();
}
//把拖拽控制权交给mDragController即DragController类
mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
//用完回收,好习惯
b.recycle();
// 不显示指示器
showScrollingIndicator(false);
}
上面方法详解:
1.获取child(也就是CellLayout中被点击的应用图标)的bitmap。此bitmap在之后伪造child时会用到。
2.child相对于DragLayer的位置,DragLayer即文章开头我们提到的看似没用的控件,从此处开始它将登上舞台。位置信息通过getLocationInDragLayer存放在mTempXY中,scale代表child相对于DragLayer的缩放比例,为啥会用到scale呢,因为getLeft、getWidth等方法得到的值时未缩放平移的值(可以参考tween动画原理,这里不赘述)。我们也可以进入getLocationInDragLayer,其内部其实最终调用到此处:
public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) {
float scale = 1.0f;
float[] pt = {coord[0], coord[1]};
descendant.getMatrix().mapPoints(pt);
scale *= descendant.getScaleX();
pt[0] += descendant.getLeft();
pt[1] += descendant.getTop();
ViewParent viewParent = descendant.getParent();
while (viewParent instanceof View && viewParent != this) {
final View view = (View)viewParent;
//mapPoints方法,getLeft等是不会计算scale、translate、skew等后的view实际位置的,
// 而,使用matrix的mapPoints可以矫正view的实际位置
view.getMatrix().mapPoints(pt);
scale *= view.getScaleX();
pt[0] += view.getLeft() - view.getScrollX();
pt[1] += view.getTop() - view.getScrollY();
viewParent = view.getParent();
}
coord[0] = (int) Math.round(pt[0]);
coord[1] = (int) Math.round(pt[1]);
return scale;
}
3.因为拖拽主要需要确定child位置,但实际主要是child中图片位置,所以dragLayerY会加上child的paddingTop值。
4.拖拽控制权转交给DragController执行。那么DragController是啥呢,它其实是外层布局DragLayer(这家伙又出来了)的TouchEvent事件接管者。不信?我们接着贴代码,以下代码都是DragLayer中的方法。
onInterceptTouchEvent方法
/**
* 如果当前down事件处于打开的folder中,那么应该让folder处理此事件
* 否则则由DragController接手事件处理
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (handleTouchDown(ev, true)) {
return true;
}
}
clearAllResizeFrames();
return mDragController.onInterceptTouchEvent(ev);
}
onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean handled = false;
int action = ev.getAction();
int x = (int) ev.getX();
int y = (int) ev.getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
if (handleTouchDown(ev, false)) {
return true;
}
}
}
if (mCurrentResizeFrame != null) {
handled = true;
switch (action) {
case MotionEvent.ACTION_MOVE:
mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
mCurrentResizeFrame.onTouchUp();
mCurrentResizeFrame = null;
}
}
if (handled) return true;
return mDragController.onTouchEvent(ev);
}
如果你对这两个方法中的handleTouchDown有疑惑,其实它内部是处理Folder的Touch处理的。handleTouchDown方法如下:
/**
* 处理down事件,重点是down事件发生时是否有folder正处于打开状态
*
* @param ev
* @param intercept
* @return
*/
private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
Rect hitRect = new Rect();
int x = (int) ev.getX();
int y = (int) ev.getY();
for (AppWidgetResizeFrame child: mResizeFrames) {
child.getHitRect(hitRect);
if (hitRect.contains(x, y)) {
if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) {
mCurrentResizeFrame = child;
mXDown = x;
mYDown = y;
//阻止父级控件拦截touch事件
requestDisallowInterceptTouchEvent(true);
return true;
}
}
}
/**
*
*如果当前down事件是在打开的folder中,那么先看down的位置是否是在可编辑的EditView中,
* 如果不是,那么让EditView回到unfocus状态,并且隐藏掉软键盘
*/
Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) {
if (currentFolder.isEditingName()) {
if (!isEventOverFolderTextRegion(currentFolder, ev)) {
currentFolder.dismissEditingName();
return true;
}
}
/**
* 如果down没有发生在正在打开的folder中,那么久隐藏此holder,执行close操作
*/
getDescendantRectRelativeToSelf(currentFolder, hitRect);
if (!isEventOverFolder(currentFolder, ev)) {
mLauncher.closeFolder();
return true;
}
}
return false;
}
其实也就是说,如果当前WorkSpace中没有被打开的Folder需要处理,那么DragLayer的事件就由DragController全权接手处理。
OK!让我们回到主题,之前说到beginDragShared方法最后会调用DragController的startDrag方法来让DragController接手长按事件的处理。那么我们有必要分析DragController类的方法结构。
public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,
DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
float initialDragViewScale) {
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);
}
//mMotionDownX,mMotionDownY皆是由onInterceptTouchEvent获取
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被DragController接收,最终DragLayer会拦截掉Touch事件
//让DragLayer自己处理onTouchEvent,然后会传到DragController自身的onTouchEvent身上来
mDragging = true;
mDragObject = new DropTarget.DragObject();
mDragObject.dragComplete = false;
mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
mDragObject.dragSource = source;//即workspace
mDragObject.dragInfo = dragInfo;//bubbleTextView的Tag,即ShortcutInfo
mVibrator.vibrate(VIBRATE_DURATION);
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));
}
dragView.show(mMotionDownX, mMotionDownY);
handleMoveEvent(mMotionDownX, mMotionDownY);
}
此处做了如下操作:
1.隐藏软键盘。
2.通知监听者listener拖拽开始即onDragStart。
3.使mDragging等于true。这一步很关键,正是因为mDragging的值决定了DragLayer能否拦截事件自己处理。
4.拖拽震动反馈。
5.使用成员变量mDragObject保存拖拽信息。包括拖拽位置、拖拽源、拖拽itemInfo(通过child.getTag()获取),并且重新创建了一个DragView,此DragView主要用于显示之前我们通过child的createDragBitmap创造的bitmap并是实际的被拖动者。还记得上文提到过的应用图标已经被隐藏了吗?其实,真正被拖动的控件式被伪造出来的DragView(可以理解为一个ImageView,当然他是直接继承的View控件,只不过在onDraw方法中绘制了bitmap)。
6.调用dragView的show方法,其内部一方面添加当前DragView到DragLayer(再次说明,这货很有用),另一方面执行了dragView的缩放动画(这里主要是对DragView放大显示,这也是为啥我们长按应用图标后图标变大的地方)。
7.调用handleMoveEvent方法。其内部调用DragView的move(X,Y)方法,达到真正的移动效果。
此处再跑下题,为什么说mDragging这个值在startDrag中被置为true很重要呢?
我们知道DragController的startDrag方法被调用是追根溯源是源自应用图标的onLongClick方法。此时可以确定的是DragLayer还没有开始拦截事件不让往下传递。但是!!!当DragController的startDrag方法把mDragging置为true时呢。我们进入到DragController的onInterceptTouchEvent中(其实本该进入到DragLayer的onInterceptTouchEvent中去看,但是其事件已经委托给DragController了,所以直接来DragController看):
public boolean onInterceptTouchEvent(MotionEvent ev) {
@SuppressWarnings("all") // suppress dead code warning
final boolean debug = false;
if (debug) {
Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging="
+ mDragging);
}
// Update the velocity tracker
acquireVelocityTrackerAndAddMovement(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:
mLastTouchUpTime = System.currentTimeMillis();
if (mDragging) {
PointF vec = isFlingingToDelete(mDragObject.dragSource);
if (vec != null) {
dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
} else {
drop(dragLayerX, dragLayerY);
}
}
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
cancelDrag();
break;
}
return mDragging;
}
可以看到Drag Layer是否拦截Touch事件正式由mDragging决定的。所以在startDrag中把mDragging置为true,也就可以理解为当用户长按应用图标时DragLayer就拦截事件自己处理了。
回到正题。我们从DragControlller的startDrag及onTouchEvent中可以知道。拖拽的过程其实就是在不断地调用handleMoveEvent()方法。那么我们有必要进入到此方法中去看一看。以下是此方法的代码段:
private void handleMoveEvent(int x, int y) {
mDragObject.dragView.move(x, y);
// Drop on someone?
final int[] coordinates = mCoordinatesTemp;
//此处的coordinates要经过修正,得出的相对于workspace的x,y值
DropTarget dropTarget = findDropTarget(x, y, coordinates);
mDragObject.x = coordinates[0];
mDragObject.y = coordinates[1];
//通知workspace当前dragView的xy值(此x,y值已经不是相对于DragLayer,而是经过修正,相对于WorkSpace)等信息,
// 方便Workspace在onDragOver回调处理像显示轮廓等信息
checkTouchMove(dropTarget);
// Check if we are hovering over the scroll areas
//Math.pow是进行次方运算,Math.sqrt进行开方运算
mDistanceSinceScroll +=
Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2));
mLastTouch[0] = x;
mLastTouch[1] = y;
//检查当前的x,y值是否进入WorkSpace的页面scroll边界,边界值为20dp,
//也就是说当前dragView进入到距离WorkSpace的左右边界20dp范围内的话就触发scroll页面
checkScrollState(x, y);
}
此方法在矫正完x,y的值后,检查当前的拖拽状态,及滑动状态(拖拽滑屏)。
我们先进入checkTouchMove中。
private void checkTouchMove(DropTarget dropTarget) {
if (dropTarget != null) {
DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject);
if (delegate != null) {
dropTarget = delegate;
}
//mLastDropTarget在down事件触发时会被置null,所以当每次重新长按时一定会走onDragEnter
if (mLastDropTarget != dropTarget) {
if (mLastDropTarget != null) {
mLastDropTarget.onDragExit(mDragObject);
}
//首次时调用onDragEnter
dropTarget.onDragEnter(mDragObject);
}
//以后每次移动时都会调用onDragOver
dropTarget.onDragOver(mDragObject);
} else {
if (mLastDropTarget != null) {
mLastDropTarget.onDragExit(mDragObject);
}
}
mLastDropTarget = dropTarget;
}
其实不难理解就是在其中调用了DropTarget即Workspace的onDragEnter及onDragOver方法。onDragEnter内部主要是做了一些基本的清理工作,没啥可说的。主要是onDragOver方法,其内部主要处理了拖动的逻辑,下面一起来看一看onDragOver方法:
public void onDragOver(DragObject d) {
//
if (mInScrollArea || mIsSwitchingState || mState == State.SMALL) return;
Rect r = new Rect();
CellLayout layout = null;
ItemInfo item = (ItemInfo) d.dragInfo;
//spanX,Y表示当前子控件占据的横向Cell个数和纵向Cell个数
if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found");
mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset,
d.dragView, mDragViewVisualCenter);
final View child = (mDragInfo == null) ? null : mDragInfo.cell;
// Identify whether we have dragged over a side page
if (isSmall()) {//当从all apps长按拖动icon到workspace时调用时会进入
if (mLauncher.getHotseat() != null && !isExternalDragWidget(d)) {
mLauncher.getHotseat().getHitRect(r);
if (r.contains(d.x, d.y)) {
layout = mLauncher.getHotseat().getLayout();
}
}
if (layout == null) {
layout = findMatchingPageForDragOver(d.dragView, d.x, d.y, false);
}
if (layout != mDragTargetLayout) {
setCurrentDropLayout(layout);
setCurrentDragOverlappingLayout(layout);
boolean isInSpringLoadedMode = (mState == State.SPRING_LOADED);
//如果处于小屏模式(从all apps页面长按进入的wrokspace,这种状态叫SPRING_MODE),那么
if (isInSpringLoadedMode) {
if (mLauncher.isHotseatLayout(layout)) {
mSpringLoadedDragController.cancel();
} else {
//此处其实内部进行了snapToPage及页面切换到mDragTargetLayout页面
mSpringLoadedDragController.setAlarm(mDragTargetLayout);
}
}
}
} else {
//此处当处于WorkSpace内部拖动时
// Test to see if we are over the hotseat otherwise just use the current page
if (mLauncher.getHotseat() != null && !isDragWidget(d)) {
mLauncher.getHotseat().getHitRect(r);
if (r.contains(d.x, d.y)) {//首先判断当前的touchX,Y是否属于HotSeat
layout = mLauncher.getHotseat().getLayout();
}
}
if (layout == null) {
//如果layout为空这说明touch位置不在HotSeat中
layout = getCurrentDropLayout();
}
Log.i(TAG, "onDragOver: "+(layout != mDragTargetLayout));
//不相等则通知以前并清除状态,此处存疑,判断都是false,待分析
if (layout != mDragTargetLayout) {
//Log.i(TAG, "onDragOver: layout != mDragTargetLayout");
setCurrentDropLayout(layout);
setCurrentDragOverlappingLayout(layout);
}
}
// Handle the drag over
if (mDragTargetLayout != null) {
//经过下面if else 计算mDragViewVisualCenter即为在mDragTargetLayout中的TouchX,Y
if (mLauncher.isHotseatLayout(mDragTargetLayout)) {
//mDragViewVisualCenter坐标相对于矫正,内部使用invert逆向+mapPoints
mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
} else {
mapPointFromSelfToChild(mDragTargetLayout, mDragViewVisualCenter, null);
}
ItemInfo info = (ItemInfo) d.dragInfo;
/**
* 计算出当前在mDragViewVisualCenter最近的Cell位置
*/
mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1], item.spanX, item.spanY,
mDragTargetLayout, mTargetCell);
//清理addToFolder、createFolder、reOrder
setCurrentDropOverCell(mTargetCell[0], mTargetCell[1]);
//此处计算出mDragViewVisualCenter距离最近的Cell也就是mTargetCell的距离
float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
//获取在mTargetCell位置上的应用图标控件,可能为空
final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0],
mTargetCell[1]);
//mDragViewVisualCenter与mTargetCell的distance是否小于特定值
// 且mDragView所代表的控件是application或者shortcut,
// 那么就通过Alarm机制,创建FolderRing,进入到DRAG_MODE_CREATE_FOLDER模式。
manageFolderFeedback(info, mDragTargetLayout, mTargetCell,
targetCellDistance, dragOverView);
int minSpanX = item.spanX;
int minSpanY = item.spanY;
if (item.minSpanX > 0 && item.minSpanY > 0) {
minSpanX = item.minSpanX;
minSpanY = item.minSpanY;
}
//判断距离mDragViewVisualCenter最近的mTargetCell是否有应用图标在。
boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
item.spanY, child, mTargetCell);
//如果没有应用图标在
if (!nearestDropOccupied) {
//在onDragOver中不断被调用,下面就是显示轮廓的具体方法
mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
(int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false,
d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion());
}
//如果当前有应用图标在,而且之前的manageFolderFeedback没成立,那么就需要让mTargetCell挪位置
else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
&& !mReorderAlarm.alarmPending() && (mLastReorderX != mTargetCell[0] ||
mLastReorderY != mTargetCell[1])) {
// Otherwise, if we aren't adding to or creating a folder and there's no pending
// reorder, then we schedule a reorder
ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child);
mReorderAlarm.setOnAlarmListener(listener);
mReorderAlarm.setAlarm(REORDER_TIMEOUT);
}
//此处按道理不会走,因为如果是创建folder或者加入folder的地方,是不可能没有被占用的
if (mDragMode == DRAG_MODE_CREATE_FOLDER || mDragMode == DRAG_MODE_ADD_TO_FOLDER ||
!nearestDropOccupied) {
if (mDragTargetLayout != null) {
mDragTargetLayout.revertTempState();
}
}
}
}
方法解释:
1.获取DragView的中心点mDragViewVisualCenter。
2.针对SPRING_LOADED模式和NORMAL模式分别进行了一些状态回置。
3.对DragView的中心点相对于mDragTargetLayout做一些矫正。
4.计算离DragView中心最近的Cell及它两的距离,此Cell上的应用图标控件(可能在最近Cell出没有图标)。
5.manageFolderFeedback方法,主要用于创建Folder(显示创建Folder时的圆环ring)。原理是计算DragView与最近Cell的距离是否小于特定值,且DragView所代表的控件是application或者shortcut,那么久显示出创建Folder时的圆环。
6.判断最近的mTargetCell是否已经被应用图标占用,如果未被占用,则在此位置显示拖拽图标的轮廓(此轮廓在Workspace的startDrag中被创建出来)。如果已经被占用,那么就使用Alarm进行图标重新排序。这里介绍下Alarm,他是个定时器,当时间到时就会执行Listener中的onAlarm方法。
接下来我们需要进入到应用重新排序的这个AlarmListener中去看看,他是怎么重排的序。
public void onAlarm(Alarm alarm) {
int[] resultSpan = new int[2];
mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1], spanX, spanY, mDragTargetLayout, mTargetCell);
mLastReorderX = mTargetCell[0];
mLastReorderY = mTargetCell[1];
//此处就是做动画,让出位置
mTargetCell = mDragTargetLayout.createArea((int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
child, mTargetCell, resultSpan, CellLayout.MODE_DRAG_OVER);
if (mTargetCell[0] < 0 || mTargetCell[1] < 0) {
mDragTargetLayout.revertTempState();
} else {
setDragMode(DRAG_MODE_REORDER);
}
boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
//
mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
(int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize,
dragView.getDragVisualizeOffset(), dragView.getDragRegion());
}
关于拖拽轮廓显示的方法mDragTargetLayout.visualizeDropLocation()解析可参见我的文章拖拽轮廓显示。关于拖拽过程中的重新排序过程mDragTargetLayout.createArea()的解析可参见我的文章拖拽过程中的排序。这里就不铺开讲了。
在讲述玩checkTouchState后,我们在来看DragController的handleMoveEvent方法中的checkScrollState,即检查是否需要滑动页面。
private void checkScrollState(int x, int y) {
final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();
final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY;
final DragLayer dragLayer = mLauncher.getDragLayer();
final boolean isRtl = (dragLayer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
final int forwardDirection = isRtl ? SCROLL_RIGHT : SCROLL_LEFT;
final int backwardsDirection = isRtl ? SCROLL_LEFT : SCROLL_RIGHT;
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);//此处会一直检查scroll状态
}
}
} 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就是判断当前DragView的x值是否小于mScrollZone(系统默认是20dp范围) ,或者x值是否大于WorkSpace的getWidth()-mScrollZone。通俗的讲,就是判断就是DragView距离左边或右边的距离值是否小于mScrollZone(20dp),如果小于此值,说明当前需要进入滑动页面操作。检查可分为如下几步:
1.具体来看,滑动页面需要判断mDragScroller即WorkSpace的onEnterScrollArea方法是否成立。
@Override
public boolean onEnterScrollArea(int x, int y, int direction) {
// Ignore the scroll area if we are dragging over the hot seat
boolean isPortrait = !LauncherApplication.isScreenLandscape(getContext());
//如果边界检查发现x值边界位于HotSeat内,那么就不进行页面切换
if (mLauncher.getHotseat() != null && isPortrait) {
Rect r = new Rect();
mLauncher.getHotseat().getHitRect(r);
if (r.contains(x, y)) {
return false;
}
}
boolean result = false;
//非小屏模式
if (!isSmall() && !mIsSwitchingState) {
mInScrollArea = true;
//页面滑动方向判断。
final int page = getNextPage() +
(direction == DragController.SCROLL_LEFT ? -1 : 1);
// 清理状态
setCurrentDropLayout(null);
//判断page是否符合workspace数量要求
if (0 <= page && page < getChildCount()) {
CellLayout layout = (CellLayout) getChildAt(page);
//对page进行重绘。
setCurrentDragOverlappingLayout(layout);
// Workspace is responsible for drawing the edge glow on adjacent pages,
// so we need to redraw the workspace when this may have changed.
invalidate();
result = true;
}
}
//如果小屏模式则返回false
return result;
}
2.成立的话就调用DragLayer的onEnterScrollArea进行DragLayer重绘,以下DragLayer是具体调用。
void onEnterScrollArea(int direction) {
mInScrollArea = true;
invalidate();
}
3.执行mScrollRunnable进行具体的页面切换。其内部调用WokSpace的scrollLeft、scrollRight执行滑动切换页面,此Runnable具体如下:
private class ScrollRunnable implements Runnable {
private int mDirection;
ScrollRunnable() {
}
public void run() {
if (mDragScroller != null) {
//根据滑动方向具体执行滑动流程
if (mDirection == SCROLL_LEFT) {
mDragScroller.scrollLeft();
} else {
mDragScroller.scrollRight();
}
mScrollState = SCROLL_OUTSIDE_ZONE;
mDistanceSinceScroll = 0;
//重绘Workspace和DragLayer
mDragScroller.onExitScrollArea();
mLauncher.getDragLayer().onExitScrollArea();
//如果当前还处于拖拽过程中,那么持续进行滑动检查
if (isDragging()) {
// Check the scroll again so that we can requeue the scroller if necessary
checkScrollState(mLastTouch[0], mLastTouch[1]);
}
}
}
void setDirection(int direction) {
mDirection = direction;
}
}
以下是WorkSpace的页面切换方法。可以看到,内部判断如果不是小屏模式就执行父类PagedView的scrollLeft或scrollRight(父类方法内部调用snapToPage,这里不再展开表述),同时如果当前页面有处于open状态的文件夹,则执行文件夹的关闭操作。
@Override
public void scrollLeft() {
//判断是否是小屏模式,如果不是最终会调用snapToPage方法
if (!isSmall() && !mIsSwitchingState) {
super.scrollLeft();
}
//如果当前页面有打开的Folder,则需要关闭folder
Folder openFolder = getOpenFolder();
if (openFolder != null) {
openFolder.completeDragExit();
}
}
@Override
public void scrollRight() {
if (!isSmall() && !mIsSwitchingState) {
super.scrollRight();
}
Folder openFolder = getOpenFolder();
if (openFolder != null) {
openFolder.completeDragExit();
}
}
至此handleTouchMove方法即DragView的Move事件我们已经了解完了,下面再来看看up事件,显然TouchUp中我们应该根据DragView的拖拽位置来确定应用的重新排序。以下是DragController的onTouchEvent方法:
public boolean onTouchEvent(MotionEvent ev) {
if (!mDragging) {
return false;
}
// Update the velocity tracker
acquireVelocityTrackerAndAddMovement(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;
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(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) {
//是否是快速拖拽删除如果有返回值,则是快速拖拽删除,如果不是则进入drop
PointF vec = isFlingingToDelete(mDragObject.dragSource);
if (vec != null) {
dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
} else {
drop(dragLayerX, dragLayerY);
}
}
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
mHandler.removeCallbacks(mScrollRunnable);
cancelDrag();
break;
}
return true;
}
可以看见在up事件中:
1.DragView移动到upX,upY坐标。
2.且移除了WorkSpace的页面滑动监听。
3.并且根据mDragging状态(此时一般为true),及mVelocityTracker所获取的拖拽速度决定当前是删除拖动应用图标(dropOnFlingToDeleteTarget()方法),还是按照DragView的拖拽位置重新对应用图标进行排序(onDrop()方法)。
在onDrop方法中先进行判断当前DragView所处在的DragTarget能否accept当前的拖拽排序。如果能接手,则进而执行WorkSpace的onDrop方法。以下即是DragController的drop方法:
private void drop(float x, float y) {
final int[] coordinates = mCoordinatesTemp;
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);
}
那么WorkSpace有是如何判断能否执行onDrop排序呢,我们走进acceptDrop方法瞧瞧:
public boolean acceptDrop(DragObject d) {
// If it's an external drop (e.g. from All Apps), check if it should be accepted
CellLayout dropTargetLayout = mDropToLayout;
if (d.dragSource != this) {
// Don't accept the drop if we're not over a screen at time of drop
if (dropTargetLayout == null) {
return false;
}
if (!transitionStateShouldAllowDrop()) return false;
mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset,
d.dragView, mDragViewVisualCenter);
// We want the point to be mapped to the dragTarget.
if (mLauncher.isHotseatLayout(dropTargetLayout)) {
mapPointFromSelfToHotseatLayout(mLauncher.getHotseat(), mDragViewVisualCenter);
} else {
mapPointFromSelfToChild(dropTargetLayout, mDragViewVisualCenter, null);
}
int spanX = 1;
int spanY = 1;
if (mDragInfo != null) {
final CellLayout.CellInfo dragCellInfo = mDragInfo;
spanX = dragCellInfo.spanX;
spanY = dragCellInfo.spanY;
} else {
final ItemInfo dragInfo = (ItemInfo) d.dragInfo;
spanX = dragInfo.spanX;
spanY = dragInfo.spanY;
}
int minSpanX = spanX;
int minSpanY = spanY;
if (d.dragInfo instanceof PendingAddWidgetInfo) {
minSpanX = ((PendingAddWidgetInfo) d.dragInfo).minSpanX;
minSpanY = ((PendingAddWidgetInfo) d.dragInfo).minSpanY;
}
mTargetCell = findNearestArea((int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, dropTargetLayout,
mTargetCell);
float distance = dropTargetLayout.getDistanceFromCell(mDragViewVisualCenter[0],
mDragViewVisualCenter[1], mTargetCell);
//创建Folder
if (willCreateUserFolder((ItemInfo) d.dragInfo, dropTargetLayout,
mTargetCell, distance, true)) {
return true;
}
//加入已存在的Folder
if (willAddToExistingUserFolder((ItemInfo) d.dragInfo, dropTargetLayout,
mTargetCell, distance)) {
return true;
}
int[] resultSpan = new int[2];
//腾出区域
mTargetCell = dropTargetLayout.createArea((int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1], minSpanX, minSpanY, spanX, spanY,
null, mTargetCell, resultSpan, CellLayout.MODE_ACCEPT_DROP);
//大于0表示可以腾出区域
boolean foundCell = mTargetCell[0] >= 0 && mTargetCell[1] >= 0;
//如果没有mTargetCell不可腾出区域,那么就返回false。
if (!foundCell) {
// Don't show the message if we are dropping on the AllApps button and the hotseat
// is full
boolean isHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
if (mTargetCell != null && isHotseat) {
Hotseat hotseat = mLauncher.getHotseat();
if (hotseat.isAllAppsButtonRank(
hotseat.getOrderInHotseat(mTargetCell[0], mTargetCell[1]))) {
return false;
}
}
mLauncher.showOutOfSpaceMessage(isHotseat);
return false;
}
}
return true;
}
可以看到,方法内部重新获取了DragView的中心点,矫正之后获取距离DragView最近的mTargetCell位置及距离。进而进行了以下三种判断。
1.如果当前mTargetCell上已经有应用图标且mTargetCell与DragView所代表的的控件内容属于ShortcutInfo或者ApplicationInfo类型。那么就创建可以Folder。
2.如果当前mTargetCell是FolderIcon且FolderIcon内部child的数量没有达到mMaxNumItems(16)那么就可以加入已存在的Folder。
3.调用createaArea查看是否能找到策略腾出mTargetCell(如果数组中的CellX,CellY大于0,表示能够腾出)。
如果以上三种判断任意一种能够成立,那么就表示WorkSpace能够接手当前DragView的应用排序。执行WorkSpace的onDrop方法:
public void onDrop(final DragObject d) {
mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView,
mDragViewVisualCenter);
CellLayout dropTargetLayout = mDropToLayout;
// 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, null);
}
}
int snapScreen = -1;
boolean resizeOnDrop = false;
if (d.dragSource != this) {
final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
(int) mDragViewVisualCenter[1] };
//表示从all apps页面拖拽过来,或者FolderIcon拖拽过来。
onDropExternal(touchXY, d.dragInfo, dropTargetLayout, false, d);
} else if (mDragInfo != null) {
final View cell = mDragInfo.cell;
Runnable resizeRunnable = null;
if (dropTargetLayout != null) {
// Move internally
boolean hasMovedLayouts = (getParentCellLayoutForView(cell) != dropTargetLayout);
boolean hasMovedIntoHotseat = mLauncher.isHotseatLayout(dropTargetLayout);
long container = hasMovedIntoHotseat ?
LauncherSettings.Favorites.CONTAINER_HOTSEAT :
LauncherSettings.Favorites.CONTAINER_DESKTOP;
int screen = (mTargetCell[0] < 0) ?
mDragInfo.screen : indexOfChild(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);
//创造Folder,主要判断流程还会根据acceptDrop走,最终调用Launcher.java的addFolder方法添加FolderIcon到WorkSpace(在addFolder中又使用addInScreen方法)中
if (!mInScrollArea && createUserFolderIfNecessary(cell, container,
dropTargetLayout, mTargetCell, distance, false, d.dragView, null)) {
return;
}
//添加到已存在的Folder中,主要是添加到FoldeInfo的contents中
if (addToExistingFolderIfNecessary(cell, dropTargetLayout, mTargetCell,
distance, d, false)) {
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 = (ItemInfo) d.dragInfo;
int minSpanX = item.spanX;
int minSpanY = item.spanY;
if (item.minSpanX > 0 && item.minSpanY > 0) {
minSpanX = item.minSpanX;
minSpanY = item.minSpanY;
}
int[] resultSpan = new int[2];
//能否在最近mTargetCell位置找出策略来腾出位置
mTargetCell = dropTargetLayout.createArea((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
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 (mCurrentPage != screen && !hasMovedIntoHotseat) {
snapScreen = screen;
snapToPage(screen);
}
if (foundCell) {
final ItemInfo info = (ItemInfo) cell.getTag();
if (hasMovedLayouts) {
// Reparent the view
//移除原来的DragView所代表的的应用图标,并重新添加添加到WorkSpace的mTargetCell位置
getParentCellLayoutForView(cell).removeView(cell);
addInScreen(cell, container, screen, 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;
cell.setId(LauncherModel.getCellLayoutChildId(container, mDragInfo.screen,
mTargetCell[0], mTargetCell[1], mDragInfo.spanX, mDragInfo.spanY));
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) {
final Runnable addResizeFrame = new Runnable() {
public void run() {
DragLayer dragLayer = mLauncher.getDragLayer();
dragLayer.addResizeFrame(info, hostView, cellLayout);
}
};
resizeRunnable = (new Runnable() {
public void run() {
if (!isPageMoving()) {
addResizeFrame.run();
} else {
mDelayedResizeRunnable = addResizeFrame;
}
}
});
}
}
LauncherModel.moveItemInDatabase(mLauncher, info, container, screen, lp.cellX,
lp.cellY);
} 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();
final Runnable finalResizeRunnable = resizeRunnable;
// 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);
if (finalResizeRunnable != null) {
finalResizeRunnable.run();
}
}
};
mAnimatingViewIntoPlace = true;
if (d.dragView.hasDrawn()) {
final ItemInfo info = (ItemInfo) cell.getTag();
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) {
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);//最终把隐藏掉的cell显示出来了
}
parent.onDropChild(cell);
}
}
上边的流程其实和acceptDrop中的判断流程类似,只不过判断成功之后执行了添加操作。
至此,算是分析完了WorkSpace页面的应用图标长按拖拽的分析,接下来的下篇我们将分析FolderIcon和AppsCustomizePagedView页面的长按拖拽功能。