Launcher是一个手机的门面,是一个程序的main函数,也是用户日常应用中使用最多的程序,因此在应用开发中非常重要。系统的Launcher源码写得相当优秀,封装了各种各样的组件,控件,还有界面的绘制,数据异步加载,都值得我们去深入学习。本人因为能力有限,时间有限,只在这里抛砖引玉,写一些初略的学习心得,大家也可以自行导入源码,好好研究研究。
一.Launcher的UI
下面是一个Launcher的基本界面元素
关于界面的实现,我们从launcher.xml入手。launcher.xml有三个文件,分别对应横屏,竖屏和平板布局,我们从竖屏入手,其他类似。
大致的简化下结构
<DragLayer>
<WorkSpace>
<CellLayout>
<CellLayout>
<CellLayout>
<CellLayout>
<CellLayout>
< /WorkSpace>
<include layout="@layout/hotseat" android:id="@+id/hotseat"/>
<include android:id="@+id/qsb_bar" layout="@layout/qsb_bar" />
<include layout="@layout/apps_customize_pane" android:id="@+id/apps_customize_pane" />
<include layout="@layout/workspace_cling" android:id="@+id/workspace_cling"/>
<include layout="@layout/folder_cling" android:id="@+id/folder_cling"/>
</ DragLayer >
这样看布局,然后对应上面的图,就比较清晰了。Launcher的root布局是一个DragLayer(可拖动的层),DragLayer里面有一个workspace,就是我们所说的idle界面,workspace默认加载了五个CellLayout,也就是我们默认五屏。然后继续往下看,有一个hotseat和一个qsb_bar,看名字就知道是最下面的快捷按钮和最上面的快速搜索栏。
后面三个布局默认都是不可见的。第一个apps_customize_pane,在点击了hotseat下面最中间那个图标后变为可见,然后加载所有的程序icon。还有两个cling,算是遮罩层,只在开机第一次启动时候加载,之后在也没有出来的机会。
现在,我们就对这几个组件依次进行初略分析:
1.DragLayer
DragLayer继承FrameLayout,并在此基础上组合了DragController实现拖放功能,DragLayer主要监听下面两个用户事件
onInterceptTouchEvent
onTouchEvent
这两个都是触摸事件,前者只存在ViewGroup里面,用来管理子控件的touch事件。当DragLayer接受到这两个事件后,会交给DragController进行处理,DragController根据是否在拖放中等信息控制控件拖放过程处理。
这里有两个接口,还有一个接口DropTarget,可以实现控件拖放的组件如WorkSpace和 Folder都实现了该接口。
2.WorkSpace
WorkSpace继承PageView,是一个可以分页显示的ViewGroup。Page View主要提供了snapToPage() 方法,可以实现页面间的滑动跳转。WorkSpace实现了DragScroller接口,在DragController处理move事件时候,调用父类snapToPage()方法实现屏幕左右切换。
WorkSpace是一个自定义布局。该布局定义了一些自己的属性。我们看launcher.xml中关于WorkSpace的定义:
<com.android.launcher2.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/qsb_bar_height_inset"
android:paddingBottom="@dimen/button_bar_height"
launcher:defaultScreen="2"
launcher:cellCountX="4"
launcher:cellCountY="4"
launcher:pageSpacing="@dimen/workspace_page_spacing"
launcher:scrollIndicatorPaddingLeft="@dimen/workspace_divider_padding_left"
launcher:scrollIndicatorPaddingRight="@dimen/workspace_divider_padding_right">
<include android:id="@+id/cell1" layout="@layout/workspace_screen" />
<include android:id="@+id/cell2" layout="@layout/workspace_screen" />
<include android:id="@+id/cell3" layout="@layout/workspace_screen" />
<include android:id="@+id/cell4" layout="@layout/workspace_screen" />
<include android:id="@+id/cell5" layout="@layout/workspace_screen" />
</com.android.launcher2.Workspace>
从这个定义中可以看到,WorkSpace是去掉快速搜索栏和HotSeat之后中间的部分(WorkSpace也是全屏的,不过设置了paddingTop和paddingBottom,所以不会和搜索栏,HotSeat重叠)。以launcher开头的属性都是自定义属性。
此处默认屏幕是第二屏(从第0屏开始)。每屏默认被划分成4*4的网格。在WorkSpace初始化的时候,如果xml中没有定义cellCountX属性和cellCountY属性,默认也是4*4,但如果是Large屏幕,如平板,会自动根据屏幕尺寸和图标尺寸计算应该是几*几。pageSpacing是屏幕内部的间距,再往下就是CellLayout相关了。
3.CellLayout
CellLayout没有实现其他接口,但是会监听down事件,在用户在屏幕上按下的时候,判断有没有点到控件,如果有,把这个控件的信息,比如行列数和高宽记录下俩,存放到CellInfo里面。
4.AppsCustomizePagedView
AppsCustomizePagedView也是一个自定义的view,父类和WorkSpace一样都是PageView,可以实现左右滑动。
点击idle界面HotSeat最中间的icon,idle界面被隐藏,AppsCustomizePagedView
在走完一个缩放动画后,被设置为可见,Launcher的状态也同时切换为State. APPS_CUSTOMIZE。菜单界面是一个TabHost组件,有TabWidget和AppsCustomizePagedView组成。TabWidget有两个item。一个用来显示所有App,一个用来显示所有widget插件。TabWidget下面的就是AppsCustomizePagedView了。
现在来看看AppsCustomizePagedView在xml中的定义:
<com.android.launcher2.AppsCustomizePagedView
android:id="@+id/apps_customize_pane_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
launcher:maxAppCellCountX="@integer/apps_customize_maxCellCountX"
launcher:maxAppCellCountY="@integer/apps_customize_maxCellCountY"
launcher:pageLayoutWidthGap="@dimen/apps_customize_pageLayoutWidthGap"
launcher:pageLayoutHeightGap="@dimen/apps_customize_pageLayoutHeightGap"
launcher:pageLayoutPaddingTop="@dimen/apps_customize_pageLayoutPaddingTop"
launcher:pageLayoutPaddingBottom="@dimen/apps_customize_pageLayoutPaddingBottom"
launcher:pageLayoutPaddingLeft="@dimen/apps_customize_pageLayoutPaddingLeft"
launcher:pageLayoutPaddingRight="@dimen/apps_customize_pageLayoutPaddingRight"
launcher:widgetCellWidthGap="@dimen/apps_customize_widget_cell_width_gap"
launcher:widgetCellHeightGap="@dimen/apps_customize_widget_cell_height_gap"
launcher:widgetCountX="@integer/apps_customize_widget_cell_count_x"
launcher:widgetCountY="@integer/apps_customize_widget_cell_count_y"
launcher:clingFocusedX="@integer/apps_customize_cling_focused_x"
launcher:clingFocusedY="@integer/apps_customize_cling_focused_y"
launcher:maxGap="@dimen/workspace_max_gap" />
同样,以launcher开头全部都是自定义属性。MaxAppCellCountX 和MaxAppCellCounY指的是所有App图标排列的最大行列数。一般设置为-1,表示无限制。pageLayoutWidthGap和pageLayoutHeightGap分别表示菜单界面与屏幕边缘的距离,一般小屏幕这里设置为-1,平板布局中,考虑到用户双手会抓在屏幕边缘,所以这里才会设置一定的边距。pageLayoutPaddingXxx指的是内填充,这个和系统的padding一样。widgetCellWithGap和widgetCellHeightGap指的是widget列表界面各个widget之间的间隔,类似系统的margin属性。widgetCountX和widgetCountY
值widget列表界面是几行几列显示。
5. HotSeat & Qsb_bar
6. Cling
Cling功能主要是在第一次进入launcher的演示界面,在第一次进入idle,第一次进入菜单,第一次使用文件夹等都会出现。Cling是个全屏的FrameLayout,定义在DragLayer的最底部,也就是处于界面的最顶层。因此,当它显示出来的时候,能遮盖住所有界面。Cling类主要封装遮罩层的一些显示逻辑和触摸逻辑,还有图片的回收。在不同的界面,或者横竖屏,Cling都能自动显示对应的布局,并拦截相应位置的触摸事件,当用户点击了之后,Cling同事也变为不可见,并释放图片资源。
二.Launcher的数据加载
Launcher中的数据提供者是LauncherProvider,它负责把Launcher的数据保存到本地数据库中。比如在idle界面哪一屏哪一行那一列有哪个icon或者widget,这些都会保存到数据库中(注意菜单界面的数据列表不会保存到数据库中,而是第一次读取后保存在内存中)。LauncherProvider在初始化的时候,新建数据库:
db.execSQL("CREATE TABLE favorites (" +
"_id INTEGER PRIMARY KEY," +
"title TEXT," +
"intent TEXT," +
"container INTEGER," +
"screen INTEGER," +
"cellX INTEGER," +
"cellY INTEGER," +
"spanX INTEGER," +
"spanY INTEGER," +
"itemType INTEGER," +
"appWidgetId INTEGER NOT NULL DEFAULT -1," +
"isShortcut INTEGER," +
"iconType INTEGER," +
"iconPackage TEXT," +
"iconResource TEXT," +
"icon BLOB," +
"uri TEXT," +
"displayMode INTEGER," +
"scene TEXT" +
");");
从这条语句我们可以大概看出数据库的表结构。当表被创建好之后,Launcher会加载一些与设置的xml文件。比如默认的每一屏的布局文件default_workspace.xml。LauncherProvider在初始化的时候在读取了default_workspace.xml的id后,执行了两个方法。
loadFavorites(db, id);
loadScene(db,id);
这两个方法,通过解析xml,把xml的配置信息,读取到了数据库中,因此,我们要修改launcher初始化屏幕图标分布,可以修改default_workspace.xml这个文件。
LauncherProvider中提供了数据的差删改查,也封装了对UI元素的插入删除等操作,例如:addAppWidget(), addUriShortcut(),addFolder()等等,这些操作都只是修改数据,不涉及UI上操作。
Launcher涉及到的数据的加载,基本都封装到LauncherModel里面。再说LauncherModel之前有个比较重要的类也要提一下,它就是
ItemInfo类,这个类其实非常简单,就是数据库中表的字段的一个映射。这样ItemInfo就作为了一个桥梁。
Launcher需要ItemInfo来确定在屏幕哪个地方布局什么icon,就从LauncherModel获取相应数据,而LauncerModel回去LauncherProvider中取Cursor数据,再转换成ItemInfo数据。
从这个也能大概看到Launcher设计中如何分层,即LauncherProvider提供原始的数据库数据,LauncherModel取到好转换为Launcher需要的数据,传给Launcher后,Launcher开始绘制界面。
因为LauncherModel中大部分数据都是异步加载,因此这里有一个很重要的接口,用来给UI回调。
public interface Callbacks {
public boolean setLoadOnResume();
public int getCurrentWorkspaceScreen();
public void startBinding();
public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end);
public void bindFolders(HashMap<Long,FolderInfo> folders);
public void finishBindingItems();
public void bindAppWidget(LauncherAppWidgetInfo info);
public void bindAllApplications(ArrayList<ApplicationInfo> apps);
public void bindAppsAdded(ArrayList<ApplicationInfo> apps);
public void bindAppsUpdated(ArrayList<ApplicationInfo> apps);
public void bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent);
public void bindPackagesUpdated();
public boolean isAllAppsVisible();
public void bindSearchablesChanged();
public void clearAndSwitchScene(String scene);
}
Launcher实现了这个接口,然后通过
public void initialize(Callbacks callbacks) {
synchronized (mLock) {
mCallbacks = new WeakReference<Callbacks>(callbacks);
}
}
传给LauncherModel,LauncherModel在通过异步线程加载完数据后,触发Launcher中的回调函数执行,绘制界面。
关于界面元素的绑定基本都在LoadTask这个线程里面。
三.Launcher的数据监听
在MTK扩展的Launcher中,还有个功能就是未读消息提醒,比如未读短信,未读电话,未读邮件都会在应用的icon上数字提醒。为了实现这个功能,所以MTK添加了MTKUnreadLoader这个广播接收器来监听未读信息的数据。此处依然使用接口回调,接口定义如下:
通过loadUnreadSupportShortcuts()方法读取后,保存在sUnreadSupportShortcuts集合中,只有在这个集合中的应用才会去更新icon右上角的图标