Android 6.0 Launcher2源码分析之拖拽(触摸)事件的简单流程

转载 极客导航  2018-07-25 09:03  阅读 59 次 评论 0 条
摘要:


在分析Launcher2的拖拽(触摸)事件之前,我们必须知道Android中事件的分发、拦截和处理机制。

有兴趣的可以看看《Android触摸事件简单分析》。不过,我这里再次简单总结一下:

1、事件一定是先到达父控件上。

2、事件简单来说可以分为三种:Down事件、Move事件、Up事件。

3、ViewGroup中才有事件的拦截方法( onInterceptTouchEvent() ),View中是没有的。

好了,我们原归正传,这里是分析Launcher2的拖拽(触摸)事件简单流程。

我用的Android源码是Android 6.0 的Launcher2,虽然各种版本(Launcher2)有些不同,但流程还是相似处理的。

我们先看看Launcher.java 中的xml布局:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:launcher="http://schemas.android.com/apk/res/com.la.launcher"
    android:id="@+id/launcher"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/workspace_bg" >

    <com.android.launcher2.DragLayer
        android:id="@+id/drag_layer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true" >

        <!-- The workspace contains 5 screens of cells -->

        <com.android.launcher2.Workspace
            android:id="@+id/workspace"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingBottom="@dimen/workspace_bottom_padding"
            android:paddingEnd="@dimen/workspace_right_padding"
            android:paddingStart="@dimen/workspace_left_padding"
            android:paddingTop="@dimen/workspace_top_padding"
            launcher:cellCountX="@integer/cell_count_x"
            launcher:cellCountY="@integer/cell_count_y"
            launcher:defaultScreen="0"
            launcher:pageSpacing="@dimen/workspace_page_spacing"
            launcher:scrollIndicatorPaddingLeft="@dimen/qsb_bar_height"
            launcher:scrollIndicatorPaddingRight="@dimen/button_bar_height" >

            <include
                android:id="@+id/cell1"
                layout="@layout/workspace_screen" />
               
               ......
        </com.android.launcher2.Workspace>

        ......

        <!-- hotseat区域 -->

        <include
            android:id="@+id/hotseat"
            android:layout_width="@dimen/button_bar_height_plus_padding"
            android:layout_height="match_parent"
            android:layout_gravity="end"
            layout="@layout/hotseat"
            android:visibility="gone" />

        ......

    </com.android.launcher2.DragLayer>

</FrameLayout>

在上面布局中,有依次有如下关系图(只显示部分控件)

//布局结构分布关系图
FrameLayout
   DragLayer
       Workspace
           CellLayout
       Hotseat
           CellLayout

从上面布局结构图和文章开头的总结,我们可以知道触摸事件一定是先出现在FrameLayout,然后传给DragLayer或Hotseat,再传给它们子类。

这里以点击Launcher界面的快捷键图标为例子讲解

1、DragLayer

DragLayer是一个自定义的布局,继承于FrameLayout

       public class DragLayer extends FrameLayout implements
		ViewGroup.OnHierarchyChangeListener {
           ......
        }

额,DragLayer是个ViewGroup,在DragLayer中只实现了onInterceptTouchEvent拦截和onTouchEvent和处理方法。这里是重点

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                         【DOWN事件时调用了handleTouchDown,如果为true时,进行拦截,不在往下分发】
			if (handleTouchDown(ev, true)) {
				return true;
			}
		}
		clearAllResizeFrames();
                【这里是调用了mDragController的拦截事件,返回true,表示进行拦截,会执行onTouchEvent方法】
		return mDragController.onInterceptTouchEvent(ev);
	}

DragLayer.handleTouchDown()

	private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
		Rect hitRect = new Rect();
		int x = (int) ev.getX();
		int y = (int) ev.getY();

		//app widget 【如果点击是是widget控件,就返回true】
		for (AppWidgetResizeFrame child : mResizeFrames) {
			child.getHitRect(hitRect);
			if (hitRect.contains(x, y)) {
				if (child.beginResizeIfPointInRegion(x - child.getLeft(), y
						- child.getTop())) {
					mCurrentResizeFrame = child;
					mXDown = x;
					mYDown = y;
					requestDisallowInterceptTouchEvent(true);
					return true;
				}
			}
		}

		//app folder 【如果是folder,打开或者关闭folder】
		Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
		if (currentFolder != null && !mLauncher.isFolderClingVisible()
				&& intercept) {
			if (currentFolder.isEditingName()) {
				if (!isEventOverFolderTextRegion(currentFolder, ev)) {
					currentFolder.dismissEditingName();
					return true;
				}
			}

			getDescendantRectRelativeToSelf(currentFolder, hitRect);
			if (!isEventOverFolder(currentFolder, ev)) {
				mLauncher.closeFolder();
				return true;
			}
		}
		return false;【默认是返回false】
	}

