您的位置:首页 > 移动开发 > Android开发

Android Launcher 拖放功能深入研究

2012-09-25 22:22 295 查看
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.cell

this = workspace

child.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 = launcher

b = 根据拖放源view创建的大小一致的bitmap对象

registrationX = 手指点击位置与拖放源view 坐标x方向的偏移

registrationY = 手指点击位置与拖放源view 坐标y方向的偏移

textureLeft = 0

textureTop = 0

textureWidth = 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;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: