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

本文详细探讨了Android Launcher3中视图拖动的实现,从View和ViewGroup的触摸事件分发机制开始,包括dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent的流程。接着介绍了在Launcher中如何处理长按事件,通过startDrag()、DragController和DragLayer来实现控件的拖动。最后,指出DragController.onInterceptTouchEvent()在拖动过程中的关键作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

研究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,否则未false
				mPrivateFlags |= 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 checks
					removeLongPressCallback();
					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 down
				final 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、FolderIcon
			if (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) {
	// 创建被拖动时的Bitmap
	final 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()即可移动view
	mDragObject.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 touch
			mMotionDownX = 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 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;
		}
		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)中

















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值