在分析Launcher2的拖拽(触摸)事件之前,我们必须知道Android中事件的分发、拦截和处理机制。
有兴趣的可以看看《Android触摸事件简单分析》。不过,我这里再次简单总结一下:
1、事件一定是先到达父控件上。
2、事件简单来说可以分为三种:Down事件、Move事件、Up事件。
3、ViewGroup中才有事件的拦截方法( onInterceptTouchEvent() ),View中是没有的。
好了,我们原归正传,这里是分析Launcher2的拖拽(触摸)事件简单流程。
我用的Android源码是Android 6.0 的Launcher2,虽然各种版本(Launcher2)有些不同,但流程还是相似处理的。
我们先看看Launcher.java 中的xml布局:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res/com.la.launcher" android:id="@+id/launcher" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/workspace_bg" > <com.android.launcher2.DragLayer android:id="@+id/drag_layer" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" > <!-- The workspace contains 5 screens of cells --> <com.android.launcher2.Workspace android:id="@+id/workspace" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/workspace_bottom_padding" android:paddingEnd="@dimen/workspace_right_padding" android:paddingStart="@dimen/workspace_left_padding" android:paddingTop="@dimen/workspace_top_padding" launcher:cellCountX="@integer/cell_count_x" launcher:cellCountY="@integer/cell_count_y" launcher:defaultScreen="0" launcher:pageSpacing="@dimen/workspace_page_spacing" launcher:scrollIndicatorPaddingLeft="@dimen/qsb_bar_height" launcher:scrollIndicatorPaddingRight="@dimen/button_bar_height" > <include android:id="@+id/cell1" layout="@layout/workspace_screen" /> ...... </com.android.launcher2.Workspace> ...... <!-- hotseat区域 --> <include android:id="@+id/hotseat" android:layout_width="@dimen/button_bar_height_plus_padding" android:layout_height="match_parent" android:layout_gravity="end" layout="@layout/hotseat" android:visibility="gone" /> ...... </com.android.launcher2.DragLayer> </FrameLayout> 在上面布局中,有依次有如下关系图(只显示部分控件)
//布局结构分布关系图 FrameLayout DragLayer Workspace CellLayout Hotseat CellLayout 从上面布局结构图和文章开头的总结,我们可以知道触摸事件一定是先出现在FrameLayout,然后传给DragLayer或Hotseat,再传给它们子类。
这里以点击Launcher界面的快捷键图标为例子讲解
1、DragLayer
DragLayer是一个自定义的布局,继承于FrameLayout
public class DragLayer extends FrameLayout implements ViewGroup.OnHierarchyChangeListener { ...... } 额,DragLayer是个ViewGroup,在DragLayer中只实现了onInterceptTouchEvent拦截和onTouchEvent和处理方法。这里是重点
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { 【DOWN事件时调用了handleTouchDown,如果为true时,进行拦截,不在往下分发】 if (handleTouchDown(ev, true)) { return true; } } clearAllResizeFrames(); 【这里是调用了mDragController的拦截事件,返回true,表示进行拦截,会执行onTouchEvent方法】 return mDragController.onInterceptTouchEvent(ev); } DragLayer.handleTouchDown()
private boolean handleTouchDown(MotionEvent ev, boolean intercept) { Rect hitRect = new Rect(); int x = (int) ev.getX(); int y = (int) ev.getY(); //app widget 【如果点击是是widget控件,就返回true】 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; requestDisallowInterceptTouchEvent(true); return true; } } } //app folder 【如果是folder,打开或者关闭folder】 Folder currentFolder = mLauncher.getWorkspace().getOpenFolder(); if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) { if (currentFolder.isEditingName()) { if (!isEventOverFolderTextRegion(currentFolder, ev)) { currentFolder.dismissEditingName(); return true; } } getDescendantRectRelativeToSelf(currentFolder, hitRect); if (!isEventOverFolder(currentFolder, ev)) { mLauncher.closeFolder(); return true; } } return false;【默认是返回false】 } 在DragLayer中实现了触摸处理事件(虽然现在不涉及,但提前放在这里分析),如果DragLayer对事件进行了拦截,就会跑到这里。当然,如果由子布局不处理,最后也会上报到这里的。
DragLayer.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)) {【这里又调用了handleTouchDown】 return true; } } } if (mCurrentResizeFrame != null) {【mCurrentResizeFrame在handleTouchDown中如果点击的是widget时候进行赋值了】 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);【调用了mDragController的处理事件,如果返回true,表示已经处理,不再上报】 } 从上面看出,DragLayer对事件拦不了拦截还要看DragController的onInterceptTouchEvent()返回值。我们看看DragController
2、DragController
DragController只是一个单独的类
public class DragController { ...... } 虽然不是ViewGroup或View,但是新建了onInterceptTouchEvent和onTouchEvent方法,并对事件进行处理
public boolean onInterceptTouchEvent(MotionEvent ev) { @SuppressWarnings("all") // suppress dead code warning final boolean debug = 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_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) {【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; } 【如果是拖拽事件,返回true;否则返回false,至于是否是拖拽事件要看是否调用了DragController.startDrag()】 return mDragging;【如果是点击事件,这里返回false】 } /** * Call this from a drag source view. */ 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) { 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; } 如果DragController在onInterceptTouchEvent()返回true,表示DragLayer对事件拦截,就不会往下传递。我们这里分析点击快捷键图标
上面mDragging是是否拖拽标签,这个是在DragController.startDrag()方法中设置为true的
public void startDrag(Bitmap b, int dragLayerX, int dragLayerY, DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion, float initialDragViewScale) { ...... for (DragListener listener : mListeners) { 【这里做了一些在拖拽时监听,比如拖拽删除快捷键图标时删除图标(垃圾篓)的显示等】 listener.onDragStart(source, dragInfo, dragAction); } ...... mDragging = true;【这里就标志了拖拽事件的开始】 ...... handleMoveEvent(mMotionDownX, mMotionDownY);【这里会调用一次 handleMoveEvent 方法,在拖拽时这个handleMoveEvent方法一直会调用,看上面MotionEvent.ACTION_MOVE】 } 如果不是拖拽事件,也就是在DragController返回false,DragLayer不拦截,把事件分发给子布局Workspace或Hotseat等。我这分析Workspace
3、Workspace & PagedView
public class Workspace extends SmoothPagedView { ...... } public abstract class SmoothPagedView extends PagedView { ...... } public abstract class PagedView extends ViewGroup{ ...... } 额额,简单的说Workspace间接继承ViewGroup,不过这里只实现了onInterceptTouchEvent方法
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN:【DOWN事件】 mXDown = ev.getX(); mYDown = ev.getY(); break; case MotionEvent.ACTION_POINTER_UP:【UP事件】 case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_REST) {【如果UP事件来时,同时状态是TOUCH_STATE_REST,就走这里】 final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); if (!currentPage.lastDownOnOccupiedCell()) {【down时是否是点击到了快捷键图标,如是,lastDownOnOccupiedCell()返回true,否则false】 onWallpaperTap(ev); } } } return super.onInterceptTouchEvent(ev);【拦不拦截要看其父类(或祖父),PagedView 中实现了onInterceptTouchEvent方法】 } Workspace 中拦不拦截要看其祖父PagedView 中实现的onInterceptTouchEvent方法
4、PagedView.onInterceptTouchEvent()
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { acquireVelocityTrackerAndAddMovement(ev); if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { return true; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: {【MOVE事件】 if (mActivePointerId != INVALID_POINTER) { determineScrollingStart(ev); break; } } case MotionEvent.ACTION_DOWN: {【down事件】 final float x = ev.getX(); final float y = ev.getY(); // Remember location of down touch mDownMotionX = x; mLastMotionX = x; mLastMotionY = y; mLastMotionXRemainder = 0; mTotalMotionX = 0; mActivePointerId = ev.getPointerId(0); mAllowLongPress = true; final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop); if (finishedScrolling) { mTouchState = TOUCH_STATE_REST; mScroller.abortAnimation(); } else { mTouchState = TOUCH_STATE_SCROLLING; } if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) { if (getChildCount() > 0) { if (hitsPreviousPage(x, y)) { mTouchState = TOUCH_STATE_PREV_PAGE; } else if (hitsNextPage(x, y)) { mTouchState = TOUCH_STATE_NEXT_PAGE; } } } break; } case MotionEvent.ACTION_UP:【UP事件】 case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_REST; mAllowLongPress = false; mActivePointerId = INVALID_POINTER; releaseVelocityTracker(); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); releaseVelocityTracker(); break; } return mTouchState != TOUCH_STATE_REST;【如果mTouchState != TOUCH_STATE_REST为true时,表示进行拦截】 } 我们这里分析是点击快捷键图标,因此上面onInterceptTouchEvent()方法返回是false,因此触摸事件继续往子布局CellLayout分发
PagedView 中的 mTouchState有如下三种状态,触摸停止,触摸拖动,向前一页滑动、向后一页滑动
//mTouchState有三种状态,如下 protected final static int TOUCH_STATE_REST = 0; protected final static int TOUCH_STATE_SCROLLING = 1; protected final static int TOUCH_STATE_PREV_PAGE = 2; protected final static int TOUCH_STATE_NEXT_PAGE = 3; 5、CellLayout
我们看CellLayout的onInterceptTouchEvent方法
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN) {【按下down时走这里】 clearTagCellInfo(); 【清除】 } if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) { return true; } if (action == MotionEvent.ACTION_DOWN) { 【按下down时走这里】 setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY()); } return false; } CellLayout.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;【默认初始化为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()); frame.offset(getPaddingLeft(), getPaddingTop()); frame.inset((int) (frame.width() * (1f - scale) / 2), (int) (frame.height() * (1f - scale) / 2)); 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; 【点击在快捷键图标上,置为true】 break; } } } 【赋值。mLastDownOnOccupiedCell 保存down时获取的状态,这个会在Workspace的UP事件中调用】 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; } setTag(cellInfo); } 到现在位置,DOWN事件被我们处理完了,接着是MOVE和UP事件。(单击事件)
UP事件和上面DOWN的流程一样,以上布局或控件都不处理,最终,处理的事件又跑回到了Launcher.java中。
6、Launcher
PS:这里说明一下,上面DOWN和UP事件的开始端是Launcher.java开始的,如果上面布局或控件都不处理,又会回到Launcher.java中的。
Launcher中没有其他消耗事件的处理,但是有快捷键图标(BubbleTextView )做了点击事件监听。
public void onClick(View v) { if (v.getWindowToken() == null) { return; } if (!mWorkspace.isFinishedSwitchingState()) { return; } Object tag = v.getTag(); if (tag instanceof ShortcutInfo) {【快捷键图标】 final Intent intent = ((ShortcutInfo) tag).intent; int[] pos = new int[2]; v.getLocationOnScreen(pos); intent.setSourceBounds(new Rect(pos[0], pos[1], pos[0] + v.getWidth(), pos[1] + v.getHeight())); boolean success = startActivitySafely(v, intent, tag); if (success && v instanceof BubbleTextView) { mWaitingForResume = (BubbleTextView) v; mWaitingForResume.setStayPressed(true); } } else if (tag instanceof FolderInfo) {【文件夹】 if (v instanceof FolderIcon) { FolderIcon fi = (FolderIcon) v; handleFolderClick(fi); } } else if (v == mAllAppsButton) {【hotseat中显示所有应用的按钮】 if (isAllAppsVisible()) { showWorkspace(true); } else { onClickAllAppsButton(v); } } } 或许你会好奇,这是什么时候注册监听事件的,这个是在Launcher中有一个createShortcut()方法中注册了,而此方法在加载数据时调用,具体可以看看LauncherModel.java中的bindWorkspaceItems()方法。
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; } 好了,点击事件目前就结束,不过写得不是特别清晰。如果觉得有点累,可以看看《Launcher桌面点击&长按&拖动事件处理流程分析》,这里分析得也挺全的。
联系我们
微信号:rssme_com