主题包---根据思路代码实现

kotlin vs java

主题包---源码解析,思路分析
SkinDemo

根据上文中所分析的思路,我们来具体实现:

  1. 首先,创建我们的activity,并且重写Factory2的方法。
  2. 然后,自定义我们的CustomAppcompatInflater继承AppcompatInflater。本来我们应该接下来重写他的onCreate()方法的,但是他是final,所以我们就自定义一个方法,功能和它一致就可以了,因为最后我们需要的是创建具体的控件的子类,所以这对我们没什么影响。
  3. 最后,创建我们具体需要改变颜色的控件CustomButton继承MaterialButton,上文也说了,一定注意继承的是该控件的最终子类,否者不会支持MaterialButton
  4. 该创建类就三个,有这三个类我们就可以实现加载布局过程中拦截Button的属性进行自定义操作。
  5. 接下来我们仿照系统兼容包处理方法,在acitivity的重写Factory2方法中创建自定义的CustomAppcompatInflater对象,然后返回自定义的方法,创建view,自定义方法中拿着获取到的控件nameattributes创建出我们的自定义控件,接下来上代码:
attrs.xml
 <!-- Button控件继承TextView,此处parent语法通过,但无效果,不像style.xml -->
    <declare-styleable name="CustomButton">
        <attr name="android:background" />
        <attr name="android:textColor" />
        <!-- 字体属性 -->
    </declare-styleable>

 @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if (openChangeSkin() && !ignoreView(name)) {
            if (viewInflater == null) {
                viewInflater = new CustomAppCompatViewInflater(context);
            }
            viewInflater.setName(name);
            viewInflater.setAttrs(attrs);
            return viewInflater.autoMatch();
        }
        return super.onCreateView(parent, name, context, attrs);
    }


/**
 * 自定义控件加载器(可以考虑该类不被继承)
 */
public final class CustomAppCompatViewInflater extends AppCompatViewInflater {

    private String name; // 控件名
    private Context context; // 上下文
    private AttributeSet attrs; // 某控件对应所有属性

    public CustomAppCompatViewInflater(@NonNull Context context) {
        this.context = context;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAttrs(AttributeSet attrs) {
        this.attrs = attrs;
    }

    /**
     * @return 自动匹配控件名,并初始化控件对象
     */
    public View autoMatch() {
        View view = null;
        switch (name) {
            case BUTTON:
                view = new CustomButton(context, attrs);
                this.verifyNotNull(view, name);
                break;
        }

        return view;
    }

    /**
     * 校验控件不为空(源码方法,由于private修饰,只能复制过来了。为了代码健壮,可有可无)
     *
     * @param view 被校验控件,如:AppCompatTextView extends TextView(v7兼容包,兼容是重点!!!)
     * @param name 控件名,如:"ImageView"
     */
    private void verifyNotNull(View view, String name) {
        if (view == null) {
            throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
        }
    }
}

/**
 * 继承TextView兼容包,9.0源码中也是如此
 * 参考:AppCompatViewInflater.java
 * 86行 + 138行 + 206行
 */
public class CustomButton extends MaterialButton implements ViewsMatch {

    private AttrsBean attrsBean;

    public CustomButton(Context context) {
        this(context, null);
    }

    public CustomButton(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.buttonStyle);
    }

    public CustomButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        attrsBean = new AttrsBean();

        // 根据自定义属性,匹配控件属性的类型集合,如:background + textColor
        TypedArray typedArray = context.obtainStyledAttributes(attrs,
                R.styleable.CustomButton,
                defStyleAttr, 0);
        // 存储到临时JavaBean对象
        attrsBean.saveViewResource(typedArray, R.styleable.CustomButton);
        // 这一句回收非常重要!obtainStyledAttributes()有语法提示!!
        typedArray.recycle();
    }

    @Override
    public void skinnableView() {
        // 根据自定义属性,获取styleable中的background属性
        int key = R.styleable.CustomButton[R.styleable.CustomButton_android_background];
      
        // 根据自定义属性,获取styleable中的textColor属性
        key = R.styleable.CustomButton[R.styleable.CustomButton_android_textColor];
        int textColorResourceId = attrsBean.getViewResource(key);
        if (textColorResourceId > 0) {
            if (SkinManager.getInstance().isDefaultSkin()) {
                ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
                setTextColor(color);
            } else {
                ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
                setTextColor(color);
            }
        }
    }
}