在DragLayer中实现了触摸处理事件(虽然现在不涉及,但提前放在这里分析),如果DragLayer对事件进行了拦截,就会跑到这里。当然,如果由子布局不处理,最后也会上报到这里的。

DragLayer.onTouchEvent()

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		boolean handled = false;
		int action = ev.getAction();

		int x = (int) ev.getX();
		int y = (int) ev.getY();

		if (ev.getAction() == MotionEvent.ACTION_DOWN) {
			if (ev.getAction() == MotionEvent.ACTION_DOWN) {
				if (handleTouchDown(ev, false)) {【这里又调用了handleTouchDown】
					return true;
				}
			}
		}

		if (mCurrentResizeFrame != null) {【mCurrentResizeFrame在handleTouchDown中如果点击的是widget时候进行赋值了】
			handled = true;
			switch (action) {
			case MotionEvent.ACTION_MOVE:
				mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y
						- mYDown);
				break;
			case MotionEvent.ACTION_CANCEL:
			case MotionEvent.ACTION_UP:
				mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y
						- mYDown);
				mCurrentResizeFrame.onTouchUp();
				mCurrentResizeFrame = null;
			}
		}
		if (handled)【如果有处理过事件了,就直接拦截】
			return true;
		return mDragController.onTouchEvent(ev);【调用了mDragController的处理事件,如果返回true,表示已经处理,不再上报】
	}

从上面看出,DragLayer对事件拦不了拦截还要看DragController的onInterceptTouchEvent()返回值。我们看看DragController

2、DragController

DragController只是一个单独的类

     public class DragController {
            ......
      }

虽然不是ViewGroup或View,但是新建了onInterceptTouchEvent和onTouchEvent方法,并对事件进行处理

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        @SuppressWarnings("all") // suppress dead code warning
        final boolean debug = false;

        // Update the velocity tracker
        acquireVelocityTrackerAndAddMovement(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) {【mDragging标签是判断是否有拖拽动作】
                    PointF vec = isFlingingToDelete(mDragObject.dragSource);
                    if (vec != null) {
                        dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
                    } else {
                        drop(dragLayerX, dragLayerY);
                    }
                }
                endDrag();
                break;
            case MotionEvent.ACTION_CANCEL:
                cancelDrag();
                break;
        }
        【如果是拖拽事件,返回true;否则返回false,至于是否是拖拽事件要看是否调用了DragController.startDrag()】
        return mDragging;【如果是点击事件,这里返回false】
    }

    /**
     * Call this from a drag source view.
     */
    public boolean onTouchEvent(MotionEvent ev) {
        if (!mDragging) {【如果不是拖拽事件,直接返回,不往下执行】
            return false;
        }

        // Update the velocity tracker
        acquireVelocityTrackerAndAddMovement(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;
            }
            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 (vec != null) {
                    dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
                } else {
                    drop(dragLayerX, dragLayerY);
                }
            }
            endDrag();
            break;
        case MotionEvent.ACTION_CANCEL:
            mHandler.removeCallbacks(mScrollRunnable);
            cancelDrag();
            break;
        }
        return true;
    }

如果DragController在onInterceptTouchEvent()返回true,表示DragLayer对事件拦截,就不会往下传递。我们这里分析点击快捷键图标

上面mDragging是是否拖拽标签,这个是在DragController.startDrag()方法中设置为true的

    public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,
            DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
            float initialDragViewScale) {
        ......

        for (DragListener listener : mListeners) {
            【这里做了一些在拖拽时监听,比如拖拽删除快捷键图标时删除图标(垃圾篓)的显示等】
            listener.onDragStart(source, dragInfo, dragAction);
        }

        ......

        mDragging = true;【这里就标志了拖拽事件的开始】

        ......

        handleMoveEvent(mMotionDownX, mMotionDownY);【这里会调用一次 handleMoveEvent 方法,在拖拽时这个handleMoveEvent方法一直会调用,看上面MotionEvent.ACTION_MOVE】
    }

