问:尼玛Android 4.1Jelly Bean都发布了,你还bb2.3,坑爹呢,这是?
答:这个真不好意思了,屌丝的特点就是后知后觉。
问:那有何用?
答:可以很不负责任的说,从2.2以后launcher 拖拽流程基本没变化。
问:基本?那还是有变化,到底还是坑爹。
答:“好吧,你赢了”。
-------------------------------------------------------------------------------------------------------------我是风格线--------
回归正题,要是做launcher的话,那拖拽事件处理、响应是必须得撸清楚的:
先来一张Launcher拖拽的时序图,以便对整个流程有个大概的把握(看不明白也没关系,跳过看下面先,回头再看就so easy了):
下面分步骤详细说明:
一,事件产生
拖拽事件的产生基于用户长按操作,分两种情况:
- 长按桌面(workspace视图)上的子视图(应用图标,文件夹,widget)事件响应是launcher.java
public boolean onLongClick(View v) { ........... else { if (!(cellInfo.cell instanceof Folder)) { // 用户长按某一组件时调用 mWorkspace.startDrag(cellInfo); } } } return true; }
F2跳转之:
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. // Note that Search takes focus when clicked rather than entering touch mode .......... mDragger.startDrag(child, this, child.getTag(), DragController.DRAG_ACTION_MOVE); invalidate(); clearVacantCache(); }
- 长按应用主菜单(AllAppGridView视图)上的子视图(应用图标)事件响应是AllAppGridView.java
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { if (!view.isInTouchMode()) { return false; } ApplicationInfo app = (ApplicationInfo) parent .getItemAtPosition(position); app = new ApplicationInfo(app); //用户长按应用主菜单应用图标时调用 mDragger.startDrag(view, this, app, DragController.DRAG_ACTION_COPY); }
该步骤的事件产生最终都定位mDragger.startDrag方法,顺着它往下摸就行了。
二,mDragger为何物?
mDragger是DragController实例,我们F3可以看到DragController其实就是一个接口,里面有几个很眼熟的抽象方法
F4一下可以知道该接口的唯一子类是DragLayer.java,必须进去一探究竟,尼玛这玩意竟然继承了frameLayout,脑子里有东西闪过,soga,原来是这样:
<com.android.launcher2.DragLayer xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher" android:id="@+id/drag_layer" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- Keep these behind the workspace so that they are not visible when we go into AllApps --> <include...........
上面的代码就是launcher的主布局文件。DragLayer作为整个launcher的RootView负责所有拖拽事件的统一分发处理。因为launcher上能看到的组件都布局在它之上
为啥它是frameLayout,我就不罗嗦了。
当用户长按某一launcher组件时,DragLayer.startDrag会扯开嗓子通知那些相关组件做好接受拖拽事件的准备,并顺便悄悄的隐藏被拖拽图标的原始view。
三,既然已经跟到frameLayout视图,那么接下来应该是顺着触屏时序处理来看比较直观。
public boolean onInterceptTouchEvent(MotionEvent ev) { .......... case MotionEvent.ACTION_UP: if (mShouldDrop && drop(x, y)) { mShouldDrop = false; } endDrag(); break; } return mDragging; }onInterceptTouchEvent作为触屏事件的节奏“打断者”在这里并没啥作为,只是在up时调用endDrag,用来在合适的时候显示刚才隐藏的view。
老大不管事,那自然小弟抗大梁了。看看onTouchEvent是如何有条不紊的处理这些拖拽事件的。
public boolean onTouchEvent(MotionEvent ev) { ......... switch (action) { case MotionEvent.ACTION_DOWN: // 初始化mScrollState状态。 mScrollState = SCROLL_OUTSIDE_ZONE; } break; case MotionEvent.ACTION_MOVE: // 不断更新当前icon的位置。 rect.union(left - 1, top - 1, left + width + 1, top + height + 1); final int[] coordinates = mDropCoordinates; //调用findDropTarget来寻找落脚点。比如是deleteZone区域,就触发相关的事件响应。 DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); ........ dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], ........ if (!inDragRegion && x < SCROLL_ZONE) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; mScrollRunnable.setDirection(SCROLL_LEFT); //拖着图标往左边换屏幕。 postDelayed(mScrollRunnable, SCROLL_DELAY); } } else if (!inDragRegion && x > getWidth() - SCROLL_ZONE) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; mScrollRunnable.setDirection(SCROLL_RIGHT); //拖着图标往右边换屏幕。 postDelayed(mScrollRunnable, SCROLL_DELAY); } } ....... case MotionEvent.ACTION_UP: removeCallbacks(mScrollRunnable); if (mShouldDrop) { //秋后算总账了,目标view是否接受该source view,还需drop(x,y)作最后定夺。 drop(x, y); mShouldDrop = false; } endDrag(); break; case MotionEvent.ACTION_CANCEL: endDrag(); } return true; }
在Action_move事件里要特别注意一下findDropTarget方法的实现,该事件会不断调用dropTarget的那几个抽象方法,只要是实现了该接口的view(userFolder,DeleteZone,workspace)都会根据特定的条件调用相应的重载方法,从而作出像文件夹开合,deleteZone变色等响应。
Action_up事件中咱们很熟练的F3,进了drop方法(太重要了,它决定了该拖拽事件最终的结果,是目标view接受了它还是拒绝都在这里都到裁决)。但代码其实很简短:
private boolean drop(float x, float y) { invalidate(); final int[] coordinates = mDropCoordinates; //如果上面findDropTarget是寻找落脚点,这里就是准备托付终身了。 DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); if (dropTarget != null) { if(shouldAccept == false && dropTarget.toString() == "ActionButton") { return false; } dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo); //如果这里的acceptDrop返回ture,那就意味着得到了男方父母的首肯,终于找到依附的view了。 if (dropTarget.acceptDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo)) { dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo); mDragSource.onDropCompleted((View) dropTarget, true); return true; } else { mDragSource.onDropCompleted((View) dropTarget, false); return true; } } return false; }
其实去年已经看这块了,怎奈人搓且奇懒无比,今天总算做个了结。