研究Launcher的一大重点的就是view的拖动的实现,但在此之前,我们需要清楚ViewGroup、View触摸事件分发拦截机制
1、View的触摸事件的分发
分发流程概括如下
dispatchKeyEvent(KeyEvent event)
--> onTouchListener(MotionEvent ev)
--> onTouchEvent(MotionEvent ev)
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
return false;
}
从上述源码中我们可以看出,触摸事件显示分发到dispatchTouchEvent(event)后,先考虑View的OnTouchListener,然后才会传给onTouchEvent(Event).
接着看onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 该view当前是disable状态,直接消耗掉该touch事件
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (mTouchDelegate != null) {
// 直接交给事件代理者
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
// 具体的事件处理
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:{
if (isInScrollingContainer) {// 如果父View是一个ViewGroup则isInScrollingContainer为true,否则未false
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
// 180MS后发起一个检测长按任务
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
// 接着--> checkForLongClick(ViewConfiguration.getTapTimeout());
//(500 - 180)毫秒后执行检测长按的任务 -->performLongClick()
//--> postDelayed(mPendingCheckForLongPress,
//ViewConfiguration.getLongPressTimeout() - delayOffset);
} else {
setPressed(true);
checkForLongClick(0);// 500MS
}
}
case MotionEvent.ACTION_MOVE:{// 主要是检测滑动范围是否在该view的触摸范围内
final int x = (int) event.getX();
final int y = (int) event.getY();
if (!pointInView(x, y, mTouchSlop)) {// 判断触摸事件是否依旧在该View的范围内
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
}
case MotionEvent.ACTION_UP:{
1) <= 180ms mPrivateFlags |= PFLAG_PREPRESSED; //把mPrivateFlags和PFLAG_PREPRESSED按位或赋给mPrivateFlags
--> performClick()
64毫秒后执行mUnsetPressedState --> setPressed(false) -->dispatchSetPressed(false);
2) > 180ms && <=500ms:移除长按检测,执行onClick()回调
3) > 500ms
... ...
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
if (!mHasPerformedLongPress) {// 500ms非长按事件
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
... ...
removeTapCallback();
}
}
... ...
return true;
}
return false;
}
再看view的点击事件和长按事件
// View的点击操作
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
return true;
}
return false;
}
// view的长按操作
public boolean performLongClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
boolean handled = false;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLongClickListener != null)
handled = li.mOnLongClickListener.onLongClick(View.this);
if (!handled)
handled = showContextMenu();
if (handled)
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return handled;
}
2、ViewGroup的触摸事件的分发
先由dispatchTouchEvent(MotionEvent ev)接收触摸事件,然后在onInterceptTouchEvent()中是否进行拦截,不拦截则将根据x,y所在的位置将touch事件分发给该view。
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
...
intercepted = onInterceptTouchEvent(ev);// 在onInterceptTouchEvent(ev)中可进行拦截,但只有为拦截时,才会进入下列if语句
...
if (onFilterTouchEventForSecurity(ev)) {
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
...
resetCancelNextUpFlag(child);
// 分发到子View --> dispatchTransformedTouchEvent() -->dispatchTouchEvent()
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//
...
break;
}
}
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
return handled;
}
}
ViewGroup的事件分发的流程概括如下:
dispatchKeyEvent(KeyEvent event)
--> onInterceptTouchEvent(MotionEvent ev)// 可在此进行事件拦截,return true:拦截该事件,否则继续向下分发
--> onTouchEvent(MotionEvent ev) -- 自己处理touch事件
了解完View和ViewGroup的事件分发后,我们来看Launcher是如何实现控件的拖动的。
首先在Launcher中接收到一个长按事件
Launcher -->
public boolean onLongClick(View v) {
// 1) 获取CellLayout上一个被拖动的对象
CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag();
if (itemUnderLongClick == null) {
// 进入widget选择界面
showWidgetAddEdit(true);
} else {
// 开始拖动ShortCut、Widget、FolderIcon
if (itemUnderLongClick instanceof JrdShortcut){
mWorkspace.startDrag(longClickCellInfo);
return true;
} else if(itemUnderLongClick instanceof AppWidgetHostView) {
mWorkspace.startDrag(longClickCellInfo);
return true;
} else if(itemUnderLongClick instanceof FolderIcon) {
...
mWorkspace.startDrag(longClickCellInfo);
return true;
}
}
}
接着Workspace中的startDrag()方法
void startDrag(CellLayout.CellInfo cellInfo) {
View child = cellInfo.cell;
// 判断点击的icon是否未空
if (child != null && child.getTag() == null) {
return;
}
// 隐藏原先的图标
mDragInfo = cellInfo;
child.setVisibility(INVISIBLE);
mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING);
beginDragShared(child, this);
}
上述child.getTag()是在Launcher的onCreateShortCur()中设置的
View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) {
BubbleTextView favorite = (BubbleTextView) mInflater.inflate(layoutResId, parent, false);
favorite.applyFromShortcutInfo(info, mIconCache);
favorite.setOnClickListener(this);//设置点击事件
return favorite;
}
public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) {
Bitmap b = info.getIcon(iconCache);
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
setCompoundDrawables(null, Utilities.createIconDrawable(b), null, null);
setCompoundDrawablePadding((int) ((grid.folderIconSizePx - grid.iconSizePx) / 2f));
setText(info.title);
setTag(info);// 设置Tag信息
}
在Workspace.beginDragShared()中调用DragConoller()的startDrag()
public void beginDragShared(View child, DragSource source) {
// 创建被拖动时的Bitmap
final Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING);
...
mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
}
DragController是拖动事件的控制中心
public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,
DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
float initialDragViewScale) {
// 1) 调用各个监听对象
for (DragListener listener : mListeners) {
listener.onDragStart(source, dragInfo, dragAction);
}
// 记录当前的状态
mDragging = true;
// 2) 创建DragView对象
final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);
...
// 3) 显示DragView对象(将该DragView添加到DragLayer上)
dragView.show(mMotionDownX, mMotionDownY); --> DragView.show(){mDragLayer.addView(this)}
// 4) 根据当前的位置处理移动事件
handleMoveEvent(mMotionDownX, mMotionDownY);
}
private void handleMoveEvent(int x, int y) {
// 1) 移动View,DragView继承至View,在方法move()中设置setTranslationX()、setTranslationY()即可移动view
mDragObject.dragView.move(x, y);
// 2)查找移动目标
DropTarget dropTarget = findDropTarget(x, y, coordinates);
checkTouchMove(dropTarget);
...
checkScrollState(x, y);
}
上述的DragView的show()方法
public void show(int touchX, int touchY) {
mDragLayer.addView(this);
...
setLayoutParams(lp);
// 设置显示位置
setTranslationX(touchX - mRegistrationX);
setTranslationY(touchY - mRegistrationY);
// 播放动画
post(new Runnable() {
public void run() {
mAnim.start();
}
});
}
至此长按一个图标到到开始拖动已经准备好,详细的拖动过程需要从DragController.onInterceptTouchEvent()说起,
DragLayer是Launcher所有布局的父容器,它的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:
mLastTouchUpTime = System.currentTimeMillis();
if (mDragging) {
PointF vec = isFlingingToDelete(mDragObject.dragSource);
if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) {
vec = null;
}
if (vec != null) {
dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
} else {
drop(dragLayerX, dragLayerY);
}
}
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
cancelDrag();
break;
}
return mDragging;
}
在startDrag中已经将mDragging设为true,所以move状态下,DragLayer对touch事件进行了拦截,DragContrlller.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;
if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) {
mScrollState = SCROLL_WAITING_IN_ZONE;
mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
} else {
mScrollState = SCROLL_OUTSIDE_ZONE;
}
handleMoveEvent(dragLayerX, dragLayerY);
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) {
// 判断是否到达可删除的区域
PointF vec = isFlingingToDelete(mDragObject.dragSource);
if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) {
vec = null;
}
if (vec != null) {
// 拖动到垃圾箱中进行删除
dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
} else {
drop(dragLayerX, dragLayerY);
}
}
// 拖放结束
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
mHandler.removeCallbacks(mScrollRunnable);
cancelDrag();
break;
}
return true;
}
// 拖放结束
private void drop(float x, float y) {
final int[] coordinates = mCoordinatesTemp;
// x,y所在区域是否有合适的目标
final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
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);
}
private void endDrag() {
if (mDragging) {
// 回复拖放状态值
mDragging = false;
clearScrollRunnable();
...
mDragObject.dragView.remove();
...
for (DragListener listener : mListeners) {
listener.onDragEnd();
}
}
releaseVelocityTracker();
}
// 更新界面,更新数据库
Workspace.acceptDrop()
--> CellLayout.createArea()
--> CellLayout.commitTempPlacement()
--> Workspace.updateItemLocationsInDatabase(this);
至此ShortCut的拖动就完成了,数据库的更新就不在此显示了,数据库的更新方法在Workspace.updateItemLocationsInDatabase(this)中