轻松实现Android 更换皮肤(主题) - 资源打包为apk,使用AssetManager动态解析调用


来源:http://www.2cto.com/kf/201501/366859.html


目前很多app都具有换肤功能,可以根据用户自己的喜好定制自己的界面,比如新浪微博,网易新闻等等。今天这里我就是要介绍一种机制实现app换肤。

我找了几款app换肤的应用,换肤基本都是更换了界面的Icon,背景图片,背景色等等,基本没有遇到更换布局的,其实布局也是可以更换的,但是觉得没有必要。所以这篇文章讲解的换肤也是指换icon,背景图片等资源。

通过网络搜索我发现网上上提供了大概这么集中换肤机制:

1、直接将皮肤包放入apk中,这种方案实现非常简单,但是不够灵活,而且还将apk搞大了。

2、将皮肤做成一个独立的apk文件,并和项目工程公用一个shareUsedId,并拥有相同的签名。这种方案较第一种方案就是灵活性比较大,缺点就是需要用户安装,新浪微博目前使用的就是这种方案。


我今天要介绍的这种方案和第二种比较类似,但是我的资源包是不要安装的,毕竟用户一般愿意装一些乱七八糟的应用。

在学习这篇文章之前最好学习我的前一篇文章《Android资源管理机制分析》,因为皮肤管理其实就是资源的管理。下面开始学习如何换肤吧


1、首先我们需要准备一个皮肤包,这个皮肤包里面不会包含任何Activity,里面只有资源文件,这里我为了简单,仅仅加入一个color.xml(其实就相当于Android系统中的framework_res.apk)

?
1
2
3
4
5
6
7
8
9
     "main_btn_color" >#E61ABD
     "main_background" >#38F709
     
     "second_btn_color" ># 000000
     "second_background" >#FFFFFF
     


2、将该资源打包成apk文件,放入sd卡中(实际项目你可以从我网络 下载)

3、将需要换肤的Activity实现ISkinUpdate(这个可以自己随便定义名称)接口


?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
public class MainActivity extends Activity implements ISkinUpdate,OnClickListener
{
     private Button btn_main;
     private View main_view;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
       this .setContentView(R.layout.activity_main);
         
         SkinApplication.getInstance().mActivitys.add( this );
         btn_main=(Button) this .findViewById(R.id.btn_main);
     btn_main.setOnClickListener( this );
         
         main_view= this .findViewById(R.id.main_view);
         
         
         
         
     }
         
     @Override
     protected void onResume() {
       super .onResume();
       if (SkinPackageManager.getInstance( this ).mResources!= null )
       {
         updateTheme();
         Log.d( "yzy" , "onResume-->updateTheme" );
       }
     }
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         // Inflate the menu; this adds items to the action bar if it is present.
         getMenuInflater().inflate(R.menu.main, menu);
         return true ;
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         int id = item.getItemId();
         if (id == R.id.action_settings) {
             //Toast.makeText(this, "change skin", 1000).show();
             File dir= new File(Environment.getExternalStorageDirectory(), "plugins" );
             
             File skin= new File(dir, "SkinPlugin.apk" );
             if (skin.exists())
             {
                   SkinPackageManager.getInstance(MainActivity. this ).loadSkinAsync(skin.getAbsolutePath(), new loadSkinCallBack() {
           @Override
           public void startloadSkin()
           {
             Log.d( "yzy" , "startloadSkin" );
           }
           
           @Override
           public void loadSkinSuccess() {
             Log.d( "yzy" , "loadSkinSuccess" );
             MainActivity. this .sendBroadcast( new Intent(SkinBroadCastReceiver.SKIN_ACTION));
           }
           
           @Override
           public void loadSkinFail() {
             Log.d( "yzy" , "loadSkinFail" );
           }
         });
             }
             return true ;
         }
         return super .onOptionsItemSelected(item);
     }
 
     @Override
     public void updateTheme()
     {
         // TODO Auto-generated method stub
         if (btn_main!= null )
         {
             try {
                 Resources mResource=SkinPackageManager.getInstance( this ).mResources;
                 Log.d( "yzy" , "start and mResource is null-->" +(mResource== null ));
                 int id1=mResource.getIdentifier( "main_btn_color" , "color" , "com.skin.plugin" );
                 btn_main.setBackgroundColor(mResource.getColor(id1));
                 int id2=mResource.getIdentifier( "main_background" , "color" , "com.skin.plugin" );
                 main_view.setBackgroundColor(mResource.getColor(id2));
                 //img_skin.setImageDrawable(mResource.getDrawable(mResource.getIdentifier("skin", "drawable","com.skin.plugin")));
             } catch (Exception e) {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
             }
         }
     }
     
     @Override
     protected void onDestroy() {
         // TODO Auto-generated method stub
         SkinApplication.getInstance().mActivitys.remove( this );
         super .onDestroy();
     }
 
     @Override
     public void onClick(View v) {
         // TODO Auto-generated method stub
         if (v.getId()==R.id.btn_main)
         {
             Intent intent= new Intent( this ,SecondActivity. class );
             this .startActivity(intent);
         }
     }
}

