一、主要文件和类
1.Launcher.java:
launcher应用的入口启动Activity。
2.DragLayer.java
从launcher的布局文件可以看出DargLayer是launcher.xml布局中的第二层布局,不过也可以将DragLayer看作是launcher内容的根视图,以为整个FrameLayout中只有这一个布局。
launcher.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3"
android:id="@+id/launcher" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/workspace_bg">
<com.android.launcher3.DragLayer android:id="@+id/drag_layer" android:layout_width="match_parent" android:layout_height="match_parent"> ...... </com.android.launcher3.DragLayer> </FrameLayout> |
DragLayer实际上也是一个抽象的界面,用来处理拖动和对事件进行初步处理然后按情况分发下去,角色是一个controller。它首先用onInterceptTouchEvent(MotionEvent)来拦截所有的touch事件,如果是长按item拖动,直接交由onTouchEvent()处理,这样就可以实现item的移动了,如果不是拖动item,就把事件传到目标view,交给目标view的事件处理函数做相应处理。如过有要对事件的特殊需求的话可以修改onInterceptTouchEvent(MotionEvent)来实现所需要的功能。
3. DragController.java:
在该文件中为Drag操作定义的一组接口DragListener和两个Drag操作相关的常量。
接口包含两个方法onDragStart()、onDragEnd()。onDragStart()是在刚开始拖动的时候被调用,onDragEnd()是在拖动完成时被调用。在launcher中典型的应用场景是DeleteZone,在长按拖动item时调用onDragStart()显示,在拖动结束的时候onDragEnd()隐藏。两个函数包括startDrag()和setDragItemInfo().startDrag()用于在拖动是传递要拖动的item的信息以及拖动的方式,setDragItemInfo()用于传递item的参数信息(包括位置以及大小)。
两个常量为DRAG_ACTION_MOVE,DRAG_ACTION_COPY来标识拖动的方式,DRAG_ACTION_MOVE为移动,表示在拖动的时候需要删除原来的item,DRAG_ACTION_COPY为复制型的拖动,表示保留被拖动的item。
4.LauncherModel.java:
在Kitkat中,主要使用Handler的方式来处理事件,封装了两个继承与Runnable的类LoaderTask和PackageUpdatedTask来作为事件在合适的时候,post到Handler中进行处理。其他的函数就是对数据库的封装,比如在删除,替换,添加程序的时候做更新数据库和UI的工作。
5.Workspace.java:
抽象的桌面。由N个celllaout组成,从CellLayout更高一级的层面上对事件的处理。
6.LauncherProvider.java:
launcher的数据库,里面存储了桌面的item的信息。在创建数据库的时候会loadFavorites(db)方法,loadFavorites()会解析xml目录下的default_workspace.xml文件,把其中的内容读出来写到数据库中,这样就完成了预置桌面应用的功能,如果需要添加或者修改桌面预置的应用位置和种类,就可以仿照该文件的内容,在相应的屏幕位置显示该应用图标。
7.CellLayout.java:
组成workspace的view, 如上图所示,CellLayout继承自viewgroup,既是一个dragSource,又是一个dropTarget,可以将它里面的item拖出去,也可以容纳拖动过来的item。在workspace_screen里面定了一些它的view参数。
8.ItemInfo.java:
对item的抽象,所有类型item的父类,item包含的属性有id(标识item的id), cellX(在横向位置上的位置,从0开始),cellY(在纵向位置上的位置,从0开始), spanX(在横向位置上所占的单位格), spanY(在纵向位置上所占的单位格),screenId(在workspace的第几屏,从0开始),itemType(item的类型,有widget,search,application等),container(item所在的容器)。
9.Folder.java:
在Kitkat中,新建一个桌面文件夹的方式为将一个应用图标拖动到另一个应用图标上,放手,就会新建一个应用文件夹,当点击新生成的图标,可以进入该文件夹的内容,可以给该文件夹命名等操作。
用户通过上述方式创建的文件夹。可以将item拖进文件夹,单击时打开文夹,点击文件夹名字,进入编辑状态,可以重命名文件夹。
10.LiveFolder.java:
系统自带的文件夹。从系统中创建出的如联系人的文件夹等。
11. DeleteDropTarget.java:
删除框。在平时是出于隐藏状态,在将item长按拖动的时候会显示出来,如果将item拖动到删除框位置时会删除item。DeleteDropTarget继承与ButtonDropTarget,而ButtonDropTarget实现了DropTarget和DragListener两个接口。
12.LauncherSettings.java:
字符串的定义。数据库项的字符串定义,另外在这里定义了container的类型,还有itemType的定义,除此还有一些特殊的widget(如search,clock的定义等)的类型定义。
定义接口ChangeLogColumns,BaseLauncherColumns,定义内部类WorkspaceScreens,Favorites。
二、主要模块
1.界面模型:
Launcher的界面的Root view是DragLayer, 它是一个FrameLayout, 在它上面workspace(应该说是celllayout)占了绝大部分的空间,celllayout的参数文件是workspace_screen.xml。workspace既是一个DropTarget又是一个DragSource,可以从AppsCustomizePagedView中拖出应用程序放在它上面,也可以把它里面的item拖走删除或者拖到bottomabar里面去。
2.Drop& Drag模型:
DragSource:可以拖动的对象来源的容器,在launcher中主要有AppsCustomizePagedView,workspace,Folder等。
DropTarget:
可以放置被拖动的对象的容器。在launcher中有Folder,workspace,等,一个View既可以是Dragsource也可以是DropTarget。主要包含以下几个接口:
acceptDrop()用来判断dropTarget是否可以接受item放置在自己里面。
onDragEnter()是item被拖动进入到一个dropTarget的时候的回调。
onDragOver()是item在上一次位置和这一次位置所处的dropTarget相同的时候的回调。
onDragExit()是item被拖出dropTarget时的回调。
onDrop()是item被放置到dropTarget时的回调。
函数的调用模式为:
DropTarget dropTarget = findDropTarget((int)x, (int)y, coordinates); if (dropTarget != null) { /** * 当这一次的 target 跟上一次相同时,根据坐标来移动item */ if (mLastDropTarget == dropTarget) { dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int)mTouchOffsetY, mDragInfo); } else { /** * 当上一次的位置跟这一次不同而且上一次的位置不为空,说明item移 *动出了,将上次的 View 根据上次的坐标重新排列,并根据当前坐标重排*当前的 */ if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int)mTouchOffsetX, (int)mTouchOffsetY, mDragInfo); } dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], (int)mTouchOffsetX, (int)mTouchOffsetY, mDragInfo);
} } else {
//如果这一次为 null ,上一次不为 null ,那么把上一次坐标位置的 cell 去掉 if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int)mTouchOffsetX, (int)mTouchOffsetY, mDragInfo);
} }
//记录上次的droptarget mLastDropTarget = dropTarget; |
3.Touch event总结:
由于launcher的事件比较多比较复杂,而Touch事件一般的处理流程如下,首先Root View,也就是我们前边说的DragLayer首先在onInterceptTouchEvent(MotionEvent)拦截所有的touch事件,然后经过一些处理后,看情况是否发给childview来处理该Touch事件。先来看看源代码,了解一下
DragLayer.java
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN) { if (handleTouchDown(ev, true)) { return true; } } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (mTouchCompleteListener != null) { mTouchCompleteListener.onTouchComplete(); } mTouchCompleteListener = null; } clearAllResizeFrames(); return mDragController.onInterceptTouchEvent(ev); } |
一上可知,无论是什么Touch事件,首先一定先接收到DOWN事件,接着会进入红色字符串所指向的方法handleTouchEvent()
private boolean handleTouchDown(MotionEvent ev, boolean intercept) { Rect hitRect = new Rect(); int x = (int) ev.getX(); int y = (int) ev.getY();
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; } } }
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; } |
判断的规则如下:
a. DOWN事件首先会传递到onInterceptTouchEvent()方法.
b. 如果该ViewGroup的onInterceptTouchEvent()在接收到DOWN事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。
c. 如果该ViewGroup的onInterceptTouchEvent()在接收到DOWN事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和DOWN事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。
d. 如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。
e. 如果最终需要处理事件的view的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。
三、几种问题的解决方式
1.将所有的应用都排列在桌面上
将所有的应用都排列在桌面是通过首先创建一个三维的boolean型全局数组来记录item的排列情况,第一维是屏数,第二维是纵向上的排列情况,第三维是横向的排列情况,如果那个位置被item所占用就标记为1,否则标记为0.在启动时把全局数组初始化为0,然后在添加的时候把相应的位置置1.凡是涉及到workspace上item的变化,比如移动、添加、删除操作时都需要维护数组,保持数组的正确性,因为在安装新程序时依据数组的状态去判断把item加到什么位置。
2.动态增加屏幕
增加屏幕的方法如下所示:
Workspace.java
public long insertNewWorkspaceScreen(long screenId, int insertIndex) { if (mWorkspaceScreens.containsKey(screenId)) { throw new RuntimeException("Screen id " + screenId + " already exists!"); }
CellLayout newScreen = (CellLayout) mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null);
newScreen.setOnLongClickListener(mLongClickListener); newScreen.setOnClickListener(mLauncher); newScreen.setSoundEffectsEnabled(false); mWorkspaceScreens.put(screenId, newScreen); mScreenOrder.add(insertIndex, screenId); addView(newScreen, insertIndex); return screenId; } |
动态增加屏幕是通过worksapce . addView (view)的方式实现。基本思路是:首先预先规定所允许的最大的屏幕数,然后在需要增加屏幕而且当前屏幕数没有超过最大屏幕数的时候通过
CellLayout newScreen = (CellLayout)mLauncher.getLayoutInflater().inflate(R.layout.workspace_screen, null); |
创建一个CellLayout实例出来,
addView(newScreen, insertIndex); |
然后通过addView ()把它加入进去。
在屏幕上的item被删除时通过从最后一屏起判断屏幕上是否有item,如果有的话保留,没有的话则删没有Item的那一屏。调用如下方法stripEmptyScreens()方法来完成上诉算法:
Workspace.java
public void stripEmptyScreens() { if (isPageMoving()) { mStripScreensOnPageStopMoving = true; return; }
int currentPage = getNextPage(); ArrayList<Long> removeScreens = new ArrayList<Long>(); for (Long id: mWorkspaceScreens.keySet()) { CellLayout cl = mWorkspaceScreens.get(id); if (id >= 0 && cl.getShortcutsAndWidgets().getChildCount() == 0) { removeScreens.add(id); } }
// We enforce at least one page to add new items to. In the case that we remove the last // such screen, we convert the last screen to the empty screen int minScreens = 1 + numCustomPages();
int pageShift = 0; for (Long id: removeScreens) { CellLayout cl = mWorkspaceScreens.get(id); mWorkspaceScreens.remove(id); mScreenOrder.remove(id);
if (getChildCount() > minScreens) { if (indexOfChild(cl) < currentPage) { pageShift++; } removeView(cl); } else { // if this is the last non-custom content screen, convert it to the empty screen mWorkspaceScreens.put(EXTRA_EMPTY_SCREEN_ID, cl); mScreenOrder.add(EXTRA_EMPTY_SCREEN_ID); } }
if (!removeScreens.isEmpty()) { // Update the model if we have changed any screens mLauncher.getModel().updateWorkspaceScreenOrder(mLauncher, mScreenOrder); }
if (pageShift >= 0) { setCurrentPage(currentPage - pageShift); } } |
3.预置桌面
a. 添加普通的应用程序快捷方式:
在../res/xml/下的default_workspace.xml文件中加入默认要放置的普通的应用程序。加入的格式为:
<favorite
launcher:packageName="..." //应用的packageName
launcher:className="..." //应用启动时的第一个activity
launcher:screen="..." //放置在第几屏(放在workspace的时候需要,从0开始,0为第一屏,1为第二屏)
launcher:x="..." //放置x方向的位置(在列中的位置)
launcher:y="..."/> //放置y方向的位置(在行中的位置)
packageName和className可以通过点击程序,然后在打印出的log中找到comp={...},例如如下信息:
comp={com.estrongs.android.taskmanager/com.estrongs.android.taskmanager.TaskManager}。其中com.estrongs.android.taskmanager为packageName, com.estrongs.android.taskmanager.TaskManager为className。
workspace的布局如下:
(0,0) |
(1,0) |
(2,0) |
(3,0) |
(4,0) |
(0,1) |
(1,1) |
(2,1) |
(3,1) |
(4,1) |
(0,2) |
(1,2) |
(2,2) |
(3,2) |
(4,2) |
b.添加widget:
在../package/apps/Launcher3/res/xml/下的default_workspace.xml文件中加入默认要放置的普通的widget应用。加入的格式为:
<widget
launcher:packageName="..." //widget的packageName
launcher:className="..." //实现 widget的 receiver 类的名称.
launcher:container="..." //放置的位置(只能为desktop)
launcher:screen="..." //放置在第几屏上
launcher:x="..." //放置的x位置
launcher:y="..." //放置的y位置
launcher:spanx="..." //在x方向上所占格数
launcher:spany="..."/> //在y方向上所占格数
例如,要在第3屏的第一行第二列放置开始放置一个x方向上占两个单位格,y方向上占两个单位格的时钟,可以加入以下代码:
<appwidget
launcher:packageName="com.android.alarmclock"
launcher:className="com.android.alarmclock.AnalogAppWidgetProvider"
launcher:container="desktop"
launcher:screen="2"
launcher:x="1"
launcher:y="0"
launcher:spanx="2"
launcher:spany="2"/>
四、xml文件
1.workspace_screen.xml
2.application.xml
Workspace的item的layout定义。