Android:换肤

tech2025-10-31  6

这里使用的是开源框架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); }

大家问题有问题可以扣扣扫码 至此本篇结束,祝大家事业顺利!

最新回复(0)