这段代码里面主要看onOptionsItemSelected,这个方法里面,通过资源apk路径,拿到该资源apk对应Resources对象。我们直接看看SkinPacakgeManager里面做了什么吧
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
  * 解析皮肤资源包
  * com.skin.demo.SkinPackageManager
  * @author yuanzeyao
  * create at 2015年1月3日 下午3:24:16
  */
public class SkinPackageManager
{
   private static SkinPackageManager mInstance;
   private Context mContext;
   /**
    * 当前资源包名
    */
   public String mPackageName;
   
   /**
    * 皮肤资源
    */
   public Resources mResources;
   
   private SkinPackageManager(Context mContext)
   {
     this .mContext=mContext;
   }
   
   public static SkinPackageManager getInstance(Context mContext)
   {
     if (mInstance== null )
     {
       mInstance= new SkinPackageManager(mContext);
     }
     
     return mInstance;
   }
   
   
   /**
    * 异步加载皮肤资源
    * @param dexPath
    *        需要加载的皮肤资源
    * @param callback
    *        回调接口
    */
   public void loadSkinAsync(String dexPath, final loadSkinCallBack callback)
   {
     new AsyncTask void ,resources>()
     {
 
       protected void onPreExecute()
       {
         if (callback!= null )
         {
           callback.startloadSkin();
         }
       };
    
       @Override
       protected Resources doInBackground(String... params)
       {
         try {
           if (params.length== 1 )
           {
             String dexPath_tmp=params[ 0 ];
             PackageManager mPm=mContext.getPackageManager();
             PackageInfo mInfo=mPm.getPackageArchiveInfo(dexPath_tmp,PackageManager.GET_ACTIVITIES);
             mPackageName=mInfo.packageName;
             
             
             AssetManager assetManager = AssetManager. class .newInstance();
             Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath" , String. class );
             addAssetPath.invoke(assetManager, dexPath_tmp);
             
             Resources superRes = mContext.getResources();
             Resources skinResource= new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
             SkinConfig.getInstance(mContext).setSkinResourcePath(dexPath_tmp);
             return skinResource;
           }
           return null ;
         } catch (Exception e) {
           return null ;
         }
         
       };
       
       protected void onPostExecute(Resources result)
       {
         mResources=result;
        
         if (callback!= null )
         {
           if (mResources!= null )
           {
             callback.loadSkinSuccess();
           } else
           {
             callback.loadSkinFail();
           }
         }
       };
       
     }.execute(dexPath);
   }
   
   /**
    * 加载资源的回调接口
    * com.skin.demo.loadSkinCallBack
    * @author yuanzeyao
    * create at 2015年1月4日 下午1:45:48
    */
   public static interface loadSkinCallBack
   {
     public void startloadSkin();
     
     public void loadSkinSuccess();
     
     public void loadSkinFail();
   }
   
   
  
}
void ,resources>
调用loadSkinAsync后,如果成功,就会发送一个换肤广播,并将当前皮肤apk的路径保存到sp中,便于下次启动app是直接加载该皮肤资源。


接受换肤广播是在SkinApplication中注册的,当接收到此广播后,随即调用所有已经启动,并且需要换肤的Activity的updateTheme方法,从而实现换肤。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SkinApplication extends Application
{
     private static SkinApplication mInstance= null ;
     
     public ArrayList mActivitys= new ArrayList();
     
     @Override
     public void onCreate() {
         // TODO Auto-generated method stub
         super .onCreate();
         mInstance= this ;
         String skinPath=SkinConfig.getInstance( this ).getSkinResourcePath();
         if (!TextUtils.isEmpty(skinPath))
         {
           //如果已经换皮肤,那么第二次进来时,需要加载该皮肤
           SkinPackageManager.getInstance( this ).loadSkinAsync(skinPath, null );
         }
         
         SkinBroadCastReceiver.registerBroadCastReceiver( this );
     }
     
     public static SkinApplication getInstance()
     {
         return mInstance;
     }
     
     @Override
     public void onTerminate() {
         // TODO Auto-generated method stub
         SkinBroadCastReceiver.unregisterBroadCastReceiver( this );
         super .onTerminate();
     }
     
     public void changeSkin()
     {
         for (ISkinUpdate skin:mActivitys)
         {
             skin.updateTheme();
         }
     }
}

由于这里换肤仅仅是更换icon,背景色之类的,所以比较简单,如果要更换布局文件,那就稍微要复杂一些,这里就不再介绍了,有兴趣的可以自己去研究..

你可能感兴趣的:(andriod)