Luancher有一个相对比较复杂的功能就是拖放功能,要深入了解launcher,深入理解拖放功能是有必要的,这篇blog,我将对launcher的拖放功能做深入的了解
1.首先直观感受什么时候开始拖放?我们长按桌面一个应用图标或者控件的时候拖放就开始了,包括在all app view中长按应用图标,下面就是我截取的拖放开始的代码调用堆栈 at com.android.launcher2.DragController.startDrag(DragController.java:170) at com.android.launcher2.Workspace.startDrag(Workspace.java:1068) at com.android.launcher2.Launcher.onLongClick(Launcher.java:1683) at android.view.View.performLongClick(View.java:2427) at android.widget.TextView.performLongClick(TextView.java:7286) at android.view.View$CheckForLongPress.run(View.java:8792) at android.os.Handler.handleCallback(Handler.java:587) at android.os.Handler.dispatchMessage(Handler.java:92) at android.os.Looper.loop(Looper.java:123) 桌面应用图标由Launcher.onLongClick负责监听处理,插入断点debug进入onLongclick函数 if (!(v instanceof CellLayout)) { v = (View) v.getParent(); } //获取桌面CellLayout上一个被拖动的对象 CellLayout.CellInfo cellInfo = (CellLayout.CellInfo) v.getTag(); ... if (mWorkspace.allowLongPress()) { if (cellInfo.cell == null) { ... } else { if (!(cellInfo.cell instanceof Folder)) { ... //调用Workspace.startDrag处理拖动 mWorkspace.startDrag(cellInfo); } } }我上面只写出关键代码,首先是获取被拖动的对象v.getTag(),Tag什么时候被设置进去的了 public boolean onInterceptTouchEvent(MotionEvent ev) { ... if (action == MotionEvent.ACTION_DOWN) { ... boolean found = false; for (int i = count - 1; i >= 0; i--) { final View child = getChildAt(i);if ((child.getVisibility()) == VISIBLE || child.getAnimation() != null) {
child.getHitRect(frame); //判断区域是否在这个子控件的区间,如果有把child信息赋给mCellInfo if (frame.contains(x, y)) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); cellInfo.cell = child; cellInfo.cellX = lp.cellX; cellInfo.cellY = lp.cellY; cellInfo.spanX = lp.cellHSpan; cellInfo.spanY = lp.cellVSpan; cellInfo.valid = true; found = true; mDirtyTag = false; break; } } } mLastDownOnOccupiedCell = found;if (!found) {
... //没有child view 说明没有点击桌面图标项 cellInfo.cell = null; } setTag(cellInfo); }看了上面代码知道,当开始点击桌面时,celllayout就会根据点击区域去查找在该区域是否有child存在,若有把它设置为tag.cell,没有,tag.cell设置为null,后面在开始拖放时launcher.onlongclick中对tag进行处理,这个理顺了,再深入到workspace.startDrag函数,workspace.startDrag调用DragController.startDrag去处理拖放mDragController.startDrag(child, this, child.getTag(), DragController.DRAG_ACTION_MOVE);再分析一下上面调用的几个参数child = tag.cellthis = workspacechild.getTag()是什么呢?在什么时候被设置?再仔细回顾原来launcher加载过程代码,在launcher.createShortcut中它被设置了:注意下面我代码中的注释 View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) { TextView favorite = (TextView) mInflater.inflate(layoutResId, parent, false);favorite.setCompoundDrawablesWithIntrinsicBounds(null,
new FastBitmapDrawable(info.getIcon(mIconCache)), null, null); favorite.setText(info.title); //设置favorite(一个桌面Shortcut类型的图标)的tag favorite.setTag(info); favorite.setOnClickListener(this);return favorite;
}继续深入解读DragController.startDrag函数 public void startDrag(View v, DragSource source, Object dragInfo, int dragAction) { //设置拖放源view mOriginator = v; //获取view的bitmap Bitmap b = getViewBitmap(v);if (b == null) {
// out of memory? return; } //获取源view在整个屏幕的坐标 int[] loc = mCoordinatesTemp; v.getLocationOnScreen(loc); int screenX = loc[0]; int screenY = loc[1]; //该函数功能解读请继续往下看 startDrag(b, screenX, screenY, 0, 0, b.getWidth(), b.getHeight(), source, dragInfo, dragAction);b.recycle();
//设置原来view不可见 if (dragAction == DRAG_ACTION_MOVE) { v.setVisibility(View.GONE); } } public void startDrag(Bitmap b, int screenX, int screenY, int textureLeft, int textureTop, int textureWidth, int textureHeight, DragSource source, Object dragInfo, int dragAction) { //隐藏软键盘 if (mInputMethodManager == null) { mInputMethodManager = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); } mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); //mListener = deletezone,在blog laucher ui框架中有说明该函数,主要就是现实deletezone if (mListener != null) { mListener.onDragStart(source, dragInfo, dragAction); } //记住手指点击位置与屏幕左上角位置偏差 int registrationX = ((int)mMotionDownX) - screenX; int registrationY = ((int)mMotionDownY) - screenY;mTouchOffsetX = mMotionDownX - screenX;
mTouchOffsetY = mMotionDownY - screenY;mDragging = true;
mDragSource = source; mDragInfo = dragInfo;mVibrator.vibrate(VIBRATE_DURATION);
//创建DragView对象 DragView dragView = mDragView = new DragView(mContext, b, registrationX, registrationY, textureLeft, textureTop, textureWidth, textureHeight); //显示Dragview对象 dragView.show(mWindowToken, (int)mMotionDownX, (int)mMotionDownY); }到这里,拖放开始处理的框框基本清楚,但是DragView的创建和显示还有必要进一步深究 DragView dragView = mDragView = new DragView(mContext, b, registrationX, registrationY, textureLeft, textureTop, textureWidth, textureHeight);//函数参数说明:mContext = launcherb = 根据拖放源view创建的大小一致的bitmap对象registrationX = 手指点击位置与拖放源view 坐标x方向的偏移 registrationY = 手指点击位置与拖放源view 坐标y方向的偏移 textureLeft = 0textureTop = 0textureWidth = b.getWidth()textureHeight = b.getHeight()//函数体 super(context); //获取window管理器 mWindowManager = WindowManagerImpl.getDefault(); //一个动画,开始拖放时显示 mTween = new SymmetricalLinearTween(false, 110 /*ms duration*/, this); //对源b 做一个缩放产生一个新的bitmap对象 Matrix scale = new Matrix(); float scaleFactor = width; scaleFactor = mScale = (scaleFactor + DRAG_SCALE) / scaleFactor; scale.setScale(scaleFactor, scaleFactor);mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height, scale, true);
// The point in our scaled bitmap that the touch events are located
mRegistrationX = registrationX + (DRAG_SCALE / 2); mRegistrationY = registrationY + (DRAG_SCALE / 2);其实函数很简单,就是记录一些参数,然后对view图片做一个缩放处理,并且准备一个tween动画,在长按桌面图标后图标跳跃到手指上显示该动画,了解这些,有助于理解函数dragView.show//windowToken来自与workspace.onattchtowindow时候获取的view 所有attch的window标识,有这个参数,可以把dragview添加到workspace所属的同一个window对象//touchX,手指点击在屏幕的位置x//touchy,手指点击在屏幕的位置y public void show(IBinder windowToken, int touchX, int touchY) { WindowManager.LayoutParams lp; int pixelFormat;pixelFormat = PixelFormat.TRANSLUCENT;
//布局参数值的注意的是view位置参数, //x=touchX-mRegistrationX=touchX-(registrationX + (DRAG_SCALE / 2))=手指点击位置-view坐标与手指点击位置偏差加上缩放值 lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, touchX-mRegistrationX, touchY-mRegistrationY, WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS /*| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM*/, pixelFormat);// lp.token = mStatusBarView.getWindowToken(); lp.gravity = Gravity.LEFT | Gravity.TOP; lp.token = windowToken; lp.setTitle("DragView"); mLayoutParams = lp; //dragview的父类是Window,也就是说dragview可以拖放到屏幕的任意位置 mWindowManager.addView(this, lp);mAnimationScale = 1.0f/mScale;
//播放开始拖动动画(直观感觉是图标变大了) mTween.start(true); }2,拖放过程
拖放过程的处理需要深入了解DragController.onTouchEvent(MotionEvent ev)函数的实现,我下面列出关键的MotionEvent.ACTION_MOVE部分代码并作出注释说明 case MotionEvent.ACTION_MOVE: // 根据手指坐标移动dragview mDragView.move((int) ev.getRawX(), (int) ev.getRawY());// 根据手指所在屏幕坐标获取目前所在的拖放目的view
final int[] coordinates = mCoordinatesTemp; DropTarget dropTarget = findDropTarget(screenX, screenY, coordinates); // 根据不同状态调用DropTarget的生命周期处理函数 if (dropTarget != null) { if (mLastDropTarget == dropTarget) { dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); } else { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); } dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); } } else { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); } } mLastDropTarget = dropTarget;//判断是否在delete区域
boolean inDeleteRegion = false; if (mDeleteRegion != null) { inDeleteRegion = mDeleteRegion.contains(screenX, screenY); } //不在delete区域,在左边切换区 if (!inDeleteRegion && screenX < SCROLL_ZONE) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; mScrollRunnable.setDirection(SCROLL_LEFT); mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); } } //不在delete区,在右边切换区 else if (!inDeleteRegion && screenX > scrollView.getWidth() - SCROLL_ZONE) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; mScrollRunnable.setDirection(SCROLL_RIGHT); mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); } } //在delete区域 else { if (mScrollState == SCROLL_WAITING_IN_ZONE) { mScrollState = SCROLL_OUTSIDE_ZONE; mScrollRunnable.setDirection(SCROLL_RIGHT); mHandler.removeCallbacks(mScrollRunnable); } }break;
拖放过程总的处理思路就是根据当前坐标位置获取dropTarget的目标位置,然后又根据相关状态和坐标位置调用dropTarget的对应生命周期函数,这里面有两个点需要进一步深入了解,一是查找dropTarget:findDropTarget(screenX, screenY, coordinates),二是mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);--1.findDropTarget private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { final Rect r = mRectTemp; //mDropTargets是一个拖放目标view别表,在laucher初始化等被添加 final ArrayList<DropTarget> dropTargets = mDropTargets; final int count = dropTargets.size(); //遍历dropTargets列表,查看{x,y}是否落在dropTarget坐标区域,若是,返回dropTarget。 for (int i=count-1; i>=0; i--) { final DropTarget target = dropTargets.get(i); target.getHitRect(r); //获取target左上角屏幕坐标 target.getLocationOnScreen(dropCoordinates); r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); if (r.contains(x, y)) { dropCoordinates[0] = x - dropCoordinates[0]; dropCoordinates[1] = y - dropCoordinates[1]; return target; } } return null; }--2.mScrollRunnable//看mScrollRunnable对象的构造类,通过setDirection设置滚动方向,然后通过一步调用DragScroller.scrollLeft/scrollRight来对桌面进行向左向右滚动,想深入了解如何实现的,敬请阅读我相关blog:Launcher——桌面移动详解 private class ScrollRunnable implements Runnable { private int mDirection;ScrollRunnable() {
}public void run() {
if (mDragScroller != null) { if (mDirection == SCROLL_LEFT) { mDragScroller.scrollLeft(); } else { mDragScroller.scrollRight(); } mScrollState = SCROLL_OUTSIDE_ZONE; } }void setDirection(int direction) {
mDirection = direction; } }3.拖放结束,入口还是在DragController.onTouchEvent(MotionEvent ev) 先看调用堆栈:at com.android.launcher2.DragController.endDrag(DragController.java:315)at com.android.launcher2.DragController.onTouchEvent(DragController.java:471)at com.android.launcher2.DragLayer.onTouchEvent(DragLayer.java:64)at android.view.View.dispatchTouchEvent(View.java:3766) onTouchEvent关键代码: case MotionEvent.ACTION_UP: mHandler.removeCallbacks(mScrollRunnable); if (mDragging) { // 拖动过程手指离开屏幕 drop(screenX, screenY); } endDrag(); break;--1.drop(screenX, screenY); final int[] coordinates = mCoordinatesTemp; //获取dropTarget对象 DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); //coordinates=点触点在dropTarget 中的xy坐标if (dropTarget != null) {
dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); //根据相关参数判断是否可dropTarget是否接受该drag view if (dropTarget.acceptDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo)) { dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); mDragSource.onDropCompleted((View) dropTarget, true); return true; } else { mDragSource.onDropCompleted((View) dropTarget, false); return true; } } return false;