Launcher3分析<一>

前言

最近需要实现一个自己的Launcher,就借机学习下原生的Launcher源码。

多个Launcher

源码里有Launcher,Launcher2,Launcher3。那它们有什么区别呢。
launcher不支持桌面小工具动画效果,launcher2添加了动画效果和3D初步效果支持。

Android 4.4 (KK)开始Launcher默认使用Launcher3,Launcher3较Launcher2 UI 有部分调整,主要包括:

  • 状态栏透明,App List 透Wallpaper;
  • 增加overview模式,可以调整workspace上页面的前后顺序;
  • 动态管理屏幕数量;
  • widget列表与app list分开显示;
  •  默认不支持预置appwidget,需要用户指定权限;       
    
  •   提供类似小米只有workspace的桌面机制 ;       
    
  •   Wallpaper 的代码全部搬移到Launcher包;
    
  •  类似Cling等细节的小变化;  
    

那我们就直接研究最新的Launcher3

Launcher3

Launcher3介绍

Launcher是开机启动的第一个应用程序,用来展示应用列表和快捷方式、小部件等。

Launcher3源码解析

AndroidManifest.xml

一些标签属性


123.png

LauncherApplication

public class LauncherApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        LauncherAppState.setApplicationContext(this);
        LauncherAppState.getInstance();
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
        LauncherAppState.getInstance().onTerminate();
    }
}

初始化LauncherAppState类,在继续看LauncherAppState类。

private LauncherAppState() {
        ...


        // set sIsScreenXLarge and mScreenDensity *before* creating icon cache
        mIsScreenLarge = isScreenLarge(sContext.getResources());
        mScreenDensity = sContext.getResources().getDisplayMetrics().density;

        mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(sContext);
        mIconCache = new IconCache(sContext);

        mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class));
        mModel = new LauncherModel(this, mIconCache, mAppFilter);

        // Register intent receivers
        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
        filter.addDataScheme("package");
        sContext.registerReceiver(mModel, filter);
        filter = new IntentFilter();
        filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
        filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
        filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
        sContext.registerReceiver(mModel, filter);
        filter = new IntentFilter();
        filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
        sContext.registerReceiver(mModel, filter);
        filter = new IntentFilter();
        filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
        sContext.registerReceiver(mModel, filter);

        // Register for changes to the favorites
        ContentResolver resolver = sContext.getContentResolver();
        resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true,
                mFavoritesObserver);
    }

初始化中读取配置,注册广播,实例化LauncherModel。Launcher2源码 LauncherModel创建是在LauncherApplication ,Launcher3移到这个类来了。

/**
 * Maintains in-memory state of the Launcher. It is expected that there should be only one
 * LauncherModel object held in a static. Also provide APIs for updating the database state
 * for the Launcher.
 */

单例,处理数据库。这个类继承BroadcastReceiver ,我们来重点看下这个类实现;

LauncherModel

private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
    static {
        sWorkerThread.start();
    }
    private static final Handler sWorker = new Handler(sWorkerThread.getLooper());

创建一个线程,用来处理
LoaderTask 加载

  • workspace icons
  • widgets
  • all apps icons
    PackageUpdatedTask 用来更新applist.
    看一下构造方法
LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) {
        final Context context = app.getContext();

        mAppsCanBeOnRemoveableStorage = Environment.isExternalStorageRemovable();
        mApp = app;
        mBgAllAppsList = new AllAppsList(iconCache, appFilter);
        mIconCache = iconCache;

        mDefaultIcon = Utilities.createIconBitmap(
                mIconCache.getFullResDefaultActivityIcon(), context);

        final Resources res = context.getResources();
        Configuration config = res.getConfiguration();
        mPreviousConfigMcc = config.mcc; // SIM卡相关
    }

