天天看点

[置顶] Android4.0 Launcher源码研究

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的数据监听

    Launcher中应用程序随时都会添加或者卸载或者更新,因此,监听系统程序安装卸载的监听器是必不可少的。查看代码发现,LauncherModel本身就是我们要找的监听器。在LauncherModel的onReceive监听中,通过Action来判断是安装,卸载,更新应用还是异常安装。通过data传递包名packageName。让后在线程PackageUpdatedTask中更新内存中数据,数据获取完毕后回调接口     

来通知UI绘制界面。

    在MTK扩展的Launcher中,还有个功能就是未读消息提醒,比如未读短信,未读电话,未读邮件都会在应用的icon上数字提醒。为了实现这个功能,所以MTK添加了MTKUnreadLoader这个广播接收器来监听未读信息的数据。此处依然使用接口回调,接口定义如下:

    因为这个功能是新添加的,所以系统没有发送未读消息的广播。因此MTK添加了

Intent.MTK_ACTION_UNREAD_CHANGED这个Anction。在短消息,联系人,邮件中,当收到新的消息,未接电话,新的邮件,都会发送这个广播。发送的时候会带上

Intent.MTK_EXTRA_UNREAD_NUMBER,传递未读的条数。

    支持显示未读记录的应用都保存在unread_support_shortcuts.xml配置文件中:

<?xml version="1.0" encoding="UTF-8"?>
<unreadshortcuts xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher"> 
    <shortcut
        launcher:unreadPackageName="com.android.contacts"
        launcher:unreadClassName="com.android.contacts.activities.DialtactsActivity"
        launcher:unreadType="0"
        launcher:unreadKey="com_android_contacts_mtk_unread"
 	/>
 	<shortcut
        launcher:unreadPackageName="com.android.mms"
        launcher:unreadClassName="com.android.mms.ui.BootActivity"
        launcher:unreadType="0"
        launcher:unreadKey="com_android_mms_mtk_unread"
 	/>
 	<shortcut
        launcher:unreadPackageName="com.android.email"
        launcher:unreadClassName="com.android.email.activity.Welcome"
        launcher:unreadType="0"
        launcher:unreadKey="com_android_email_mtk_unread"
 	/>
 	<shortcut
        launcher:unreadPackageName="com.android.calendar"
        launcher:unreadClassName="com.android.calendar.AllInOneActivity"
        launcher:unreadType="0"
        launcher:unreadKey="com_android_calendar_mtk_unread"
 	/>
    </unreadshortcuts>      

    通过loadUnreadSupportShortcuts()方法读取后,保存在sUnreadSupportShortcuts集合中,只有在这个集合中的应用才会去更新icon右上角的图标