首先介绍两个实现的方式,这两种也是网上大多数博客写过的:
1. 在应用中配置多种主题,让用户可以选择在不同的场景下切换不同的主题,最常用的就是白天和黑夜两种主题的切换。这种方式的缺点是,不够灵活,应用一旦发布,主题的种类就定死了,如果要新加主题,就需要重新发版。
2. 主题包。把应用中的资源打成一个.apk文件,这个文件中没有activity,只有资源文件,且和主应用中的相同资源的id一样,使用主题包中的资源替换主应用中的资源。主题包可以放在服务器端,通过网络进行下载。这样,我们不同重新发布应用就可以替换主题。但是一般的做法将主题包和主应用的sharedUserId设置为相同的,以便于他们之间可以相互访问,并且需要安装主题apk文件,使用createPackageContext(packageName, flags)方法获取主题包的Context,然后访问主题包中的资源。但是,对于用户来讲,安装一些不可见的文件是很不好的,所以用户体验上可能并不是很好。
今天,我们讲的是第二种方式的主题切换,但是我们的方案并不需要安装主题包。(其实这个方案是在网上看一哥们写的,只是这个哥们只贴出了部分代码,大概讲解了一下,我只是完善了一下,最后会贴出源码下载地址), 先上图说话:
我们大致的先说一下思路:
1. 首先需要去服务器下载主题包,然后存放到SD卡的指定路径下,当然,我们这里没有服务器,所以直接将两个主题的apk文件拷到sd卡的根目录下
2. 然后通过主题包的解析类是解析主题包,这个过程只有个异步的过程。解析成功后,发送一个广播去通知所有的activity去更换主题。
3. 每一个activity需要实现一个更新主题的接口ISkinUpdate来更新主题。
4. 将当前使用主题的路径保存在XML文件中,以便于下次启动应用是继续加载此主题。
好了,接下来,Let’s do it ~
首先,新建两个主题包,删除里边默认建的activity,在values下新建color.xml, 我们这里的主题只是简单改一下应用中的文字和背景颜色,所有颜色的值都在color.xml中。
theme包中的color.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="activity_bg">#E61ABD</color>
<color name="text_bg">#FF3366</color>
<color name="btn_bg">#FF3366</color>
<color name="text_color">#FFFFFF</color>
</resources>
theme2包中的color.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="activity_bg">#4CAA75</color>
<color name="text_bg">#DDE179</color>
<color name="btn_bg">#AA79E1</color>
<color name="text_color">#FFFFFF</color>
</resources>
将上面的两个主题包用签名打包成apk文件,并拷到SD卡根目录下。
接下来,新建我们的主应用,在values文件下同样新建一个color.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="activity_bg">#FFFFFF</color>
<color name="text_bg">#EBF3FD</color>
<color name="btn_bg">#EBF3FD</color>
<color name="text_color">#000000</color>
</resources>
可以看到,我们主应用中的color.xml和主题包中的color的name都是一样的,只是值不一样。
自定义应用的application:
package com.example.update_theme;
import java.util.ArrayList;
import com.example.update_theme.SkinPackageManager.LoadSkinCallBack;
import android.app.Application;
import android.content.Intent;
import android.text.TextUtils;
import android.util.Log;
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 s
super.onCreate();
mInstance = this;
SkinBroadCastReceiver.getInstance(this);
String skinPath = SkinConfig.getInstance(this).getSkinResourcePath();
if (!TextUtils.isEmpty(skinPath)) {
// 如果已经换皮肤,那么第二次进来时,需要加载该皮肤
SkinPackageManager.getInstance(this).loadSkinAsync(skinPath,
new LoadSkinCallBack() {
@Override
public void startloadSkin() {
Log.d("yzy", "startloadSkin");
}
@Override
public void loadSkinSuccess() {
Log.d("yzy", "loadSkinSuccess");
mInstance.sendBroadcast(new Intent(
SkinBroadCastReceiver.SKIN_ACTION));
}
@Override
public void loadSkinFail() {
Log.d("yzy", "loadSkinFail");
}
});
}
SkinBroadCastReceiver.registerBroadCastReceiver();
}
public static SkinApplication getInstance() {
return mInstance;
}
@Override
public void onTerminate() {
// TODO Auto-generated method stub
SkinBroadCastReceiver.unregisterBroadCastReceiver();
super.onTerminate();
}
/**
* 更新所有界面的主题
*/
public void changeSkin() {
for (ISkinUpdate skin : mActivitys) {
skin.updateTheme();
}
}
}
在SkinApplication 中主要是注册了一个广播接收器,以便于主题解析成功后去通过所有activity更换主题,还有就是检查我们XML文件中是否存有主题的路径,如果有,说明这个主题就是当前主题,去加载这个主题。
接下来我们主题的解析类SkinPackageManager,我们看这个类中最主要的方法:
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);
}
使用异步去解析主题,重点都在doInBackground方法中,在此方法中,我们通过反射去得到了主题包的Resouces对象,之后,我们就可以通过这个对象去访问主题包中的资源了。
继续来看MainActivity文件:
public class MainActivity extends Activity implements ISkinUpdate,
OnClickListener {
private RelativeLayout mLayout;
private TextView mTextView;
private Button mButton, mTheme1Btn, mTheme2Btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SkinApplication.getInstance().mActivitys.add(this);
mLayout = (RelativeLayout) findViewById(R.id.id_layout);
mTextView = (TextView) findViewById(R.id.id_text1);
mButton = (Button) findViewById(R.id.id_btn);
mTheme1Btn = (Button) findViewById(R.id.id_btn_theme1);
mTheme2Btn = (Button) findViewById(R.id.id_btn_theme2);
mButton.setOnClickListener(this);
mTheme1Btn.setOnClickListener(this);
mTheme2Btn.setOnClickListener(this);
}
/**
* 解析主题包
* @param themeName 主题名
* @return
*/
private boolean parseTheme(String themeName) {
File skin = new File(Environment.getExternalStorageDirectory(),
themeName + ".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 false;
}
@Override
protected void onResume() {
updateTheme();
super.onResume();
}
@Override
protected void onDestroy() {
SkinApplication.getInstance().mActivitys.remove(this);
super.onDestroy();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.id_btn:
Intent intent = new Intent(MainActivity.this, OtherActivity.class);
startActivity(intent);
break;
case R.id.id_btn_theme1:
if (!parseTheme("theme")){
Toast.makeText(MainActivity.this, "该主题不存在", Toast.LENGTH_SHORT).show();
}
break;
case R.id.id_btn_theme2:
if (!parseTheme("theme2")){
Toast.makeText(MainActivity.this, "该主题不存在", Toast.LENGTH_SHORT).show();
}
break;
}
}
@Override
public void updateTheme() {
Resources mResource = SkinPackageManager.getInstance(this).mResources;
if (mResource != null) {
try {
String packageName = SkinPackageManager.getInstance(this).mPackageName;
Log.d("yzy", "start and mResource is null-->"
+ (mResource == null));
int activityBackgroud = mResource.getIdentifier("activity_bg", "color",
packageName);
int textBackgroud = mResource.getIdentifier("text_bg", "color",
packageName);
int TextColor = mResource.getIdentifier("text_color", "color",
packageName);
int ButtonBackgroud = mResource.getIdentifier("btn_bg", "color",
packageName);
mLayout.setBackgroundColor(mResource.getColor(activityBackgroud));
mTextView.setBackgroundColor(mResource.getColor(textBackgroud));
mTextView.setTextColor(mResource.getColor(TextColor));
mButton.setBackgroundColor(mResource.getColor(ButtonBackgroud));
mButton.setTextColor(mResource.getColor(TextColor));
mTheme1Btn.setBackgroundColor(mResource.getColor(ButtonBackgroud));
mTheme1Btn.setTextColor(mResource.getColor(TextColor));
mTheme2Btn.setBackgroundColor(mResource.getColor(ButtonBackgroud));
mTheme2Btn.setTextColor(mResource.getColor(TextColor));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
在parseTheme(String themeName)方法中我们调用SkinPackageManager类中的loadSkinAsync这个方法去解析指定路径下的主题包,解析成功后,在回调接口的loadSkinSuccess()方法中发送一个广播,通知所有界面更新。
注意,每一个activity需要实现ISkinUpdate接口,此接口中只有一个方法public void updateTheme(),在解析主题成功后,在这个方法中我们去设置需要更改的控件的颜色值。
到此,基本所有的功能就讲解完了,以下是源码:
完整源码下载
说明:请将解压后三个项目导入eclipse中,然后将theme和theme2打包成apk,并拷到sd卡根目录下,否则切换主题不会成功。