以上代码,在每次主题切换的时候遍历控件调用skinnableView()方法,该方法中判断是否存在主题资源文件,根据资源id获取宿主或者主题包里面的对应颜色值设置颜色。


加载主题包

直接上代码,这个个人觉得没太大必要细讲,一看就能懂,而且代码都注释很清楚,值得注意的是里面三个位置的TODO注释:


public class SkinManager {

    private static SkinManager instance;
    private Application application;
    private Resources appResources; // 用于加载app内置资源
    private Resources skinResources; // 用于加载皮肤包资源
    private String skinPackageName = ""; // 皮肤包资源所在包名(注:皮肤包不在app内,也不限包名)
    private boolean isDefaultSkin = true; // 应用默认皮肤(app内置)
    private static final String ADD_ASSET_PATH = "addAssetPath"; // 方法名
    private Map<String, SkinCache> cacheSkin;

    private SkinManager(Application application) {
        this.application = application;
        appResources = application.getResources();
        cacheSkin = new HashMap<>();
    }

    /**
     * 单例方法,目的是初始化app内置资源(越早越好,用户的操作可能是:换肤后的第2次冷启动)
     */
    public static void init(Application application) {
        if (instance == null) {
            synchronized (SkinManager.class) {
                if (instance == null) {
                    instance = new SkinManager(application);
                }
            }
        }
    }

    public static SkinManager getInstance() {
        return instance;
    }

    /**
     * 加载皮肤包资源
     *
     * @param skinPath 皮肤包路径,为空则加载app内置资源
     */
    public void loaderSkinResources(String skinPath) {
        // 优化:如果没有皮肤包或者没做换肤动作,方法不执行直接返回!
        if (TextUtils.isEmpty(skinPath)) {
            isDefaultSkin = true;
            return;
        }

        // 优化:app冷启动、热启动可以取缓存对象
        if (cacheSkin.containsKey(skinPath)) {
            isDefaultSkin = false;
            SkinCache skinCache = cacheSkin.get(skinPath);
            if (null != skinCache) {
                skinResources = skinCache.getSkinResources();
                skinPackageName = skinCache.getSkinPackageName();
                return;
            }
        }

        try {
            // 创建资源管理器(此处不能用:application.getAssets())
            AssetManager assetManager = AssetManager.class.newInstance();
            // 由于AssetManager中的addAssetPath和setApkAssets方法都被@hide,目前只能通过反射去执行方法
            Method addAssetPath = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
            // 设置私有方法可访问
            addAssetPath.setAccessible(true);
            // 执行addAssetPath方法
            addAssetPath.invoke(assetManager, skinPath);
            //==============================================================================
            // 如果还是担心@hide限制,可以反射addAssetPathInternal()方法,参考源码366行 + 387行
            //==============================================================================

            // 创建加载外部的皮肤包(net163.skin)文件Resources(注:依然是本应用加载)
            skinResources = new Resources(assetManager,
                    appResources.getDisplayMetrics(), appResources.getConfiguration());

            // 根据apk文件路径(皮肤包也是apk文件),获取该应用的包名。兼容5.0 - 9.0(亲测)
            skinPackageName = application.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;

            // 无法获取皮肤包应用的包名,则加载app内置资源
            isDefaultSkin = TextUtils.isEmpty(skinPackageName);
            if (!isDefaultSkin) {
                cacheSkin.put(skinPath, new SkinCache(skinResources, skinPackageName));
            }

            Log.e("skinPackageName >>> ", skinPackageName);

        } catch (Exception e) {
            e.printStackTrace();
            // 发生异常,预判:通过skinPath获取skinPacakageName失败!
            isDefaultSkin = true;
        }
    }

