Android换肤插件(一)

Author:xqforever

Time:2013.5.28

最近从事Android插件相关工作,调研了动态换肤的一些方法。

1、需求:

      皮肤插件不需要安装

      只包括图像,文字等资源,不含有任何逻辑代码

      可动态加载替换升级

      加载实现简单,View的加载交给Android系统完成

     

2、过程:

      目前对插件加载,可以查看DexClassLoader用法,获取AssetManager和Resources 

       DexClassLoader classLoader =new DexClassLoader(apkPath, dexPath, null,
                            getClassLoader());                      

       AssetManager  am = (AssetManager)AssetManager.class.newInstance();
                     am.getClass().getMethod("addAssetPath",String.class)
                                   .invoke(am,apkPath);
                     //创建Resources
                     ResourcessuperRes = getResources();
                   Resources  res= new Resources(am, superRes.getDisplayMetrics(),
                                   superRes.getConfiguration());

       通过资源Resources获取文字,图片或者layout信息。

        Resources的getResource().getIdentifier(String,category,
                            packageName);

       例如:

       int id=resource.getResource().getIdentifier("image", "drawable",
                            "com.example.test");

        Drawble drawable= resource.getResource().getDrawable(id);

       可以获取图片资源,类似的颜色资源,字符串资源,布局资源等等,都可以通过此种方式得到。

       可以假设,如果在主程序中定义控件,然后将资源从皮肤包中取出来,通过view的set属性方法注入,可以从理论上实现换肤功能。

       但这种方式缺点在于代码量繁琐,做了很多Android系统绘制控件的工作,重复造轮子,框架开发也繁琐。

       

       理想的换肤框架,应该是皮肤插件为主程序提供资源,主程序撰写逻辑,而View的填充完全交给Android系统完成。

       思路:主程序定义一个theme的xml布局文件,

       <?xml version="1.0"encoding="utf-8"?>
    <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >


    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/button1_text"
         />


    <TextView
        android:id="@+id/textview1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/textview1_text"
          />


    <ImageView
        android:id="@+id/imageview1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />
</LinearLayout>

 

在资源包中包含有同样的theme布局文件

例如皮肤1中

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <Button
        android:id="@+id/button1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/button1_text"
         android:textColor="@color/yellow"
         />
    <TextView
        android:id="@+id/textview1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/textview1_text"
        android:textColor="@color/yellow"
        />
    <ImageView
        android:id="@+id/imageview1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/guide_map" />
</LinearLayout>

 

控件的id设置相同,便于换肤框架的实现。

主程序中加载main布局, 

<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
   android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >


        <Spinner
            android:id="@+id/spinner1"
            android:layout_width="fill_parent"
           android:layout_height="wrap_content" />
    </LinearLayout>


    <LinearLayout
        android:id="@+id/customUi"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >
    </LinearLayout>


</LinearLayout>

main布局通过一个layout,id叫customUi来加载皮肤包的theme布局,实现换肤。

view = (LinearLayout) ((LayoutInflater) mContext
                                   .getSystemService(Context.LAYOUT_INFLATER_SERVICE))
                                   .inflate(
                                                 currentSkinResource.getResource().getLayout(
                                                               layoutId),null);

这个方法能够加载皮肤包资源的layout布局,当然要通过上文所述方法获取layout的id。

然后将这个View,加入到主界面的layout中,就能实现动态加载。View的绘制完全由Android完成。

