Android 换肤

96
Rrtoyewx
2016.12.02 17:34* 字数 6206

Android 换肤

标签(空格分隔): android

资源的概括

一个apk文件,实质为为zip文件,而对于Android来说,应用的安装的过程,其实就是一个复制过程,将第三方应用apk文件复制到/data/app目录,只不过中间涉及一些权限的问题。那么就对apk进行分析

解压过后文件,


image_1b2t09h82nd91c6g1sp310gr1knb9.png-11.5kB
image_1b2t09h82nd91c6g1sp310gr1knb9.png-11.5kB

其中对于.dex 文件和AndroidManifest.xml这里不做说明,因为这里涉及资源文件的问题。

res目录下:
1,存放不能编译的文件,比如jpg,png等资源形文件
2,存放编译的文件,layout.xml,drawable.xml,color.xml文件等

resources.arsc:资源映射表
映射放在res目录下的所有文件

每个应用都有一个ResourceTable的大表对象,而ResourceTable 又拥有多个资源包对象(ResourcePackageGroup),用包名来区分,而每个资源包对象又拥有不同类型的资源文件(Resource Type),比如layout,drawable,color等,每个不同类型的资源又拥有多个Resource Config 的对象,根据Android设备的硬件信息,而每个Resource Config对象拥有着不同的Resource Entity对象,从而将res文件目录下所有文件全部映射到resources.arsc中

换肤的根本

无论是通过更改主题的方式去更改主题,还是给每个控件设置新的资源而言,其根本都是通过将每个控件资源文件换下,

理解这一点,对换肤的流程大致就有了初步的了解了,我们只需要在换肤的命令后,我们将每个需要换肤的控件重新设置新的资源文件即可,对,就是这么简单。
那么摆在前面的问题就被分解了下面2个问题

1,如何区别哪些控件需要在换肤的命令做做出响应的更改,哪些不是?
2,如何找到我们真正需要的资源文件?

问题的解决

区分需要更改换肤的控件

针对这一问题,通常的做法就是标识,用标识去标记哪些view是需要进行更换资源,常见的做法有三种

自定义接口

将需要的进行更换资源的view都去实现这个接口,然后去遍历加载完成的view,找到里面实现该接口的view,存放在list中,这样也就是MultipleTheme找寻view的方法。关键代码如下
ColorUiInterface接口

public interface ColorUiInterface {
    public View getView();

    public void setTheme(Resources.Theme themeId);
}

对于需要更换的资源的textView命名为ColorTextView

public class ColorTextView extends TextView implements ColorUiInterface {
...}

然后在自定义ColorTextView放入xml文件中

<derson.com.multipletheme.colorUi.widget.ColorTextView
    android:textColor="?attr/main_textcolor"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_world" />

当改变皮肤的操作命令下达后,调用changeTheme的方法

public static void changeTheme(View rootView, Resources.Theme theme) {
    if (rootView instanceof ColorUiInterface) {
        ((ColorUiInterface) rootView).setTheme(theme);
        if (rootView instanceof ViewGroup) {
            int count = ((ViewGroup) rootView).getChildCount();
            for (int i = 0; i < count; i++) {
                changeTheme(((ViewGroup) rootView).getChildAt(i), theme);
            }
        }
            ...
}

从上面基本查找流程可以看出来,虽然可以实现了查找到我们需要更换资源的view,但是有两个不太理想的地方,第一成本太高,需要我们将更换资源的view对应换成实现ColorUiInterface接口ColorView,第二在查找过程,都是换肤的命令下达后,才开始查找,对于较为复杂的布局来说,时间上可能存在问题,这个时候可以事先就找好所有需要更换资源的view,存放在pool中,等命令下达后。直接去这些的view进行更换资源的操作。
</br>

尽然布局,设置较为复杂,那么就用代码来手动声明吧

我们可以用代码声明那些view是需要更换资源,这些view的哪些属性是需要更换的等信息。声明过后再将这些view存放list中吧,就像Colorful那样查找view一样。
关键代码如下

Colorful mColorful = new Colorful.Builder(this)
    .backgroundDrawable(R.id.root_view, R.attr.root_view_bg)
    .backgroundColor(R.id.change_btn, R.attr.btn_bg)  
    .textColor(R.id.textview, R.attr.text_color) 
    .create();

这样成本不算太高,但是如果页面需要更换的资源的view较多,或者说view的需要更换的属性较多,按照上面的写法,基本就是一个view中一条属性就是一行代码来看,是不是有太多冗余了。另外,我是不太喜欢在代码中使用R.id.xxx的这样的写法,除非是findViewById,因为这样做,如果资源文件修改,类似的代码还需要修改。维护起来,也比较麻烦。

从根本出发,LayoutInflater去解决

简单说下LayoutInflater这个类,这个负责解析layout.xml文件,解析属性。然后生成view

生成view的主要流程如下

1,inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
调用inflate(parser, root, attachToRoot),parser解析xml文件。

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        ...
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

2,inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot),调用createViewFromTag()去创建view

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ...
    final AttributeSet attrs = Xml.asAttributeSet(parser);
    ...
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    ...
}

