Android Launcher3(二) -- Drag拖动实现

来源:互联网 发布:手机数据恢复方法 编辑:程序博客网 时间:2024/06/05 17:19

研究Launcher的一大重点的就是view的拖动的实现,但在此之前,我们需要清楚ViewGroup、View触摸事件分发拦截机制

1、View的触摸事件的分发

分发流程概括如下

        dispatchKeyEvent(KeyEvent event)
            --> onTouchListener(MotionEvent ev)           
                --> onTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent event) {if (onFilterTouchEventForSecurity(event)) {ListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {return true;}if (onTouchEvent(event)) {return true;}}return false;}

从上述源码中我们可以看出,触摸事件显示分发到dispatchTouchEvent(event)后,先考虑View的OnTouchListener,然后才会传给onTouchEvent(Event).

接着看onTouchEvent()方法

public boolean onTouchEvent(MotionEvent event) {final int viewFlags = mViewFlags;if ((viewFlags & ENABLED_MASK) == DISABLED) {// 该view当前是disable状态,直接消耗掉该touch事件return (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));}if (mTouchDelegate != null) {// 直接交给事件代理者if (mTouchDelegate.onTouchEvent(event)) {return true;}}if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {// 具体的事件处理switch (event.getAction()) {case MotionEvent.ACTION_DOWN:{if (isInScrollingContainer) {// 如果父View是一个ViewGroup则isInScrollingContainer为true,否则未falsemPrivateFlags |= PFLAG_PREPRESSED;if (mPendingCheckForTap == null) {mPendingCheckForTap = new CheckForTap();}// 180MS后发起一个检测长按任务postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());// 接着--> checkForLongClick(ViewConfiguration.getTapTimeout());//(500 - 180)毫秒后执行检测长按的任务 -->performLongClick()//--> postDelayed(mPendingCheckForLongPress, //ViewConfiguration.getLongPressTimeout() - delayOffset);} else {setPressed(true);checkForLongClick(0);// 500MS}}case MotionEvent.ACTION_MOVE:{// 主要是检测滑动范围是否在该view的触摸范围内final int x = (int) event.getX();final int y = (int) event.getY();if (!pointInView(x, y, mTouchSlop)) {// 判断触摸事件是否依旧在该View的范围内removeTapCallback();if ((mPrivateFlags & PFLAG_PRESSED) != 0) {// Remove any future long press/tap checksremoveLongPressCallback();setPressed(false);}}}case MotionEvent.ACTION_UP:{1) <= 180ms mPrivateFlags |= PFLAG_PREPRESSED; //把mPrivateFlags和PFLAG_PREPRESSED按位或赋给mPrivateFlags--> performClick()64毫秒后执行mUnsetPressedState --> setPressed(false) -->dispatchSetPressed(false);2) > 180ms && <=500ms:移除长按检测,执行onClick()回调3) > 500ms... ...if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {if (!mHasPerformedLongPress) {// 500ms非长按事件removeLongPressCallback();if (!focusTaken) {if (mPerformClick == null) {mPerformClick = new PerformClick();}if (!post(mPerformClick)) {performClick();}}}if (prepressed) {postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {mUnsetPressedState.run();}... ...removeTapCallback();}}... ...return true;}return false;}

再看view的点击事件和长按事件

    // View的点击操作    public boolean performClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        ListenerInfo li = mListenerInfo;        if (li != null && li.mOnClickListener != null) {            playSoundEffect(SoundEffectConstants.CLICK);            li.mOnClickListener.onClick(this);            return true;        }        return false;    }    // view的长按操作    public boolean performLongClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);        boolean handled = false;        ListenerInfo li = mListenerInfo;        if (li != null && li.mOnLongClickListener != null)            handled = li.mOnLongClickListener.onLongClick(View.this);        if (!handled)            handled = showContextMenu();        if (handled)            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);        return handled;    }


2、ViewGroup的触摸事件的分发

先由dispatchTouchEvent(MotionEvent ev)接收触摸事件,然后在onInterceptTouchEvent()中是否进行拦截,不拦截则将根据x,y所在的位置将touch事件分发给该view。

public boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = false;...intercepted = onInterceptTouchEvent(ev);// 在onInterceptTouchEvent(ev)中可进行拦截,但只有为拦截时,才会进入下列if语句...if (onFilterTouchEventForSecurity(ev)) {if (!canceled && !intercepted) {if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {final int actionIndex = ev.getActionIndex(); // always 0 for downfinal int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;final int childrenCount = mChildrenCount;if (newTouchTarget == null && childrenCount != 0) {final float x = ev.getX(actionIndex);final float y = ev.getY(actionIndex);final View[] children = mChildren;final boolean customOrder = isChildrenDrawingOrderEnabled();for (int i = childrenCount - 1; i >= 0; i--) {final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;final View child = children[childIndex];... resetCancelNextUpFlag(child);// 分发到子View --> dispatchTransformedTouchEvent() -->dispatchTouchEvent()if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// ...break;}}}if (newTouchTarget == null && mFirstTouchTarget != null) {// Did not find a child to receive the event.// Assign the pointer to the least recently added target.newTouchTarget = mFirstTouchTarget;while (newTouchTarget.next != null) {newTouchTarget = newTouchTarget.next;}newTouchTarget.pointerIdBits |= idBitsToAssign;}}}return handled;}}

ViewGroup的事件分发的流程概括如下:
dispatchKeyEvent(KeyEvent event)

    --> onInterceptTouchEvent(MotionEvent ev)// 可在此进行事件拦截,return true:拦截该事件,否则继续向下分发
         --> onTouchEvent(MotionEvent ev) -- 自己处理touch事件


了解完View和ViewGroup的事件分发后,我们来看Launcher是如何实现控件的拖动的。

首先在Launcher中接收到一个长按事件

Launcher -->public boolean onLongClick(View v) {// 1) 获取CellLayout上一个被拖动的对象CellLayout.CellInfo longClickCellInfo = (CellLayout.CellInfo) v.getTag();if (itemUnderLongClick == null) {// 进入widget选择界面showWidgetAddEdit(true);} else {// 开始拖动ShortCut、Widget、FolderIconif (itemUnderLongClick instanceof JrdShortcut){mWorkspace.startDrag(longClickCellInfo);return true;} else if(itemUnderLongClick instanceof AppWidgetHostView) {mWorkspace.startDrag(longClickCellInfo);return true;} else if(itemUnderLongClick instanceof FolderIcon) {...mWorkspace.startDrag(longClickCellInfo);return true;}}}


接着Workspace中的startDrag()方法

void startDrag(CellLayout.CellInfo cellInfo) {View child = cellInfo.cell;// 判断点击的icon是否未空if (child != null && child.getTag() == null) {return;}// 隐藏原先的图标mDragInfo = cellInfo;child.setVisibility(INVISIBLE);mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING);beginDragShared(child, this);}

上述child.getTag()是在Launcher的onCreateShortCur()中设置的

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;}public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache) {Bitmap b = info.getIcon(iconCache);LauncherAppState app = LauncherAppState.getInstance();DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();setCompoundDrawables(null, Utilities.createIconDrawable(b), null, null);setCompoundDrawablePadding((int) ((grid.folderIconSizePx - grid.iconSizePx) / 2f));setText(info.title);setTag(info);// 设置Tag信息}

在Workspace.beginDragShared()中调用DragConoller()的startDrag()

public void beginDragShared(View child, DragSource source) {// 创建被拖动时的Bitmapfinal Bitmap b = createDragBitmap(child, new Canvas(), DRAG_BITMAP_PADDING);...mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);}


DragController是拖动事件的控制中心

public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,float initialDragViewScale) {// 1) 调用各个监听对象for (DragListener listener : mListeners) {listener.onDragStart(source, dragInfo, dragAction);}// 记录当前的状态mDragging = true;// 2) 创建DragView对象final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);...// 3) 显示DragView对象(将该DragView添加到DragLayer上)dragView.show(mMotionDownX, mMotionDownY); --> DragView.show(){mDragLayer.addView(this)}// 4) 根据当前的位置处理移动事件handleMoveEvent(mMotionDownX, mMotionDownY);}


private void handleMoveEvent(int x, int y) {// 1) 移动View,DragView继承至View,在方法move()中设置setTranslationX()、setTranslationY()即可移动viewmDragObject.dragView.move(x, y);// 2)查找移动目标DropTarget dropTarget = findDropTarget(x, y, coordinates);checkTouchMove(dropTarget);...checkScrollState(x, y);}


上述的DragView的show()方法

public void show(int touchX, int touchY) {mDragLayer.addView(this);...setLayoutParams(lp);// 设置显示位置setTranslationX(touchX - mRegistrationX);setTranslationY(touchY - mRegistrationY);// 播放动画post(new Runnable() {public void run() {mAnim.start();}});}


至此长按一个图标到到开始拖动已经准备好,详细的拖动过程需要从DragController.onInterceptTouchEvent()说起,

DragLayer是Launcher所有布局的父容器,它的onInterceptTouchEvent()已交由DragController.onInterceptTouchEvent()来处理,

public boolean onInterceptTouchEvent(MotionEvent 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 touchmMotionDownX = dragLayerX;mMotionDownY = dragLayerY;mLastDropTarget = null;break;case MotionEvent.ACTION_UP:mLastTouchUpTime = System.currentTimeMillis();if (mDragging) {PointF vec = isFlingingToDelete(mDragObject.dragSource);if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) {vec = null;}if (vec != null) {dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);} else {drop(dragLayerX, dragLayerY);}}endDrag();break;case MotionEvent.ACTION_CANCEL:cancelDrag();break;}return mDragging;}


在startDrag中已经将mDragging设为true,所以move状态下,DragLayer对touch事件进行了拦截,DragContrlller.onTouchEvent()中的做统一处理

public boolean onTouchEvent(MotionEvent 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 startedmMotionDownX = 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;}handleMoveEvent(dragLayerX, dragLayerY);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 (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) {vec = null;}if (vec != null) { // 拖动到垃圾箱中进行删除dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);} else {drop(dragLayerX, dragLayerY);}}// 拖放结束endDrag();break;case MotionEvent.ACTION_CANCEL:mHandler.removeCallbacks(mScrollRunnable);cancelDrag();break;}return true;}// 拖放结束private void drop(float x, float y) {final int[] coordinates = mCoordinatesTemp;// x,y所在区域是否有合适的目标final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);boolean accepted = false;if (dropTarget != null) {mDragObject.dragComplete = true;dropTarget.onDragExit(mDragObject);if (dropTarget.acceptDrop(mDragObject)) {dropTarget.onDrop(mDragObject);accepted = true;}}mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted);}private void endDrag() {if (mDragging) {// 回复拖放状态值mDragging = false;clearScrollRunnable();...mDragObject.dragView.remove();...for (DragListener listener : mListeners) {listener.onDragEnd();}}releaseVelocityTracker();}// 更新界面,更新数据库Workspace.acceptDrop()--> CellLayout.createArea()--> CellLayout.commitTempPlacement()--> Workspace.updateItemLocationsInDatabase(this);

至此ShortCut的拖动就完成了,数据库的更新就不在此显示了,数据库的更新方法在Workspace.updateItemLocationsInDatabase(this)中

















0 0
原创粉丝点击