思路虽然很简单,但是layout里面带有很多的坑,因为其对资源文件的引用,是在编译过程中写死的,而对于同一个image控件记载图片,由于Android图片缓存机制,也只能通过更换layout方法加载一次,

   

       经过几天的摸索,现有的成果和大家分享下:

       Android的资源变量是整个Application唯一的,即Resouces中的mSystem,而Asserts也是唯一的,即Resouces中的mAssets。

        可以通过反射机制去对这两个变量进行注入。

       即布局文件加载前,保存当前的resource和assets,然后将皮肤包中得到的Resouces和Asserts注入。

 

                SkinResourceskinResource = skinResources.get(skinResIndex);
              ResourcescurrentResources = skinResource.getResource();
              intlayoutId = currentResources.getIdentifier(layoutXml, "layout",
                            skinResource.getPackageName());
              Resourcesresources = mContext.getResources();
              Class<?>clazz = resources.getClass();
              Field[]fs = clazz.getDeclaredFields();
              //System.out.println(fs.length);
              for(int i = 0; i < fs.length; i++) {
                     System.out.println(fs[i].getName());
                     if(fs[i].getName().equals("mSystem")) {
                            Fieldf = fs[i];
                            f.setAccessible(true);
                            Objectval = f.get(resources);
                            System.out.println("name:"+ f.getName() + "\t value = " + val);
                            f.set(resources,currentResources);
                            f.setAccessible(false);
                     }
                     if(fs[i].getName().equals("mAssets")) {
                            Fieldf = fs[i];
                            f.setAccessible(true);
                            Objectval = f.get(resources);
                            System.out.println("name:"+ f.getName() + "\t value = " + val);
                            f.set(resources,currentResources.getAssets());
                            f.setAccessible(false);
                     }
                     if(fs[i].getName().equals("mDrawableCache")) {
                            Fieldf = fs[i];
                            f.setAccessible(true);
                            LongSparseArray<WeakReference<Drawable.ConstantState>>list = new LongSparseArray<WeakReference<ConstantState>>();
                            list= (LongSparseArray<WeakReference<ConstantState>>) f
                                          .get(resources);
                            list.clear();
                            f.setAccessible(false);
                     }

       SkinResource 是一个自定义的类,里面有Resouces和PackageName,其余代码主要做注入功能,对mDrawableCache清空,这样layout注入时,图片资源也能更新。

       注入后,主程序的layout布局就能对其中定义的控件进行引用,

       例如:button1 = (Button)skinManager.findViewByID("button1", skin);

       最后在布局结束后,需要还原现场,将原来系统的res和assets还原,同样通过注入方式。

       

3、结果:通过这个框架,可以实现换肤功能。

      缺陷如下:系统inflate->资源的索引->资源实际位置->绘制View。我们对res和assets通过反射注入只是做到了资源实际位置的替换,但是索引却依然是主程序生成的,所以要注意皮肤包和主程序的索引的一致性。

     layout解析,会涉及layout对资源的引用,而对这个引用的解析,是查询R.java被编译成二进制的文件获取资源地址的。R.java文件即为索引作用,映射到具体的Resouces文件。

      由于R.java无法实现注入,是根据编译平台,版本自动生成,所以必须保证皮肤包的R.java文件与主程序的R.java文件相同,或者是对各种图片,字符串等文件的顺序一致。R.java打包一致外,可能还需要编译平台以及编译版本的一致性(此条件未验证),才能保证主程序的引用。

      理论解决方案:将所有res中的资源,按照在主程序中加载的顺序,依此添加进皮肤包,因为R.java文件记住的是索引,所以在皮肤文件不改名情况下,可以对文件内容进行更改,这样就能生成皮肤包,且主程序能正确的识别并且换肤。

     这种方法扩展性对于版本升级的扩展性差, 对以前版本的皮肤包不能兼容。


另附上插件App为安装时,换肤机制实现。

apk格式

apk之间读取数据的条件是:有相同签名并且AndroidManifest.xml中配置android:sharedUserId有相同的属性值,这样两个apk运行在同一个进程中,就能互相访问数据了。

方法如下:

a)应用程序和皮肤程序的AndroidManifest.xml中配置

例如:android:sharedUserId="com.zj"

b)访问资源的方法

Contextcontext = createPackageContext("com.zj.skin",Context.CONTEXT_IGNORE_SECURITY); 

获取到com.zj.skin对应的Context,通过返回的context对象就可以访问到com.zj.skin中的任何资源。

例如:应用apk要获得皮肤apk中的bg.png,可按照上述框架获取资源id,然后通过id获取相应图片资源或者layout布局。

Drawabledrawable = context.getResources().getDrawable(id);

这样就得到了图片的引用,其他xml资源文件的获取方式也是类似的。

获取了皮肤插件的Context后,就解决了索引与资源的对应问题,也就是可以不用通过Res和Assets注入与还原这两部分,然后采用本文描述的换肤框架即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值