3,调用createViewFromTag

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
   //一大堆factory去生成view,
    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }
    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    //当一堆factory生成view失败了,或者return null,则交给createView生成
    if (view == null) {
        if (-1 == name.indexOf('.')) {
            //android系统view,调用onCreateView其实去加完整的包名,从而在createView通过反射去生成view
            view = onCreateView(parent, name, attrs);
        } else {
            //自定义的view
            view = createView(name, null, attrs);
        }
    } 
    ...
    return view;
}

4,createView
通过反射去生成view

public final View createView(String name, String prefix, AttributeSet attrs){
    ...
    Class clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
    ...             
    constructor = clazz.getConstructor(mConstructorSignature);
    constructor.setAccessible(true);
    final View view = constructor.newInstance(args);
    ...
}

上面简单说了下LayoutInflater加载view的原理,我们猜想能不能同通过LayoutInflater的去获取生成view的相关属性呢?然后在利用一个属性来作为特殊的标识呢?答案是肯定的。在XmlPullParser去解析xml的文件的时候,返回是AttributeSet对象,AttributeSet是一个属性结合,我们知道布局文件是一个xml文件,AttributeSet是当前xml文件被解析后对象,有了AttributeSet,我们就可以将读取我们那个作为标识的属性值了。好吧,现在的问题转移到如何去获取这个AttributeSet对象了,方法有两种,第一种我们去模仿系统解析,自己去生成xml文件解析对象,去解析文件,这不失为一个好的方法,但是这样似乎在性能不太好,毕竟系统已经帮我们解析好了一次,另外解析xml文件本身就是性能的工作,所以我们还是尽可能使用系统给我们已经解析好的AttributeSet的对象吧,所以有了第二种方法,LayoutInflaterFactory,我们发现在创建view的时候,总是先去调用一大堆的LayoutInflaterFactory去创建view,而创建的view的时候,会将AttributeSet的对象去带过去,实现创建,那么我们只要给
LayoutInflater随便设置一个LayoutInflaterFactory即可。好吧,现在所有的问题都有了解决的思路,下面给出这一思路的解决方式。
这种方法也就是Android_Skin_Loader的解决方式

解决方式

我们通过给每个view在xml文件设置属性用来标记该view是否为更换资源的view,例如下面TextView则被标记为需要更换资源的view了。

<TextView
    android:id="@+id/tv_item_main_list_hint_message"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:text="更改主题"
    android:textColor="@color/color_main_list_text"
    android:textSize="16sp"
    skin:enable="true"/>

生成SkinInflaterFactory对象,去生成view和解析view的属性,判断其中哪些view是我们需要更换资源的。

public class SkinInflaterFactory implements Factory {
    private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
    
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        View view = createView(context, name, attrs);
        parseSkinAttr(context, attrs, view);
        return view;
    }
}

将生成SkinInflaterFactory绑定到LayoutInflater上。这样每次去加载布局文件生成对应的view的时候,我们都可以在List<SkinItem> mSkinItems获取到关于这个页面文件所有需要更换资源的view了。
相对于前两者,这种方式改动成本较小,只需要在布局加一行属性即可,但是这种方式比较粗暴,主要问题有两个,第一,干扰了view生成,对于成功生成view的来说,实质是调用了SkinInflaterFactory的createview的方法去生成view的,我们不知道在SkinInflaterFactor生成view跟系统生成的view有那些不一样,而去随着android版本的迭代,也不利于兼容。第二,对于createview失败的view,就无法放在mSkinItems中,因为在解析过程,是将生成view一并带过去,存在skinitem中。所以可能存在创建view的创建情况,就没办法知道这个view是不是需要更换资源了,也同样的更换资源的时候,这个view也没办法去更换资源了,存在准确性问题。

