目录
在分析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桌面点击&长按&拖动事件处理流程分析》,这里分析得也挺全的。