当前位置: 代码迷 >> Android >> 轻便实现Android 更换皮肤(主题)
  详细解决方案

轻便实现Android 更换皮肤(主题)

热度:517   发布时间:2016-04-28 03:01:15.0
轻松实现Android 更换皮肤(主题)

目前很多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>


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

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();  }     }
调用loadSkinAsync后,如果成功,就会发送一个换肤广播,并将当前皮肤apk的路径保存到sp中,便于下次启动app是直接加载该皮肤资源。


接受换肤广播是在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,背景色之类的,所以比较简单,如果要更换布局文件,那就稍微要复杂一些,这里就不再介绍了,有兴趣的可以自己去研究..

1楼singwhatiwanna昨天 22:51
赞,换肤好
Re: yuanzeyao2008昨天 23:08
回复singwhatiwannan原来是任兄光临..
  相关解决方案