Android 4.0 Launcher2源码分析—桌面快捷图标的拖拽

标签: it |
http://blog.youkuaiyun.com/chenshaoyang0011/article/details/7854947
本文来自http://blog.youkuaiyun.com/chenshaoyang0011
通过上一篇文章Android4.0Launcher2源码分析(五)——Workspace的滑动中,已经了解了Launcher的ViewTree中各层所负责的工作,在DragLayer中就负责对快捷图标和AppWidget等组件的拖拽工作。桌面的滑动和图标的拖拽是两项独立的工作,正常情况下我们用手指滑动桌面会触发滑动操作,而当长按一个图标时,则会触发图标的拖拽操作,此时再滑动则会拖拽图标移动而桌面不会滑动。那么这里就分两大部分来探讨:1、拖拽操作的启动。2、拖拽。
一、拖拽操作的启动
那么首先进入Launcher.onCreate()中来探究下如何激活拖拽的状态。
- protected
void onCreate(Bundle savedInstanceState) { -
...... -
setupViews(); -
...... -
}
接着进入setupViews();
- private
void setupViews() { -
...... -
mWorkspace.setOnLongClickListener(this); -
...... -
}
从这里我们可以看到对Workspace设置了OnLongClickListener,而Launcher又实现了这个接口。接着进入Launcher.onLongClick()
- public
boolean onLongClick(View v) { -
...... -
if (!(v instanceof CellLayout)) { -
v = (View) v.getParent().getParent(); -
} -
resetAddInfo(); -
CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag(); -
...... -
// The hotseat touch handling does not go through Workspace, and we always allow long press -
// on hotseat items. -
final View itemUnderLongClick = longClickCellInfo.cell; -
boolean allowLongPress = isHotseatLayout(v) || mWorkspace.allowLongPress(); -
if (allowLongPress && !mDragController.isDragging()) { -
if (itemUnderLongClick == null) { -
...... -
} else { -
if (!(itemUnderLongClick instanceof Folder)) { -
// User long pressed on an item -
mWorkspace.startDrag(longClickCellInfo); -
} -
} -
} -
return true; -
}
当用户在一个item上长按时,则itemUnderLongClick != null,再通过调用Workspace.startDrag()来激活item的拖拽。下面先通过时序图来看下拖拽状态激活所经历的过程:
图标拖拽功能的激活大概可以分为六步,下面就一步一步的探究下其中的实现:
Step1:Workspace.startDrag(CellLayout.CellInfocellInfo)
- void
startDrag(CellLayout.CellInfo cellInfo) { -
View child = cellInfo.cell; -
...... -
mDragInfo = cellInfo; -
//使图标从桌面上消失,给人一种被“拖到空中”的感觉 -
child.setVisibility(GONE); -
...... -
final Canvas canvas = new Canvas(); -
-
// We need to add extra padding to the bitmap to make room for the glow effect -
final int bitmapPadding = HolographicOutlineHelper .MAX_OUTER_BLUR_RADIUS; -
-
// The outline is used to visualize where the item will land if dropped -
//图标的轮廓,在桌面上的对应的位置绘制图标的轮廓,显示当手松开图标时它在桌面上的落点 -
mDragOutline = createDragOutline(child, canvas, bitmapPadding); -
beginDragShared(child, this); -
}
在这个方法中,主要的工作就是让图标从桌面上消失,并且显示一个图标的外部轮廓,以表明它将要放置的位置,其显示的效果如下
显示图标的轮廓可以从视觉上给用户更加好的体验。接着,进入beginDragShared()
Step2:Workspace.beginDragShared(Viewchild,DragSource source)
- public
void beginDragShared(View child, DragSource source) { -
...... -
// The drag bitmap follows the touch point around on the screen -
final Bitmap b = createDragBitmap(child, new Canvas(), bitmapPadding); -
final int bmpWidth = b.getWidth(); -
-
//我们将在DragLayer中绘制“拖拽后”的图标,通过DragLayer.getLoactionInDragLayer() -
//获取在DragLayer中的坐标,并存放在mTempXY中。 -
mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY); -
final int dragLayerX = (int) mTempXY[0] + (child.getWidth() - bmpWidth) / 2; -
int dragLayerY = mTempXY[1] - bitmapPadding / 2; -
-
Point dragVisualizeOffset = null; -
Rect dragRect = null; -
-
//无论child是BubbleTextView或者PagedViewIncon或者FolderIcon的实例 -
//定位图标的位置与大小 -
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; -
// 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(-bitmapPadding / 2, iconPaddingTop - bitmapPadding / 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); -
} -
...... -
mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(), -
DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect); -
b.recycle(); -
}
Workspace.beginSharedDrag()中主要所做的工作就是计算拖拽目标位于DragLayer中的坐标和尺寸大小,接着又调用DragController.startDrag()
Step3:DragController.startDrag(Bitmapb ,int dragLayerX, int dragLayerY,DragSource source, ObjectdragInfo, int dragAction, Point dragOffset, RectdragRegion)
-
- public
void startDrag(Bitmap b, int dragLayerX, int dragLayerY, -
DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion) { -
...... -
for (DragListener listener : mListeners) { -
listener.onDragStart(source, dragInfo, dragAction); -
} -
-
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()中根据这个值判断是否拦截MotionEvent -
mDragging = true; -
-
//实例化DragObject,表示拖拽的对象 -
//封装了拖拽对象的信息 -
mDragObject = new DropTarget.DragObject(); -
-
mDragObject.dragComplete = false; -
mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); -
mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); -
mDragObject.dragSource = source; -
mDragObject.dragInfo = dragInfo; -
...... -
final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, -
registrationY, 0, 0, b.getWidth(), b.getHeight()); -
...... -
//将拖拽的图标显示在DragLayer中 -
dragView.show(mMotionDownX, mMotionDownY); -
handleMoveEvent(mMotionDownX, mMotionDownY); - }
代码中显示通过一个for语句调用了DragListener.onDragStart()方法,通知它们已经开始拖拽了,其中由于Workspace实现了DragListener并且添加到了mListeners中。所以Workspace.onDragStart()被调用。然后又封装了一个DragObject对象,封装DragSource、DragInfo和DragView等信息。接着,将调用DragView.show()将DragView显示在DragLayer中。
Step4:DragView.show(int touchX,inttouchY)
-
-
public void show(int touchX, int touchY) { -
//将DragView添加到DragLayer中 -
mDragLayer.addView(this); -
-
//设置位置、尺寸等信息 -
DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0); -
lp.width = mBitmap.getWidth(); -
lp.height = mBitmap.getHeight(); -
lp.x = touchX - mRegistrationX; -
lp.y = touchY - mRegistrationY; -
lp.customPosition = true; -
setLayoutParams(lp); -
mLayoutParams = lp; -
mAnim.start(); -
}
其中的内容很简单易懂,就是在将DragView添加到了DragLayer中,并且在合适的位置显示了出来。接着应该调用在DragController.startDrag()中调用handleMoveEvent(),这个将在后文将拖拽过程分析时在看。到这一步,拖拽操作的启动过程就完成了。接着就可以拖拽图标了。
二、拖拽
通过了前面文章的分析,已经知道了拖拽过程的实现在DragLayer中,当进行图标的拖拽时,DragLayer.onInterceptTouchEvent()就会对MotionEvent进行拦截。并且
在自身的onTouchEvent()方法中进行操作,从而实现图标的移动。由于onInterceptTouchEvent()拦截了MotionEvent,因此Workspace等UI控件不会接收到事件,从而不会产生
干扰。那么首先进入DragLayer.onInterceptTouchEvent():
- public
boolean onInterceptTouchEvent(MotionEvent ev) { -
...... -
return mDragController.onInterceptTouchEvent(ev); -
}
代码中省略了与其他功能的不部分代码,最后调用了DragController.onInterceptTouchEvent() ,并取其返回值作为自身方法的返回值。进入DragController.onInterceptTouchEvent()。
-
-
public boolean onInterceptTouchEvent(MotionEvent 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: -
if (mDragging) { -
drop(dragLayerX, dragLayerY); -
} -
endDrag(); -
break; -
case MotionEvent.ACTION_CANCEL: -
cancelDrag(); -
break; -
} -
-
return mDragging; -
}
这里我们关心的是它的返回值。可以看到方法将mDragging作为返回值。当触发了拖拽状态,在的DragController.startDrag()中将mDragging的值改为true。所以这里也将返回true。DragLayer将拦截MotionEvent,并传给自身的onTouchEvent()方法,在onTouchEvent()中对图标进行移动,刷新界面。
-
- public
boolean onTouchEvent(MotionEvent 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; -
-
//判断当前的触点是否处于屏幕边缘的ScrollZone,当处于这个区域时 -
//状态mScrollState将转变为SCROLL,并且在一定时间的停留之后,屏幕滑动到另一屏。 -
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()处理图标移动 -
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) { -
"WHITE-SPACE: pre"> //根据目前相对DragLayer的坐标,将图标“降落”到指定的DropTarget上。 -
drop(dragLayerX, dragLayerY); -
} -
endDrag(); -
break; -
case MotionEvent.ACTION_CANCEL: -
cancelDrag(); -
break; -
} -
return true; - }
onTouchEvent()中处理的事件涉及到不同状态之间的转换,以及每种状态之下对相应的MotionEvent的对策。这里同样,从简单的情况入手:图标拖拽起来后,移动一段距离,在屏幕的另一个位置放下。
首先,当拖拽起图标时,拖拽图标的状态被启动,这就是第一部分所探讨的内容。
然后,移动拖拽的图标。此时触发了MotionEvent.ACTION_MOVE事件,紧接着调用handleMoveEvent()来处理移动。进入handleMoveEvent()来看看图标移动是怎么实现的。
- private
void handleMoveEvent(int x, int y) { -
//更新在DragLayer中的位置 -
mDragObject.dragView.move(x, y); -
-
// Drop on someone? -
final int[] coordinates = mCoordinatesTemp; -
-
//根据当前的位置寻找DropTarget对象来放置图标 -
DropTarget dropTarget = findDropTarget(x, y, coordinates); -
mDragObject.x = coordinates[0]; -
mDragObject.y = coordinates[1]; -
if (dropTarget != null) { -
DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject); -
if (delegate != null) { -
dropTarget = delegate; -
} -
-
if (mLastDropTarget != dropTarget) { -
if (mLastDropTarget != null) { -
//从最后一次记录的DropTarget中退出 -
mLastDropTarget.onDragExit(mDragObject); -
} -
//进入到当前寻找到的DropTarget -
dropTarget.onDragEnter(mDragObject); -
} -
dropTarget.onDragOver(mDragObject); -
} else { -
if (mLastDropTarget != null) { -
mLastDropTarget.onDragExit(mDragObject); -
} -
} -
mLastDropTarget = dropTarget; -
-
// Scroll, maybe, but not if we're in the delete region. -
boolean inDeleteRegion = false; -
if (mDeleteRegion != null) { -
inDeleteRegion = mDeleteRegion.contains(x, y); -
} -
-
// After a scroll, the touch point will still be in the scroll region. -
// Rather than scrolling immediately, require a bit of twiddling to scroll again -
final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop (); -
mDistanceSinceScroll += -
Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); -
mLastTouch[0] = x; -
mLastTouch[1] = y; -
-
//判断当前拖拽的图标是否处于ScrollZone即滑动区域。 -
//并且根据在哪个一个ScrollZone来处理屏幕滑动的方向。 -
if (!inDeleteRegion && x < mScrollZone) { -
if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { -
mScrollState = SCROLL_WAITING_IN_ZONE; -
if (mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT)) { -
mScrollRunnable.setDirection(SCROLL_LEFT); -
mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); -
} -
} -
} else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) { -
if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { -
mScrollState = SCROLL_WAITING_IN_ZONE; -
if (mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT)) { -
mScrollRunnable.setDirection(SCROLL_RIGHT); -
mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); -
} -
} -
} else { -
if (mScrollState == SCROLL_WAITING_IN_ZONE) { -
mScrollState = SCROLL_OUTSIDE_ZONE; -
mScrollRunnable.setDirection(SCROLL_RIGHT); -
mHandler.removeCallbacks(mScrollRunnable); -
mDragScroller.onExitScrollArea(); -
} -
} -
}
handleMoveEvent()主要处理拖拽过程中需要处理的事务。包括:1、在更新图标在屏幕中的位置,并刷新UI。2、判断图标当前所处的位置。包括SCROLL_OUTSIDE_ZONE和SCROLL_WAITING_IN_ZONE,对处于SCROLL_WAITING_IN_ZONE位置时,需要根据具体的位置,向前或向后切换显示的屏幕。再回到上面假设的情况中。则此时只是简单的刷新了位置信息,并重新绘制图标。
最后,当松开拖拽的对象时,触发了MotionEvent.ACTION_UP事件。则进入下面一段代码: