这里使用的是开源框架Android-skin-loader。这个框架已经停止维护了,能满足基本功能需求。
基础使用
添加依赖
将这个库的lib作为module导入,这样方便你根据自己的需求去添加一些功能。
使用
- 继承BaseActivity或者BaseFragmentActivity或者BaseFragment
- 在Application中初始化
public class YourApplication extends Application {
public void onCreate() {
super.onCreate();
// Must call init first
SkinManager.getInstance().init(this);
SkinManager.getInstance().load();
}
}
- 在布局中标识需要换肤的view
//命名空间
xmlns:skin="http://schemas.android.com/android/skin"
<TextView
skin:enable="true"
/>
- 从已生成的皮肤文件中设置皮肤
File skin = new File("skin path");
SkinManager.getInstance().load(skin.getAbsolutePath(),
new ILoaderListener() {
@Override
public void onStart() {
}
@Override
public void onSuccess() {
}
@Override
public void onFailed() {
}
});
生成皮肤文件
生成皮肤文件apk
创建一个App module(记住不是library module)。这个module不需要java文件,可以直接将module_name/src/main/java目录删除。然后在res目录下添加你需要更换的资源文件。 记住:需要更换的资源文件必须和主module中的资源文件名字保持一致。 然后直接打包生成apk文件。
复制到主module
将apk文件复制到主module的某个目录下,比如main_module/src/main/assets目录。
更改皮肤文件后缀名
为了防止皮肤文件被用户点击安装,可以将文件后缀改成.skin。或者你自定义一个后缀名。
生成多个皮肤文件
要生成多个皮肤文件,直接在gradle做配置,而无需创建多个module。在skin_module/src目录下创建不同种类的皮肤文件目录,与main同级。这样就可以编译生成不同皮肤的apk。
- 添加buildType
android {
buildTypes{
bmw {
}
benz {
}
toyota {
}
}
}
-
自定义task 自定义一个task,通过获取buildTypes来生成对应的文件夹。
task createAllBuildTypeChildDir() { //遍历main/res下的子目录,然后为不同的buildType生成对应的目录。这里要使用project.rootDir来表示根目录,这样可以自动适配不同的电脑系统 def file = new File("${project.rootDir}/skin_module/src/main/res") file.listFiles().each { childFile -> def dirName = childFile.name project.extensions.each { extension -> extension.getByName("android").properties.each { property -> if (property.key == "buildTypes") { property.value.each { value -> def variantName = value["name"] if (("debug" != variantName) && ("release" != variantName)){ def dest = new File("${project.rootDir}/skin_module/src/"+variantName+"/res",dirName) if (!dest.exists()){ dest.mkdirs() } } } } } } } }
修改不同文件夹下的资源文件,再去编译对应的皮肤文件即可。
标识需要换肤的view
在SkinConfig中定义了命名空间,在需要换肤的布局中添加该命名空间。
public class SkinConfig {
public static final String NAMESPACE = "http://schemas.android.com/android/skin";
}
这个命名空间如何使用到的?
在SkinInflaterFactory的onCreateView方法中先判断了布局中是否存在这个命名空间
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
也就是只有在布局中使用了这个命名空间的布局才能被换肤。
SkinInflaterFactory
SkinInflaterFactory实现了Layoutinflater.Factory。这个类在setContentView(layout)方法之前调用,可以过滤并修改我们需要换肤的view。
onCreateView
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
// if this is NOT enable to be skined , simplly skip it
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
if (!isSkinEnable){
return null;
}
View view = createView(context, name, attrs);
if (view == null){
return null;
}
parseSkinAttr(context, attrs, view);
return view;
}
createView
private View createView(Context context, String name, AttributeSet attrs) {
View view = null;
try {
if (-1 == name.indexOf('.')){
if ("View".equals(name)) {
view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
}
if (view == null) {
view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
}
if (view == null) {
view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
}
}else {
view = LayoutInflater.from(context).createView(name, null, attrs);
}
} catch (Exception e) {
L.e("error while create 【" + name + "】 : " + e.getMessage());
view = null;
}
return view;
}
if(-1==name.indexOf('.')这一句是判断布局中的View是否包含完全路径名。比如TextView,Button等。因此在生成这些View的对象时,需要补全路径。
parseSkinAttr
private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
for (int i = 0; i < attrs.getAttributeCount(); i++){
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
//判断该attr在换肤时是否支持更换,支持换肤的attr由用户来决定
if(!AttrFactory.isSupportedAttr(attrName)){
continue;
}
if(attrValue.startsWith("@")){
try {
//该attrValue对应的资源id
int id = Integer.parseInt(attrValue.substring(1));
//该attrValue对应的资源名字
String entryName = context.getResources().getResourceEntryName(id);
//该attrValue对应的资源类型,比如color,string,drawable等
String typeName = context.getResources().getResourceTypeName(id);
//根据attrName构造一个SkinAttr对象
SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
if (mSkinAttr != null) {
viewAttrs.add(mSkinAttr);
}
} catch (NumberFormatException e) {
e.printStackTrace();
} catch (NotFoundException e) {
e.printStackTrace();
}
}
}
if(!ListUtils.isEmpty(viewAttrs)){
SkinItem skinItem = new SkinItem();
skinItem.view = view;
skinItem.attrs = viewAttrs;
mSkinItems.add(skinItem);
if(SkinManager.getInstance().isExternalSkin()){
//通过SkinAttr的实现类实现换肤功能
skinItem.apply();
}
}
}
SkinManager
负责初始化以及切换皮肤。换肤的方法为load(String path,ILoaderListener listener)。
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = context.getResources();
//重新构造一个Resource对象,这个Resource对象是皮肤包的。
Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
加载皮肤包时,顺便重新构造了皮肤包的对应资源对象Resource。这个可以帮我们获取皮肤包中的资源。以便在主module中动态切换某个资源。
例如:我的项目中在换肤时,要切换支付二维码中间的logo。在SkinManager中新增方法。
//resId是图片在主module中的资源名字。比如:R.drawable.icon_app
public Bitmap getLogo(int resId){
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resId);
if (null!=bitmap&&isDefaultSkin){
return bitmap;
}
//资源的名字。比如:icon_app
String resName = context.getResources().getResourceEntryName(resId);
//资源在皮肤包中的实际id。这里是通过皮肤包的Resource来获取的。
int trueResId = mResources.getIdentifier(resName, "drawable", skinPackageName);
bitmap = BitmapFactory.decodeResource(mResources, trueResId);
return bitmap;
}
自定义SkinAttr
这个开源库对控件的切换支持并不完善,ImageView就不支持切换。我们可以继承SkinAttr来实现对ImageView换肤的支持。
public class ImageAttr extends SkinAttr {
@Override
public void apply(View view) {
if (view instanceof ImageView){
if (attrName.equals(IMAGE_SRC)) {
((ImageView) view).setImageDrawable(SkinManager.getInstance().getDrawable(attrValueRefId));
}
}
}
}
在AttrFactory中增加对ImageView的支持。
public class AttrFactory {
public static final String IMAGE_SRC = "src";
public static SkinAttr get(String attrName, int attrValueRefId, String attrValueRefName, String typeName){
SkinAttr mSkinAttr = null;
if (IMAGE_SRC.equals(attrName)){
//生成ImageAttr
mSkinAttr = new ImageAttr();
}
}
...
}
public static boolean isSupportedAttr(String attrName){
return BACKGROUND.equals(attrName) || TEXT_COLOR.equals(attrName)
||LIST_SELECTOR.equals(attrName) || DIVIDER.equals(attrName)
//支持ImageView
||IMAGE_SRC.equals(attrName);
}