好的,简单说了下前三种方法,个有个的优点和缺点,似乎说缺点优点多,但是并不是代表前人做的不好,相对而言,我觉得如果没有前任去踩的坑,我们是很难进步的。接下来,我来说下我找寻view思路,欢迎吐槽。

考虑到改动成本和稳定性两个问题,我在第三种方法上面改进了一点,设置属性,这样修改起来的成本较低,也比较符合作为一个开源库来使用原则。但是在稳定性的上来说,我巧妙利用id的这个属性,我还是通过SkinInflaterFactory去解析AttributeSet的属性,只不过return null,把生成view的操作还是交给系统去做,因为,我们只需要去解析AttributeSet的属性即可,而不是去生成view,这样就没有干涉到view的生成,从而稳定性有了保证。可能,你会问我,既然没有生成view,那么你这么知道哪些的view时需要的更换资源的?在这里我对需要更换资源资源的view有个硬性要求,就是id,我在解析AttributeSet对象的时候将id保存下来了,然后在布局加载完成后,通过这些id去相应找到对应view即可,然后在放在一个list中。是不是很机智?但是这样做有两个缺点:第一个需要更换资源的view都要有id属性,第二同一个布局文件,不允许出现两个id一样的view,这样findViewById的总是按照布局从上而下找到id的view,找到则不就找了。对于这个两个缺点,个人觉得第二个不算缺点,第一个可能算个缺点。当然其他的缺点,我暂时也没发现,希望大家能指出,关键代码如下
SkinInflaterFactory的代码

public class SkinInflaterFactory implements LayoutInflaterFactory {
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        parseSkinAttr(context, attrs, name);
        return null;
    }
    
    private void parseSkinAttr(Context context, AttributeSet attrs, String name) throws Exception {
        ...
        if (!viewAttrs.isEmpty()) {
            if (skinView.getViewId() != -1) {
                skinView.setSkinAttrList(viewAttrs);
                mSkinViewList.add(skinView);
            } else {
                SkinL.e("类型为:" + name + "并没有设定id的属性值,请设定!!!");
            }
        }
    }
}

SkinLayoutInflater:查询需要更换资源的view

 private void findSkinView(View root, List<SkinView> skinViewList) {
        View view = null;
        Iterator<SkinView> iterator = skinViewList.iterator();
        while (iterator.hasNext()) {
            SkinView skinView = iterator.next();

            int viewId = skinView.getViewId();
            view = root.findViewById(viewId);

            if (view != null) {
                skinView.setSkinView(view);
            } else {
                SkinL.d("未找到id value:" + viewId + ",请检查" + root.getClass().getSimpleName() + "xml文件");
                iterator.remove();
            }
        }
    }

寻找我们想要的资源

基本上,使用上面的方式即可区分出哪些时要更换资源的view哪些不是,现在主要的问题就剩下如何查找我们想要的资源文件了。
从文章一开头就说明了,resources.arsc中封装了apk中所有资源映射关系,如何在这张表找到我们想要的资源时我们解决应用内换肤的根本问题,既然也提到resources.arsc的只封装了本apk,那么对于从网络下载的皮肤资源包(apk文件),又该如何加载其中的资源文件呢?这是我们解决应用外换肤的难题?好在针对这一个难题,前人已经又了较为完善的解决方案。

如何找到本apk资源文件

我们知道应用在预编译的时候,会生成R.java文件和APP._ap,对于R.java 文件我们并不陌生,而app._ap是什么呢?其实app._ap在apkbuilder工具生成resources.arsc,可以理解为resources.arsc的前身。归结起来就是,R.java相当为应用的资源项建立一张索引表,而每个整形常量都对应存储在app._ap文件一个资源项,app._ap是根据资源内容编译而成的二进制文件,像一张大表收录了应用内所有资源信息,每个资源项可以资源目录的一个文件,列表型xml文件中一个资格资源项。平常我们获取某个资源文件都是通过context.getResource.get(R.xxx);可以将此过程分解为Resource对象先查询R.java,查询相应的整形常量,然后拿着这个整形常量,去app._ap中查重资源文件。那么对于我们来说,找到我们想要的资源文件,我们只需要自动资源文件在R文件对应id即可。
再了解上面所说的,那么接下来,还有一个小问题,如何在特定的主题找到我们相应的资源,那么就说主题和主题下的资源应该存在着联系。对于这个联系主要的有下面三种方式去约束
第一种方式:setTheme
这种方式是ColorfulMultipleTheme采取的方式
利用android系统自带的Theme的方式,我们可以在不同主题下放不同资源,从而使得主题和主题下的资源存在联系。至于具体资源可以通过下面的方式来获取theme主题下R.xx为mAttrResId的资源文件了。