如果不是拖拽事件,也就是在DragController返回false,DragLayer不拦截,把事件分发给子布局Workspace或Hotseat等。我这分析Workspace

3、Workspace & PagedView

     public class Workspace extends SmoothPagedView  {
      ......
      }

     public abstract class SmoothPagedView extends PagedView {
         ......
     }

     public abstract class PagedView extends ViewGroup{
         ......
     }

额额,简单的说Workspace间接继承ViewGroup,不过这里只实现了onInterceptTouchEvent方法

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		switch (ev.getAction() & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_DOWN:【DOWN事件】
			mXDown = ev.getX();
			mYDown = ev.getY();
			break;
		case MotionEvent.ACTION_POINTER_UP:【UP事件】
		case MotionEvent.ACTION_UP:
			if (mTouchState == TOUCH_STATE_REST) {【如果UP事件来时,同时状态是TOUCH_STATE_REST,就走这里】
				final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage);
				if (!currentPage.lastDownOnOccupiedCell()) {【down时是否是点击到了快捷键图标,如是,lastDownOnOccupiedCell()返回true,否则false】
					onWallpaperTap(ev);
				}
			}
		}
		return super.onInterceptTouchEvent(ev);【拦不拦截要看其父类(或祖父),PagedView 中实现了onInterceptTouchEvent方法】
	}

Workspace 中拦不拦截要看其祖父PagedView 中实现的onInterceptTouchEvent方法

4、PagedView.onInterceptTouchEvent()

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        acquireVelocityTrackerAndAddMovement(ev);

        if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev);

        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) &&
                (mTouchState == TOUCH_STATE_SCROLLING)) {
            return true;
        }

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE: {【MOVE事件】
                if (mActivePointerId != INVALID_POINTER) {
                    determineScrollingStart(ev);
                    break;
                }
            }

            case MotionEvent.ACTION_DOWN: {【down事件】
                final float x = ev.getX();
                final float y = ev.getY();
                // Remember location of down touch
                mDownMotionX = x;
                mLastMotionX = x;
                mLastMotionY = y;
                mLastMotionXRemainder = 0;
                mTotalMotionX = 0;
                mActivePointerId = ev.getPointerId(0);
                mAllowLongPress = true;
                final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX());
                final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop);
                if (finishedScrolling) {
                    mTouchState = TOUCH_STATE_REST;
                    mScroller.abortAnimation();
                } else {
                    mTouchState = TOUCH_STATE_SCROLLING;
                }

                if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) {
                    if (getChildCount() > 0) {
                        if (hitsPreviousPage(x, y)) {
                            mTouchState = TOUCH_STATE_PREV_PAGE;
                        } else if (hitsNextPage(x, y)) {
                            mTouchState = TOUCH_STATE_NEXT_PAGE;
                        }
                    }
                }
                break;
            }

            case MotionEvent.ACTION_UP:【UP事件】
            case MotionEvent.ACTION_CANCEL:
                mTouchState = TOUCH_STATE_REST;
                mAllowLongPress = false;
                mActivePointerId = INVALID_POINTER;
                releaseVelocityTracker();
                break;

            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                releaseVelocityTracker();
                break;
        }

        return mTouchState != TOUCH_STATE_REST;【如果mTouchState != TOUCH_STATE_REST为true时,表示进行拦截】
    }

我们这里分析是点击快捷键图标,因此上面onInterceptTouchEvent()方法返回是false,因此触摸事件继续往子布局CellLayout分发

PagedView 中的 mTouchState有如下三种状态,触摸停止,触摸拖动,向前一页滑动、向后一页滑动

    //mTouchState有三种状态,如下
    protected final static int TOUCH_STATE_REST = 0;
    protected final static int TOUCH_STATE_SCROLLING = 1;
    protected final static int TOUCH_STATE_PREV_PAGE = 2;
    protected final static int TOUCH_STATE_NEXT_PAGE = 3;

5、CellLayout

我们看CellLayout的onInterceptTouchEvent方法

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		final int action = ev.getAction();

		if (action == MotionEvent.ACTION_DOWN) {【按下down时走这里】
			clearTagCellInfo(); 【清除】
		}

		if (mInterceptTouchListener != null
				&& mInterceptTouchListener.onTouch(this, ev)) {
			return true;
		}

		if (action == MotionEvent.ACTION_DOWN) { 【按下down时走这里】
			setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY());
		}
		return false;
	}

