Launcher3桌面开发(5)-Launcher3 添加主题功能

主目录见:Android高级进阶知识(这是总目录索引)
Launcher3源码地址:Launcher3-master
[This tutorial was written by Ticoo]

前言

我们知道,Launcher图标的加载是在 IconCache 这个类上,协同一些工具类完成桌面图标的加载,源码里并没有主题功能的设计。所以在这里介绍一下主题设计的简单开发。

主题的构思

要添加主题功能我们得了解图标的加载,缓存等机制。还有,调研市面上的主题加载方式主要有两种方式,

1. APK包的方式,将主题资源放在Android工程上通过打包安装实现主题的替换
2. 指定格式的压缩包方式,如 .HW, .theme格式等等

在这基础之上我们可能还会有,主题的加密需求(目前也就想到这个)。

按上面的需求做出如下的设计:

graph LR

ThemeArchiveParser -->ThemeParser
ApkParser--> ThemeParser

定义接口 ThemeParser,用于解析主题压缩包和apk,让子类主题规则解析器ThemeArchiveParser 和 APK主题解析器 ApkParser 分别实现解析方法.

public interface ThemeParser<T extends ThemeEntity> {
    String TAG = ThemeParser.class.getSimpleName();
    /**
     * parse the theme archieve or apk or something else in future
     *
     * @param path
     * @return return the Theme wrap entity
     */
    T parse(String path);

}

当我们有新的规则,或者想适配市面上的某一款主题时,只要添加一个解析器来解析即可,而不用修改原本的解析去适配各种情况。
对于复杂类的创建我们有多种选择,这种情况下,我们就可以设计一个工厂模式 ThemeParseFactory 来创建特定的解析器。工厂模式是什么?出门左转就到了

然后将解析好的资源统一包装成 ThemeEntity,这个entity不外乎就是图标,壁纸,主题配置等等,大家自行设计即可,设计公司源码就不方便透露了,见谅。

解析器有了,我们就拿到资源了,在开发过程中还遇到几个坑,

1. 同一个应用有多个入口
2. 系统图标和三方图标的识别方式不一样。系统图标通常用 ic_music.png等方式命名,三方图标通常用包名来标识

由此,我们可以设计一个过滤器接口IconFilter,让子类系统图标过滤器和三方图标过滤器实现过滤方法。考虑到Launche里IconCache都是使用 ComponentKey 来标示图标,我们沿用即可。返回的String呢,是主题包里的图标命名。

public interface IconFilter {

    String TAG = IconFilter.class.getSimpleName();

    /**
     * find the proper icon key by componentKey and
     * return the icon key we cached
     *
     * @param componentKey IconCache key filter
     * @return the fileName or key from theme archive
     */
    String filter(ComponentKey componentKey);
}

同时,由于有多个过滤器,我们也不清楚什么时候用哪个?甚至以后可能会有新的过滤器,新的过滤规则,故这里设计一个装饰者 IconFilterDecorate,同样的实现 IconFilter接口。什么是装饰者,出门右转

这样有新规则,新过滤器时,加到装饰器的过滤方法里即可。这样整体的适配性就好多了

graph LR

SystemIconFilter-->IconFilter
ThirdPartyIconFilter-->IconFilter
IconFilterDecorate-->IconFilter

通常我们会有一个主题商店,在主题商店里下载主题并应用主题,倘若在主题商店里实现对launcher图标的替换,也就是操作Launcher的icon数据库,这并不是很合理。由于是不同的应用,我们可以通过AIDL跨进程通信的方式来让Launcher应用主题。

    interface IThemeServiceInterface {
        /**
        * apply the new theme.
        * @param path theme pkg file path
        * return true when apply theme success.
        */
        boolean apply(String path);
    
        /**
        *  apply the default theme of Launcher
        */
        boolean reset();
    }

AIDL接口有了,解析器有了,过滤器也有了,然后我们就要把这些东西用起来了。
我们设计一个 ThemeManger 主题管理器,供AIDL接口实现和Launcher的IconCache使用,使用的时候注意 ThemeManager实例的唯一性,避加载过多的ThemeManager造成内存浪费。

ThemeManager 可以设计如下几个方法(仅供参考):

1. Bitmap loadIcon(ComponentKey componentKey)
2  boolean apply(Context context, String path, boolean updateWorkspace) 

加载主题图标和应用主题的功能。在 loadIcon 方法里,就可以使用我们的 IconFilter过滤匹配正确的主题图标。 apply 方法里,就可以使用我们设计好的的工厂类 ThemeParseFactory,可以根据文件路径识别文件类型找到正确的解析器来解析。

除了上面内容外,图片缓存机制,如数据库,图片处理工具就不介绍了,方式很多。

主题应用到 IconCahce