TypedValue typedValue = new TypedValue();
theme.resolveAttribute(mAttrResId, typedValue, true);

这种方式开发成本较低,另外方便管理,使用者只需要在style文件定义主题即可

<!-- 日间主题 -->
<style name="DayTheme" parent="AppTheme">
    <item name="root_view_bg">@drawable/bg_day</item>
    <item name="btn_bg">@color/white_btn_color</item>
    <item name="text_color">@color/black_tx_color</item>
</style>

<!-- 夜间主题 -->
<style name="NightTheme" parent="AppTheme">
    <item name="root_view_bg">@drawable/bg_night</item>
    <item name="btn_bg">@color/black_btn_color</item>
    <item name="text_color">@color/white_tx_color</item>
</style>

添加和改变主题直接对这个文件进行操作即可,但是这样方式存在两个缺点,第一通过activity.setTheme的方式去更改主题,并且要重启activity才能更新主题,所以就得在setTheme后去手动刷新一次所有页面,在手动的刷新的过程,部分存在缓存view的视图并没有得到即使更新,这一点在RecyclerView,ListView尤为明显,所有通过反射去调用RecyclerView和ListView的recyler中缓存清空。对于这一点,作者都又去更改,第二,对于现在换肤也是比较致命的,就是没办法去做到加载应用外的资源。

第二种解决的方法:自我约定
既然不能像theme那样,将一套主题下的所有资源放在一起管理的话,那么就自己约定一些关系,比如在red主题下,所有的资源文件名字后面都加上“_red”,对于blue的主题下,所有的资源文件名字后面都加上“_blue”,这样我们默认主题下,有个颜色为“color_main_background”的颜色资源,那么在red的主题下,我们要查找的资源名字就为“color_main_background_red”,这样即可,最终我们通过下面的方式根据资源文件的名字去查找资源文件在R类的整形常量

//resourceName:资源文件名字
//resourceType:对应一开头说的layout,color,drawable等不同类型
//packageName:包名,android包名,apk下包名,以及aar下包名
int resourceId =resources.getIdentifier(resourceName, resourceType, packageName);

针对第二种解决方式,有较大灵活性,但是灵活性的同时带来的不方便管理,对于color等资源还容易方便管理,可以做成下面这样这样,用来区分不同主题的下的资源,但是对于drawable资源文件来说,目前还没有找到相应的解决方法,如果你有,请联系我。

image_1b2v619p73d2kbd584dgu1g8p9.png-12.7kB
image_1b2v619p73d2kbd584dgu1g8p9.png-12.7kB

第三种,接口约束
另外针对这个问题,曾今尝试过通过代码去约束主题和主题下的资源文件,就是定义ITheme接口,每个方法返回需要更换的资源,不同主题主题去实现这个接口,然后在接口各个方法去返回该主题下相应的资源。例如下面这样
ITheme类

public interface ITheme{
    ...
    int colorMainText();
    ...
}

默认主题, DefaultTheme

public class DefaultTheme implemets ITheme{
    ...
    public int colorMainText(){
        return R.color.black;
    }
    ...
}

红色主题,RedTheme

public class RedTheme implements ITheme{
    ...
    public int colorMainText(){
        return R.color.red;
    }
    ...
}

这样似乎就能很好管理好每个主题下资源,虽然可能需要写一个这样类,看起来似乎就这样的问题。但是需要更换资源文件较多的话,这个类,也比较冗长,而且在后期迭代的时候,可能某个控件不需要了,也就需要删除对应的资源以及类中对应的方法,相对而言,维护起来也不太方便,另外还有个缺点,就是加载外部apk文件的里面资源的时候,我们需要反射外部apk的对应主题.java文件,我们知道未安装的apk文件里面的类只能通过DexClassLoade去loadclass,然为了防止两个不同的apk文件里面类名命令规则是一样,而资源文件不一样的,还得需要用不同DexClassLoader去加载,不然会认为这两个类时相同的,以至于换肤不成功。这样无疑又多了对classLoader的维护。所以最终考虑我还是采用了第二种方式去实现

