Launcher2之拖拽事件

Android  源码分析  2018年7月25日 am9:03发布6年前 (2018)更新 城堡大人
56 0 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桌面点击&长按&拖动事件处理流程分析》,这里分析得也挺全的。

 历史上的今天

  1. 2023: JNI之函数介绍四之数组操作(0条评论)
  2. 2022: Android进程间通信方式Messenger的简单记录(0条评论)
  3. 2021: 余光中:绝色(0条评论)
  4. 2019: 龙应台:中国人,你为什么不生气?(0条评论)
版权声明 1、 本站名称: 笔友城堡
2、 本站网址: https://www.biumall.com/
3、 本站部分文章来源于网络,仅供学习与参考,如有侵权,请留言

暂无评论

暂无评论...

随机推荐

蒙田:热爱生命

我对某些词语赋予特殊的含义,拿“度日”来说吧,天色不佳,令人不快的时候,我将“度日”看成是“消磨光阴”。而风和日丽的时候,我却不愿意去“度”,这时候我是在慢慢赏玩,领略美好的时光。坏日子,要飞快“度”过去!好日子,要停下来细细品尝。“度日”和“消磨时光”的常用语令人想起那些“哲人”的习气。他们以为...

泰戈尔:你一定要走吗?

旅人,你一定要走吗?夜是静谧的,黑暗昏睡在树林上。露台上灯火辉煌,繁花朵朵鲜丽,年轻的眼睛也还是清醒的。旅人,你一定要走吗?我们不曾以恳求的手臂束缚你的双足,你的门是开着的,你的马上了鞍子站在门口。如果我们设法挡住你的去路,那也不过是用我们的歌声罢了,如果我们曾设法挡住你,那也不过是用...

海子:七月不远

七月不远性别的诞生不远爱情不远————马鼻子下湖泊含盐因此青海湖不远湖畔一捆捆蜂箱使我显得凄凄迷人青草开满鲜花。青海湖上我的孤独如天堂的马匹(因此 天堂的马匹不远)我就是那个情种:诗中吟唱的野花天堂的马肚子里唯一含毒的野花(青海湖 请熄灭我的爱情!)野花青梗不远医...

去除USB权限效验弹框

修改路径:frameworks/base/core/res/res/values/config.xml 修改内容:<bool name="config_disableUsbPermissionDialogs">true</bool>

Android ACTION_MEDIA_BUTTON的监听

前言这里记录一下普通应用监听MediaButton的使用。正文直接上代码,下面代码是测试过的。当获取到焦点时,调用registerMediaButton,丢失焦点时unRegisterMediaButton隐藏内容!付费阅读后才能查看!¥3多个隐藏块只需支付一次付费阅读参考文章《...

常见的文件头或文件尾十六进制表示

前言最近在加载图片时,由于需要对不同图片使用不同的加载方式,因此需要通过判断图片的类型进行条用不同的接口。因此摘抄于此,以便查阅。正文下面的文件头或文件尾都是用十六进制表示的。JPEG (jpg)文件头:FFD8FF文件尾:FFD9PNG (png)文件头:89504E47文件尾...