使用插件化换肤
1,在baseActivity里面的super.onCreate(savedInstanceState);
方法之前添加监听。LayoutInflaterCompat.setFactory(getLayoutInflater(),factory);
2,创建一个factory类,实现接口LayoutInflaterFactory
3,在这个里面通过类加载器,使用接口返回的名字获取对应名字的控件。
4,把符合的控件和控件属性存储起来。
5,创建一个皮肤管理类,通过传入插件的路径,获取插件的包名,并且获取插件的resource对象。
6,通过宿主的resId去获取控件的资源名,通过资源名去插件中找到对应的控件resId,在通过这个resId在插件中得到对应资源的值,然后返回给宿主,用来替换,完成换肤。
7,这里用textColor和background两个属性进行试验。
为了模拟插件化,进行换肤,我们会用到两个apk,一个是宿主apk,就是安装在手机上的,顶部两个图片的示例;另外一个是皮肤的apk,这个apk我们直接复制到sd卡上就可以了,里面就只要用到color或者drawable里面的资源。
工程目录如下:
app:
皮肤的:
其中skin2皮肤工程,里面不需要写java代码,只要用到res里面的,如果是用到图片资源,那么图片的名字要和app里面的需要替换的名字相同,如果是用到color,那么color里面的对应的名字也要相同。
下面是代码:
MainActivity:
package andfix.shenbin.com.skindemo; import android.os.Bundle; import android.os.Environment; import android.view.View; import java.io.File; import andfix.shenbin.com.skindemo.struct.SkinManager; public class MainActivity extends SkinActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } public void change(View view) { File file = new File(Environment.getExternalStorageDirectory(),"skin.apk"); SkinManager.getInstance().loadSkin(file.getAbsolutePath()); updateSkin(); } }
SkinActivity:
package andfix.shenbin.com.skindemo; import android.app.Activity; import android.os.Bundle; import android.support.v4.view.LayoutInflaterCompat; import andfix.shenbin.com.skindemo.struct.SkinFactory; import andfix.shenbin.com.skindemo.struct.SkinManager; public class SkinActivity extends Activity { private SkinFactory factory = null; @Override protected void onCreate(Bundle savedInstanceState) { factory = new SkinFactory(); LayoutInflaterCompat.setFactory(getLayoutInflater(),factory); SkinManager.getInstance().init(this); super.onCreate(savedInstanceState); } public void updateSkin(){ factory.apply(); } }
SkinManager:
package andfix.shenbin.com.skindemo.struct; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Created by Administrator on 2017/9/8. */ public class SkinManager { private Context context; private Resources skinResource; private static final SkinManager ourInstance = new SkinManager(); private String defPackage; public static SkinManager getInstance(){ return ourInstance; } public SkinManager(){ } /** * 初始化,传入context * @param context */ public void init(Context context){ this.context = context; } /** * 加载皮肤,这里是进行模拟插件化形式,所以先把skin.apk复制到设备的sd卡。 * @param path 皮肤的apk路径 /storage/emulated/0/skin.apk */ public void loadSkin(String path){ PackageManager packageManager = context.getPackageManager(); //得到插件的包名 defPackage = packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES).packageName; //资源管理器 AssetManager assetManager = null; try { assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class); addAssetPath.invoke(assetManager,path); //得到皮肤插件的resource skinResource = new Resources(assetManager,context.getResources().getDisplayMetrics(), context.getResources().getConfiguration()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } /** *通过资源名 去插件中找对应的资源,用来替换宿主apk里面的。 * @param resId 是宿主view 对应的属性对应的id * @return */ public int getColor(int resId){ if(skinResource == null){ return resId; } //1,首先通过宿主的resId找到资源名,然后因为宿主和插件apk的资源名是一样的, // 2,通过资源名去插件中找到对应的资源名的id // 3,再通过找到的id去获取插件中对应的资源的值,返回给宿主进行替换。 String resName = context.getResources().getResourceName(resId); //andfix.shenbin.com.skindemo:color/colorAccent String resName2 = resName.split("/")[1]; int skinId = skinResource.getIdentifier(resName2,"color",defPackage);//资源名,资源类型,包名 if(skinId == 0){ //如果为0,就表示在插件的apk里面没有找到相同名字的资源。 return resId; } return skinResource.getColor(resId); } /** * 图片资源 *通过资源名 去插件中找对应的资源,用来替换宿主apk里面的。 * @param resId 是宿主view 对应的属性对应的id * @return */ public Drawable getDrawable(int resId){ if(skinResource == null){ return context.getResources().getDrawable(resId); } //1,首先通过宿主的resId找到资源名,然后因为宿主和插件apk的资源名是一样的, // 2,通过资源名去插件中找到对应的资源名的id // 3,再通过找到的id去获取插件中对应的资源的值,返回给宿主进行替换。 String resName = context.getResources().getResourceName(resId); //andfix.shenbin.com.skindemo:color/colorAccent String resName2 = resName.split("/")[1]; int skinId = skinResource.getIdentifier(resName2,"drawable",defPackage);//资源名,资源类型,包名 if(skinId == 0){ //如果为0,就表示在插件的apk里面没有找到相同名字的资源。 return context.getResources().getDrawable(resId); } return skinResource.getDrawable(skinId); } }SkinFactory:
package andfix.shenbin.com.skindemo.struct; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.support.v4.view.LayoutInflaterFactory; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; /** * 皮肤控件接口监听类 * Created by Administrator on 2017/9/8. */ public class SkinFactory implements LayoutInflaterFactory { //自定义的控件,这里面放包控件的包路径 public static String[] sClassPrefixList = { "andfix.shenbin.com.skindemo.struct" }; //存放符合条件的皮肤数据源 private List<SkinView> skinCacheList = new ArrayList<>(); @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { View view = null; if (!name.contains(".")) { //表示系统的控件 android.widget.LinearLayout view = createView(context, attrs, "android.widget."+name); }else{ //自定义view for (String str : sClassPrefixList){ view = createView(context,attrs,str+"."+name); if(view != null){ break; } } } if(view != null){ parseSkinView(context,attrs,view); } return view; } private View createView(Context context, AttributeSet attrs, String name) { try { //获得当前的类加载器 ClassLoader classLoader = context.getClass().getClassLoader(); //通过名字,获得控件的类 Class clz = classLoader.loadClass(name); Constructor<? extends View> constructor = clz.getConstructor(new Class[]{Context.class, AttributeSet.class}); //返回控件 return constructor.newInstance(context, attrs); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } /** * 筛选,过滤,把符合条件的控件和资源存储起来 * @param context * @param attrs * @param view */ private void parseSkinView(Context context,AttributeSet attrs,View view){ List<SkinItem> lists = new ArrayList<>(); for (int i = 0; i < attrs.getAttributeCount(); i++) { String attrName = attrs.getAttributeName(i); //textColor String attrValue = attrs.getAttributeValue(i); //@7f030000 R.java //如果是背景或者textColor属性 if("background".equalsIgnoreCase(attrName) || "textColor".equalsIgnoreCase(attrName)){ //得到属性值id int id = Integer.parseInt(attrValue.substring(1)); //得到属性类型 String attrValueType = context.getResources().getResourceTypeName(id); //得到属性名 String attrValueName = context.getResources().getResourceEntryName(id); //创建皮肤实例 SkinItem skinItem = new SkinItem(attrName,id,attrValueType,attrValueName); //放到集合中 lists.add(skinItem); } } if(!lists.isEmpty()){ //创建实例,保存,用来换肤 SkinView skinView = new SkinView(view,lists); skinCacheList.add(skinView); } } /** * 皮肤类 */ class SkinItem{ String attrName; //background int refId; //R 文件标示的id String attrValueType; //资源类型,@drawable 、@color String atrrValueName; //资源名字 ic_launcher public SkinItem(String attrName,int refId,String attrValueType,String atrrValueName){ this.attrName = attrName; this.refId = refId; this.attrValueType = attrValueType; this.atrrValueName = atrrValueName; } } class SkinView{ private View view; private List<SkinItem> lists; public SkinView(View view,List<SkinItem> lists){ this.view = view; this.lists = lists; } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public void apply() { for (SkinItem skinItem : lists){ //切换textView的字体颜色 if("textColor".equalsIgnoreCase(skinItem.attrName)){ if(view instanceof TextView){ ((TextView)view).setTextColor(SkinManager.getInstance().getColor(skinItem.refId)); } } //这个是切换背景颜色 // else if("background".equalsIgnoreCase(skinItem.attrName)){ // // ((View)view).setBackgroundColor(SkinManager.getInstance().getColor(skinItem.refId)); // // } //切换背景图片 else if("background".equalsIgnoreCase(skinItem.attrName)){ ((View)view).setBackground(SkinManager.getInstance().getDrawable(skinItem.refId)); } } } } public void apply(){ for (SkinView skinView : skinCacheList){ skinView.apply(); } } }activity_main.xml:<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="@drawable/bj" tools:context="andfix.shenbin.com.skindemo.MainActivity"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="换肤" android:textSize="20dp" android:textColor="@color/colorAccent" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="切换皮肤" android:onClick="change"/> </LinearLayout>app的color.xml:<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#3F51B5</color> <color name="colorPrimaryDark">#303F9F</color> <color name="colorAccent">#FF4081</color> </resources>skin2的color.xml:<?xml version="1.0" encoding="utf-8"?> <resources> <color name="colorPrimary">#FF4081</color> <color name="colorPrimaryDark">#303F9F</color> <color name="colorAccent">#303F9F</color> </resources>背景图片大家可以自己下载两张,名字一样,一个工程放一张就可以了以上就是全部代码了,运行时,测试步骤如下:1,先在skin2工程里面把图片资源和color资源搞定。2,把skin2编译,得到apk包,改名为skin.apk(也可以取其他名字,在MainActivity里面也改成相应的就可以了)3,把skin.apk复制到手机存储卡上,有的是放在sd卡,在MainActivity里面修改相应的路径。4,运行app工程,然后点击按钮进行切换皮肤。5,背景颜色或背景图片,在这里进行注释与放开:如图以上就是全部的代码,代码中有很多注释,相信大家能看懂。通过其中的类加载器ClassLoader和资源管理器AssetManager,然后大家是不是也对插件化有了一定的了解呢。大家有不明白或者需要源码工程的,可以在下面评论留言,留下邮箱,我会发给大家。