如何加载外部apk的资源文件

前面所说的都是在加载应用内部的资源文件,那么如何加载外部apk的资源文件呢?
我们通过context.getResource()去获取的Resource对象去管理在apk中所有投影在resources.asrc文件资源文件,而不能管理不在resources.asrc的资源资源文件,那么,我们能不能自己去生成一个resource对象,或者说能不能拿到管理外部apk所有投影在resources.asrc文件资源文件的resources对象呢?这就成了我们解决这个问题所在了。
对于第一解决方式,我们能自己去生成一个呢?答案是肯定的,要不然应用内是怎么生成的。我们可以先看下应用内怎么生成resource对象
ContextImpl.java中mResources生成过程,调用了mResourcesManager.getTopLevelResources();

private ContextImpl(ContextImpl container, ActivityThread mainThread,LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,Display display, Configuration overrideConfiguration) {
    ...
    resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,overrideConfiguration, compatInfo, activityToken);
    
    mResources = resources;
    ...
}

ResourcesManager.getTopLevelResources

public Resources getTopLevelResources(String resDir, int displayId,  Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) { 
    ...
    AssetManager assets = new AssetManager();  
    if (assets.addAssetPath(resDir) == 0) {  
        return null;  
    }  
    resource = new Resources(assets, dm, config, compatInfo, token);  
    ...
}

了解Resource生成过程,那么我们也生成我们想要的Resource对象类似下面的代码

 AssetManager assetManager = null;
 assetManager = AssetManager.class.newInstance();
 Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
 addAssetPath.invoke(assetManager, file.getAbsolutePath());
 Resources superRes = mContext.getResources();
 mResources = new Resources(assetManager,superRes.getDisplayMetrics(), superRes.getConfiguration());

这样的话就可以生成外部apk文件的Resources对象,有了这个Resources对象,我们就可以找到外部apk文件里面相应资源了,

第二种方式,找到外部apk文件的Resources对象
PackageManager中,有一些获取Resources对象方法


image_1b2va4c3s9ijdb516fg6hup7um.png-43.3kB
image_1b2va4c3s9ijdb516fg6hup7um.png-43.3kB

既然packagerManager,那么就意味这外部资源的apk文件必须要安装在手机里面了。所以我选择第一方法。

综上,我们已经将换肤两个问题都已经解决了,是不是觉得换肤是不是很简单?

andskin

接下来简单说下我写的andskin,传送地址,在android_skin_loader基础上做了改进,

  1. 修改查找需要更换资源的view策略,不干扰view的生成
  2. 能够加载应用内和应用外的皮肤资源,并能够来回切换,
  3. 支持AppCompatActivity,
  4. 支持状态栏换色,当然sdk>19,
  5. 对在更换皮肤的时候,加载资源出错,选择还原默认资源,还是选择上一套资源。有一定完善性
  6. 应用退出后保留上一次皮肤资源相关设置,并在下次应用开启时自动还原上一次主题,并且在主题还原过程中失败自动还原默认主题

相关说明如下:

支持应用内部换肤和外部插件换肤,目前支持background,src,textColor等属性,支持状态栏,方便扩展。

效果图

加载应用内的主题

这里写图片描述

加载应用外的主题

这里写图片描述

加载皮肤皮肤失败后还原设置

这里写图片描述

优缺点

优点

  • 相比较多换肤的框架,并没有侵入View的生成过程,仅仅只是占用了LayoutInflaterFactory,如果在使用中需要LayoutInflaterFactory,可以借助LayoutInflater其他的Factory
  • 易于扩展,你只需要写下那个属性,另外在AttrFactory.java 注册即可
  • 初始化外部资源失败后,会进行自动切换资源加载,选择上一套或者默认
  • 能够选择是否开启更改状态栏的颜色
  • 支持动态加载的view
  • 更改起来方便,只需要在布局文件中的控件添加如下代码
skin:enable="true"

缺点或者说应该注意地方

在布局文件中,添加skin:enable="true"代码的控件必须申明<font color= "red">具有id属性</font>,否则不会更换

比如,对于下面两个控件,在换肤的时候并不会去更换资源的

<TextView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:textColor="@color/text_color_main"
     skin:enable="true" />
