转自:http://blog.youkuaiyun.com/yuanzeyao/article/details/42390431
目前很多app都具有换肤功能,可以根据用户自己的喜好定制自己的界面,比如新浪微博,网易新闻等等。今天这里我就是要介绍一种机制实现app换肤。
我找了几款app换肤的应用,换肤基本都是更换了界面的Icon,背景图片,背景色等等,基本没有遇到更换布局的,其实布局也是可以更换的,但是觉得没有必要。所以这篇文章讲解的换肤也是指换icon,背景图片等资源。
通过网络搜索我发现网上上提供了大概这么集中换肤机制:
1、直接将皮肤包放入apk中,这种方案实现非常简单,但是不够灵活,而且还将apk搞大了。
2、将皮肤做成一个独立的apk文件,并和项目工程公用一个shareUsedId,并拥有相同的签名。这种方案较第一种方案就是灵活性比较大,缺点就是需要用户安装,新浪微博目前使用的就是这种方案。
我今天要介绍的这种方案和第二种比较类似,但是我的资源包是不要安装的,毕竟用户一般愿意装一些乱七八糟的应用。
在学习这篇文章之前最好学习我的前一篇文章《Android资源管理机制分析》,因为皮肤管理其实就是资源的管理。下面开始学习如何换肤吧
1、首先我们需要准备一个皮肤包,这个皮肤包里面不会包含任何Activity,里面只有资源文件,这里我为了简单,仅仅加入一个color.xml(其实就相当于Android系统中的framework_res.apk)
- <?xml version="1.0" encoding="utf-8"?>
- <resources>
- <color name="main_btn_color">#E61ABD</color>
- <color name="main_background">#38F709</color>
- <color name="second_btn_color">#000000</color>
- <color name="second_background">#FFFFFF</color>
- </resources>
3、将需要换肤的Activity实现ISkinUpdate(这个可以自己随便定义名称)接口
- 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里面做了什么吧
- /**
- * 解析皮肤资源包
- * com.skin.demo.SkinPackageManager
- * @author yuanzeyao <br/>
- * 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<String,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 <br/>
- * create at 2015年1月4日 下午1:45:48
- */
- public static interface loadSkinCallBack
- {
- public void startloadSkin();
- public void loadSkinSuccess();
- public void loadSkinFail();
- }
- }
接受换肤广播是在SkinApplication中注册的,当接收到此广播后,随即调用所有已经启动,并且需要换肤的Activity的updateTheme方法,从而实现换肤。
- public class SkinApplication extends Application
- {
- private static SkinApplication mInstance=null;
- public ArrayList<ISkinUpdate> mActivitys=new ArrayList<ISkinUpdate>();
- @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,背景色之类的,所以比较简单,如果要更换布局文件,那就稍微要复杂一些,这里就不再介绍了,有兴趣的可以自己去研究..