    /**
     * 参考:resources.arsc资源映射表
     * 通过ID值获取资源 Name 和 Type
     *
     * @param resourceId 资源ID值
     * @return 如果没有皮肤包则加载app内置资源ID,反之加载皮肤包指定资源ID
     */
    private int getSkinResourceIds(int resourceId) {
        // 优化:如果没有皮肤包或者没做换肤动作,直接返回app内置资源!
        if (isDefaultSkin) return resourceId;

        // 使用app内置资源加载,是因为内置资源与皮肤包资源一一对应(“netease_bg”, “drawable”)
        String resourceName = appResources.getResourceEntryName(resourceId);
        String resourceType = appResources.getResourceTypeName(resourceId);

        // 动态获取皮肤包内的指定资源ID
        // getResources().getIdentifier(“netease_bg”, “drawable”, “com.netease.skin.packages”);
        int skinResourceId = skinResources.getIdentifier(resourceName, resourceType, skinPackageName);

        // 源码1924行:(0 is not a valid resource ID.)
        // TODO: 2020/8/5 此处有问题,当我主题包中没有该资源的时候会导致isDefault变成true,这就导致了遍历view的时候无法改变主题,然而这里skinResourceId == 0是对每一个资源的判断不能代表整个资源包
//        isDefaultSkin = skinResourceId == 0;
        // TODO: 2020/8/5 此处直接返回资源包获取的id值,交给下一步操作判断当前id应该是获取的宿主的还是资源包的
        return skinResourceId;
    }

    public boolean isDefaultSkin() {
        return isDefaultSkin;
    }

    //==============================================================================================
    // TODO: 2020/8/5 此处根据获取的资源包id来判断是该加载宿主还是资源包
    public int getColor(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return (ids == 0 || ids == resourceId) ? appResources.getColor(resourceId) : skinResources.getColor(ids);
    }

    public ColorStateList getColorStateList(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return (ids == 0 || ids == resourceId) ? appResources.getColorStateList(resourceId) : skinResources.getColorStateList(ids);
    }

    // mipmap和drawable统一用法(待测)
    public Drawable getDrawableOrMipMap(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return (ids == 0 || ids == resourceId) ? appResources.getDrawable(resourceId) : skinResources.getDrawable(ids);
    }

    public String getString(int resourceId) {
        int ids = getSkinResourceIds(resourceId);
        return (ids == 0 || ids == resourceId) ? appResources.getString(resourceId) : skinResources.getString(ids);
    }

    // 返回值特殊情况:可能是color / drawable / mipmap
    public Object getBackgroundOrSrc(int resourceId) {
        // 需要获取当前属性的类型名Resources.getResourceTypeName(resourceId)再判断
        String resourceTypeName = appResources.getResourceTypeName(resourceId);

        switch (resourceTypeName) {
            case "color":
                return getColor(resourceId);

            case "mipmap": // drawable / mipmap
            case "drawable":
                return getDrawableOrMipMap(resourceId);
        }
        return null;
    }

    // 获得字体
    public Typeface getTypeface(int resourceId) {
        // 通过资源ID获取资源path,参考:resources.arsc资源映射表
        String skinTypefacePath = getString(resourceId);
        // 路径为空,使用系统默认字体
        if (TextUtils.isEmpty(skinTypefacePath)) return Typeface.DEFAULT;
        return isDefaultSkin ? Typeface.createFromAsset(appResources.getAssets(), skinTypefacePath)
                : Typeface.createFromAsset(skinResources.getAssets(), skinTypefacePath);
    }
}

总结:两篇文章,大致分析了整个实现过程,最后总结梳理一下,首先是我们实现主体更好的切入点是Factory接口,整个接口是专门拦截控件的,然后仿造系统兼容包的实现自定义Inflater,创建我们的兼容对象,通过我们兼容对象来自定义颜色等属性,这里的颜色获取就是通过宿主或者主题资源包来获得,主题资源包我们通过资源加载的方式通过宿主的上下文加载资源包的资源,获取对应的属性值,因为我们资源包的资源定义名称和宿主保持一致。

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