<TextView
     android:id="@+id/tv_main_content"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:textColor="@color/text_color_main"/>
指定某个控件的单个/几个属性时,需要显示通过skin:attrs="src|background",默认不写的话,则按照attr支持所有的属性去检查,并生成相应属性对象,

比如对于ImageView,默认会生成src,background两个属性,即意味着,你在换肤的操作的时候,src和background各需要一套资源,即两者都需要更改,否则更换失败。

<ImageView
     android:id="@+id/iv_main_image"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="center_horizontal"
     android:background="@drawable/bg_image_selector"
     android:clickable="true"
     android:src="@drawable/bg_image_selector"
     skin:enable="true"/>

下面这种只会生成一个src的属性,即换肤的时候,仅仅只有src更换,background并不会更换

<ImageView
     android:id="@+id/iv_main_image"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="center_horizontal"
     android:background="@drawable/bg_image_selector"
     android:clickable="true"
     android:src="@drawable/bg_image_selector"
     skin:enable="true"
     skin:attrs="src"/>

同理,我们也可以设定background的属性,如下

<ImageView
     android:id="@+id/iv_main_image"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="center_horizontal"
     android:background="@drawable/bg_image_selector"
     android:clickable="true"
     android:src="@drawable/bg_image_selector"
     skin:enable="true"
     skin:attrs="background"/>
对于应用内部资源应该定义好后缀,并且应用内另一套资源名字为当前资源名字+后缀名字,而且要具有上述你所定义的所有要更换资源的资源文件。

例如,应用内部的默认名字为

    <color name="bg_main">#FFFFFF</color>
    <color name="text_color_main">#000000</color>
    <color name="status_bar_color">#000000</color>
    <color name="status_color">#000000</color>

则blue主题的资源文件为

    <color name="bg_main_blue">#444444</color>
    <color name="text_color_main_blue">#1111AA</color>
    <color name="status_bar_color_blue">#1111AA</color>
    <color name="status_color_blue">#0000FF</color>

<font color ="red">注意,对于更换资源包里,必须要包含所有要更换的资源属性,</font>比如,对于red主题来说,当加载red主题的时候,会失败,并且回退到上次的主题

<color name="bg_main_red">#AAAAAA</color>
<!--<color name="text_color_main_red">#AA2222</color>-->
<color name="status_bar_color_red">#AA2222</color>
<color name="status_color_red">#FF0000</color>
对于外部资源包的时候,可以定义的原来的名字,也可以像更换应用内资源一样定义好后缀名字
动态加载view

这里动态加载view,不只是addView的形式,而是在生成view的时候,比如listView的getView的方法,通常做法为

    if(contentView==null){
        contentView = LayoutInflater.from(context).inflate();
    }

这里通过inflat的时候加载view的时候,可以理解为广泛的动态加载view,类似的情况还有RecyclerView.onBindViewHolder以及Fragment.onCreateView等,这里需要将布局生成代码换成

convertView = baseSkinActivity.inflaterView(R.layout.item_main_list, parent, false);

内部实质是调用SkinLayoutInflater去解析xml布局可能存在需要更改的view

如何使用

添加依赖

gradle

compile 'com.rrtoyewx.andskinlibrary:andskinlibrary:1.0.0'

maven

<dependency>
  <groupId>com.rrtoyewx.andskinlibrary</groupId>
  <artifactId>andskinlibrary</artifactId>
  <version>1.0.0</version>
  <type>pom</type>
</dependency>

2016.12.1 下午add to jcenter, 估计到2016.12.3 审核完

修改步骤

1.Application继承BaseSkinApplication
对于已经继承其他的Application,可以将BaseSkinApplication的相关代码复制到你的Application中

   beforeInit();
   SkinLoader.getDefault().init(this);
   afterInit();

2.Activity继承BaseSkinActivity

3.确认当前Activity是否需要更换皮肤,如果对于当前页面,没有需要更改换肤的控件的时候,可以选择重载shouldRegister()的方法,默认追踪当前的页面所有的资源。

    protected boolean shouldRegister() {
            return true;
    }

4.确定当前的Activity在换肤的操作过程,是否需要更改状态栏的颜色,如果想要关闭,则可以选择重载shouldChangeStatusBarColor(),默认打开。

    protected boolean shouldChangeStatusBarColor() {
            return true;
    }