在应用之前,我们要先了解下IconCache。在LauncherModel的加载桌面流程里,也会初始化所有图标的信息。在IconCache扫描到所有应用后,会开启一个线程遍历所有应用自带的图标缓存到内存和数据库里。

    /**
     * A runnable that updates invalid icons and adds missing icons in the DB for the provided
     * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the
     * worker thread doesn't get blocked.
     */
    @Thunk
    class SerializedIconUpdateTask implements Runnable {
     @Override
        public void run() {
            if (!mAppsToUpdate.isEmpty()) {
                LauncherActivityInfoCompat app = mAppsToUpdate.pop();
                String cn = app.getComponentName().flattenToString();
                ContentValues values = updateCacheAndGetContentValues(app, true);
                mIconDb.update(values,
                        IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
                        new String[]{cn, Long.toString(mUserSerial)});
                mUpdatedPackages.add(app.getComponentName().getPackageName());
                // add folder
                mUpdatedPackages.add(ThemeCache.KEY_FOLDER);
                if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
                    // No more app to update. Notify model.
                    LauncherAppState.getInstance().getModel().onPackageIconsUpdated(
                            mUpdatedPackages, mUserManager.getUserForSerialNumber(mUserSerial));
                }

                // Let it run one more time.
                scheduleNext();
            } else if (!mAppsToAdd.isEmpty()) {
                LauncherActivityInfoCompat app = mAppsToAdd.pop();
                PackageInfo info = mPkgInfoMap.get(app.getComponentName().getPackageName());
                if (info != null) {
                    synchronized (IconCache.this) {
                        addIconToDBAndMemCache(app, info, mUserSerial);
                    }
                }

                if (!mAppsToAdd.isEmpty()) {
                    scheduleNext();
                }
            }
        }
    }

调用 addIconToDBAndMemCache 方法开始缓存操作,通过 updateCacheAndGetContentValues 方法创建图标。因此,在给 entry.icon 赋值之前,优先加载我们主题里的图标, ThemeManager的loadIcon方法,没有的话在使用原本的获取机制。

 ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app,
                                                 boolean replaceExisting) {
        final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser());
        CacheEntry entry = null;
        if (!replaceExisting) {
            entry = mCache.get(key);
            // We can't reuse the entry if the high-res icon is not present.
            if (entry == null || entry.isLowResIcon || entry.icon == null) {
                entry = null;
            }
        }
        if (entry == null) {
            entry = new CacheEntry();
            Bitmap icon = mThemeMan.loadIcon(key);
            if (icon == null) {
                entry.icon = Utilities.createBadgedIconBitmap(
                        app.getIcon(mIconDpi), app.getUser(), mThemeMan.getThemeCache(), mContext);
            } else {
                entry.icon = Utilities.createBadgedIconBitmap(
                        new FastBitmapDrawable(icon), app.getUser(), mThemeMan.getThemeCache(), mContext);
            }
        }
        entry.title = app.getLabel();
        entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser());
        mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry);

        return newContentValues(entry.icon, entry.title.toString(), mActivityBgColor);
    }

此外,我们还知道IconCache读取数据库图标是在 cacheLocked 方法里。Launcher加载流程里会预加载图标,第一次读取数据库时,数据库是空的,也就是 getEntryFromDB 方法会返回false, 所以在 if 里,需要添加上我们的主题图标获取。

    private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
                                   UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) {
        ComponentKey cacheKey = new ComponentKey(componentName, user);
        CacheEntry entry = mCache.get(cacheKey);
        if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
            entry = new CacheEntry();
            mCache.put(cacheKey, entry);

            // Check the DB first.
            if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
                if (info != null) {
                    Bitmap icon = mThemeMan.loadIcon(cacheKey);
                    if (icon != null) {
                        entry.icon = Utilities.createBadgedIconBitmap(
                                new FastBitmapDrawable(icon), info.getUser(), mThemeMan.getThemeCache(), mContext);
                    } else {
                        entry.icon = Utilities.createBadgedIconBitmap(
                                info.getIcon(mIconDpi), info.getUser(), mThemeMan.getThemeCache(), mContext);
                    }

                } else {
                    ...
                }
            } else {
                // saveBitmap(mContext, entry.icon, cacheKey.componentName.getClassName());
            }
            ...
        }
        return entry;
    }

当然第二次度数据库的时候,在就能拿到数据库里的图标了,所以在 getEntryFromDB 里也要加上我们获取主题图标的方式,这样基本就满足我们对主题图标的替换了。

还有一个比较重要的,因为主题包大部分是图片资源,加载需要一定的时间和消耗内存,除了保证唯一性外,我们也要预加载主题图标的资源。熟悉Launcher的小伙伴知道,我们可以在LauncherModel的加载流程里,优先加载我们的主题资源,然后后续的工作就会自动完成了。不需要我们在加载完默认的图标后再应用我们的主题了。

码农不易,感谢阅读

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

推荐阅读更多精彩内容