传入LauncherAppState 来获取 context,WidgetPreviewLoader.CacheDb操作数据库;
IconCache类是用来缓存app图标
LauncherModle继承BroadcastReceiver 我们看下onReceive做了什么处理;

 /**
     * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
     * ACTION_PACKAGE_CHANGED.
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        ...
        if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
                || Intent.ACTION_PACKAGE_REMOVED.equals(action)
                || Intent.ACTION_PACKAGE_ADDED.equals(action)) {
            final String packageName = intent.getData().getSchemeSpecificPart();
            final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);

            int op = PackageUpdatedTask.OP_NONE;

            if (packageName == null || packageName.length() == 0) {
                // they sent us a bad intent
                return;
            }

            if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
                op = PackageUpdatedTask.OP_UPDATE;
            } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
                if (!replacing) {
                    op = PackageUpdatedTask.OP_REMOVE;
                }
                // else, we are replacing the package, so a PACKAGE_ADDED will be sent
                // later, we will update the package at this time
            } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
                if (!replacing) {
                    op = PackageUpdatedTask.OP_ADD;
                } else {
                    op = PackageUpdatedTask.OP_UPDATE;
                }
            }

            if (op != PackageUpdatedTask.OP_NONE) {
                enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName }));
            }
          //移动APP完成之后,发出的广播
        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
            // First, schedule to add these apps back in.
            String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
            enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages));
            // Then, rebind everything.
            startLoaderFromBackground();
          //正在移动APP时,发出的广播
        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
            String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
            enqueuePackageUpdated(new PackageUpdatedTask(
                        PackageUpdatedTask.OP_UNAVAILABLE, packages));
        } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { //设备当前区域设置已更改时发出的广播
            // If we have changed locale we need to clear out the labels in all apps/workspace.
            forceReload();
//设备当前设置被改变时发出的广播(包括的改变:界面语言,设备方向,等
        } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) {
             // Check if configuration change was an mcc/mnc change which would affect app resources
             // and we would need to clear out the labels in all apps/workspace. Same handling as
             // above for ACTION_LOCALE_CHANGED
             Configuration currentConfig = context.getResources().getConfiguration();
             if (mPreviousConfigMcc != currentConfig.mcc) {
                   Log.d(TAG, "Reload apps on config change. curr_mcc:"
                       + currentConfig.mcc + " prevmcc:" + mPreviousConfigMcc);
                   forceReload();
             }
             // Update previousConfig
             mPreviousConfigMcc = currentConfig.mcc;
        } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action) ||
                   SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)) {
            if (mCallbacks != null) {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.bindSearchablesChanged();
                }
            }
        }
    }

接收应用安装卸载后的广播,当接收到广播调用enqueuePackageUpdated来启动这个任务。

public void run() {
            final Context context = mApp.getContext();

            final String[] packages = mPackages;
            final int N = packages.length;
            switch (mOp) {
                case OP_ADD:
                    for (int i=0; i<N; i++) {
                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
                        mBgAllAppsList.addPackage(context, packages[i]);
                    }
                    break;
                case OP_UPDATE:
                    for (int i=0; i<N; i++) {
                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
                        mBgAllAppsList.updatePackage(context, packages[i]);
                        WidgetPreviewLoader.removePackageFromDb(
                                mApp.getWidgetPreviewCacheDb(), packages[i]);
                    }
                    break;
                case OP_REMOVE:
                case OP_UNAVAILABLE:
                    for (int i=0; i<N; i++) {
                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
                        mBgAllAppsList.removePackage(packages[i]);
                        WidgetPreviewLoader.removePackageFromDb(
                                mApp.getWidgetPreviewCacheDb(), packages[i]);
                    }
                    break;
            }
            ....

        if (added != null) {
                // Ensure that we add all the workspace applications to the db
                Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
                if (!AppsCustomizePagedView.DISABLE_ALL_APPS) {
                    addAndBindAddedApps(context, new ArrayList<ItemInfo>(), cb, added);
                } else {
                    final ArrayList<ItemInfo> addedInfos = new ArrayList<ItemInfo>(added);
                    addAndBindAddedApps(context, addedInfos, cb, added);
                }
            }
}

安装应用向AllAppsList添加应用信息;
卸载应用向AllAppsList 删除数据,并删除数据库的数据。
调用addAndBindAddedApps方法 :处理新添加的应用程序并首先将它们添加到数据库中;

callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
                                        addNotAnimated, addAnimated, allAppsApps);

回调到Launcher 主Activity中,这个callback是Launcher初始化后调用的,我们后面在介绍。
定义

public interface Callbacks {
    //如果Launcher在加载完成之前被强制暂停,那么需要通过这个回调方法通知Launcher
    //在它再次显示的时候重新执行加载过程
    public boolean setLoadOnResume();
    //获取当前屏幕序号
    public int getCurrentWorkspaceScreen();
    //启动桌面数据绑定
    public void startBinding();
    //批量绑定桌面组件:快捷方式列表,列表的开始位置,列表结束的位置,是否使用动画
    public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end,
                          boolean forceAnimateIcons);
    //批量绑定桌面页,orderedScreenIds 序列化后的桌面页列表
    public void bindScreens(ArrayList<Long> orderedScreenIds);
    public void bindAddScreens(ArrayList<Long> orderedScreenIds);
    //批量绑定文件夹,folders 文件夹映射列表
    public void bindFolders(LongArrayMap<FolderInfo> folders);
    //绑定任务完成
    public void finishBindingItems();
    //批量绑定小部件,info 需要绑定到桌面上的小部件信息
    public void bindAppWidget(LauncherAppWidgetInfo info);
    //绑定应用程序列表界面的应用程序信息,apps 需要绑定到应用程序列表中的应用程序列表
    public void bindAllApplications(ArrayList<AppInfo> apps);
    // Add folders in all app list.
    public void bindAllApplications2Folder(ArrayList<AppInfo> apps, ArrayList<ItemInfo> items);
    //批量添加组件
    public void bindAppsAdded(ArrayList<Long> newScreens,
                              ArrayList<ItemInfo> addNotAnimated,
                              ArrayList<ItemInfo> addAnimated,
                              ArrayList<AppInfo> addedApps);
    //批量更新应用程序相关的快捷方式或者入口
    public void bindAppsUpdated(ArrayList<AppInfo> apps);
    public void bindShortcutsChanged(ArrayList<ShortcutInfo> updated,
            ArrayList<ShortcutInfo> removed, UserHandleCompat user);
    public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
    public void bindRestoreItemsChange(HashSet<ItemInfo> updates);
    // 从桌面移除一些组件,当应用程序被移除或者禁用的时候调用
    public void bindComponentsRemoved(ArrayList<String> packageNames,
                    ArrayList<AppInfo> appInfos, UserHandleCompat user, int reason);
    public void bindAllPackages(WidgetsModel model);
    //全局搜索或者搜索属性更新
    public void bindSearchablesChanged();
    public boolean isAllAppsButtonRank(int rank);
    /**
     * 指示正在绑定的页面
     * @param page  桌面页序号
     */
    public void onPageBoundSynchronously(int page);
    //输出当前Launcher信息到本地文件中
    public void dumpLogsToLocalData();
}

接口都是在Launcher这个类实现的。

总结

  • LauncherModel 是一个广播接收者BroadcastReceiver,监听应用的安装、卸载、移动等事件。
  • 启动LoaderTask 初始化Launcher数据
    Over
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 226,488评论 6 524
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,466评论 3 411
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 174,084评论 0 371
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,024评论 1 305
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,882评论 6 405
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,395评论 1 318
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,539评论 3 433
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,670评论 0 282
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,194评论 1 329
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,173评论 3 352
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,302评论 1 362
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,872评论 5 354
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,581评论 3 342
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,984评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,179评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,888评论 3 385
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,306评论 2 369

推荐阅读更多精彩内容