5.更换资源

    //更换应用内部的资源
    SkinLoader.loadSkin(String resourceSuffix);
    //更换外部包的资源
    SkinLoader.loadSkin(String pluginAPKPackageName,String pluginAPKPath,String resourceSuffix)

6.注册监听,监听换肤操作是否成功

    SkinLoader.addOnChangeSkinListener(OnChangeSkinListener listener)

注册监听的对象会随着在注册监听时所处于页面的消失而被自动remove掉,这个是为了解决内存泄漏的问题

一些细节的说明

对于应用打开初始化的过程

st=>start: 初始化
e=>end: loadSkinEnd

DataManagerInit=>operation: DataManager初始化
GlobalManagerInit=>operation: GlobalManager初始化
ResourceManageInit=>operation: ResourceManage初始化

generateResource=>operation: 生成Resource对象
generateResourceCondition=>condition: Resource对象生成成功?

onInitSuccess=>operation: 回调初始化成功的监听
onInitError=>operation: 回调初始化失败的监听
restoreDefaultSkin=>operation: 还原默认的皮肤

st->DataManagerInit->GlobalManagerInit->ResourceManageInit->generateResource->generateResourceCondition(yes)->onInitSuccess->e

generateResourceCondition(no)->onInitError->restoreDefaultSkin

对于页面注册流程图

st=>start: 注册当前页面
e=>end: 结束

notifyAllChangeSkinObserverList=>operation: 通知当前注册页面更换资源
findAllResource=>operation: 查找所有的资源
findAllResourceCondition=>condition: 查找所有的资源成功?

allChangeSkinObserverList=>operation: 所有控件进行更换操作

restoreDefaultSkin=>operation: 还原默认的皮肤

st->notifyAllChangeSkinObserverList->findAllResource->findAllResourceCondition->findAllResourceCondition(yes)->allChangeSkinObserverList->e
findAllResourceCondition(no)->restoreDefaultSkin->

对于应用启动后,点击换肤的流程图

st=>start: loadSkin
e=>end: loadSkinEnd

saveDataManager=>operation: DataManager存储需要换肤的相关信息
saveDataManagerCondition=>condition:  DataManager存储信息成功?

generateResource=>operation: 生成Resource对象
generateResourceCondition=>condition: Resource对象生成成功?

notifyAllChangeSkinObserverList=>operation: 通知所有注册页面更换资源
findAllResource=>operation: 查找所有的资源
findAllResourceCondition=>condition: 查找所有的资源成功?

allChangeSkinObserverList=>operation: 所有控件进行更换操作

restoreDefaultSkin=>operation: 还原默认的皮肤
restoreLastSkin=>operation: 还原上一套的皮肤

flushGlobalManagerInfos=>operation: 刷新GlobalManager关于资源的相关信息

onLoadSkinError=>operation: 回调加载皮肤失败的监听

onLoadSkinSuccess=>operation: 回调加载皮肤成功的监听

st->saveDataManager->saveDataManagerCondition
saveDataManagerCondition(yes)->generateResource->generateResourceCondition->generateResourceCondition(yes)->notifyAllChangeSkinObserverList->findAllResource->findAllResourceCondition->findAllResourceCondition(yes)->allChangeSkinObserverList->flushGlobalManagerInfos->onLoadSkinSuccess

saveDataManagerCondition(no)->onLoadSkinError->restoreLastSkin->saveDataManager

generateResourceCondition(no)->onLoadSkinError->restoreLastSkin->saveDataManager

findAllResourceCondition(no)->onLoadSkinError->restoreLastSkin->saveDataManager

说明</br>

  1. restoreLastSkin或者restoreDefaultSkin都是一次loadSkin的过程
  2. DataManager保存信息成功或者失败,成功是代表上一次资源相关信息和本次换肤的资源相关信息不一样,则需要保存,即为成功。反之则为失败</br>
  3. 生成Resource对象成功或者失败,成功是指加载外部插件成功生成相应的Resources对象。</br>
  4. 因为采用的是才查找资源再更换的操作,所有在当所有资源都查找成功,才会进行回调,如果存在任一一个资源查找不到,也会认为失败,对于这一点,想了很久业务上面,避免了页面过度重绘</br>
  5. 对于应用打开还原上次资源失败后采用的是还原默认皮肤,而对于应用正常启动,换肤操作失败还原上一次皮肤。</br>
android