[置顶] 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右上角的图标

你可能感兴趣的:(Android4.0)