天天看點

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右上角的圖示