CellLayout.setTagToCellInfoForPoint()

	public void setTagToCellInfoForPoint(int touchX, int touchY) {
		final CellInfo cellInfo = mCellInfo;
		Rect frame = mRect;
		final int x = touchX + getScrollX();
		final int y = touchY + getScrollY();
		final int count = mShortcutsAndWidgets.getChildCount();

		boolean found = false;【默认初始化为false】
		for (int i = count - 1; i >= 0; i--) {
			final View child = mShortcutsAndWidgets.getChildAt(i);
			final LayoutParams lp = (LayoutParams) child.getLayoutParams();

			if ((child.getVisibility() == VISIBLE || child.getAnimation() != null)
					&& lp.isLockedToGrid) {
				child.getHitRect(frame);

				float scale = child.getScaleX();
				frame = new Rect(child.getLeft(), child.getTop(),
						child.getRight(), child.getBottom());
				frame.offset(getPaddingLeft(), getPaddingTop());
				frame.inset((int) (frame.width() * (1f - scale) / 2),
						(int) (frame.height() * (1f - scale) / 2));

				if (frame.contains(x, y)) {
					cellInfo.cell = child;
					cellInfo.cellX = lp.cellX;
					cellInfo.cellY = lp.cellY;
					cellInfo.spanX = lp.cellHSpan;
					cellInfo.spanY = lp.cellVSpan;
					found = true; 【点击在快捷键图标上,置为true】
					break;
				}
			}
		}
                【赋值。mLastDownOnOccupiedCell 保存down时获取的状态,这个会在Workspace的UP事件中调用】
		mLastDownOnOccupiedCell = found;

		if (!found) {
			final int cellXY[] = mTmpXY;
			pointToCellExact(x, y, cellXY);

			cellInfo.cell = null;
			cellInfo.cellX = cellXY[0];
			cellInfo.cellY = cellXY[1];
			cellInfo.spanX = 1;
			cellInfo.spanY = 1;
		}
		setTag(cellInfo);
	}

到现在位置,DOWN事件被我们处理完了,接着是MOVE和UP事件。(单击事件)

UP事件和上面DOWN的流程一样,以上布局或控件都不处理,最终,处理的事件又跑回到了Launcher.java中。

6、Launcher

PS:这里说明一下,上面DOWN和UP事件的开始端是Launcher.java开始的,如果上面布局或控件都不处理,又会回到Launcher.java中的。

Launcher中没有其他消耗事件的处理,但是有快捷键图标(BubbleTextView )做了点击事件监听。

	public void onClick(View v) {
		if (v.getWindowToken() == null) {
			return;
		}
		if (!mWorkspace.isFinishedSwitchingState()) {
			return;
		}
		Object tag = v.getTag();
		if (tag instanceof ShortcutInfo) {【快捷键图标】
			final Intent intent = ((ShortcutInfo) tag).intent;
			int[] pos = new int[2];
			v.getLocationOnScreen(pos);
			intent.setSourceBounds(new Rect(pos[0], pos[1], pos[0]
					+ v.getWidth(), pos[1] + v.getHeight()));

			boolean success = startActivitySafely(v, intent, tag);

			if (success && v instanceof BubbleTextView) {
				mWaitingForResume = (BubbleTextView) v;
				mWaitingForResume.setStayPressed(true);
			}
		} else if (tag instanceof FolderInfo) {【文件夹】
			if (v instanceof FolderIcon) {
				FolderIcon fi = (FolderIcon) v;
				handleFolderClick(fi);
			}
		} else if (v == mAllAppsButton) {【hotseat中显示所有应用的按钮】
			if (isAllAppsVisible()) {
				showWorkspace(true);
			} else {
				onClickAllAppsButton(v);
			}
		}
	}

或许你会好奇,这是什么时候注册监听事件的,这个是在Launcher中有一个createShortcut()方法中注册了,而此方法在加载数据时调用,具体可以看看LauncherModel.java中的bindWorkspaceItems()方法。

	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;
	}

好了,点击事件目前就结束,不过写得不是特别清晰。如果觉得有点累,可以看看《Launcher桌面点击&长按&拖动事件处理流程分析》,这里分析得也挺全的。

历史上的今天:

本文地址: https://www.125la.com/537.html
关注我们:请关注一下我们站长微信:扫描二维码125啦读书导航的微信号,微信号:yudemi(十三少)
版权声明:本文为原创或转载文章,版权归原作者所有,欢迎分享本文,转载请保留出处!
第一个读书导航

发表评论


表情