Android实现apk插件方式换肤

学向勤中得,萤窗万卷书。这篇文章主要讲述Android实现apk插件方式换肤相关的知识,希望能为你提供帮助。
换肤思路:
1.什么时候换肤?
xml加载前换肤,如果xml加载后换肤,用户将会看见换肤之前的色彩,用户体验不好。
2.皮肤是什么?
皮肤就是apk,是一个资源包,包含了颜色、图片等。
3.什么样的控件应该进行换肤?
包含背景图片的控件,例如textView文字颜色。
4.皮肤与已安装的资源如何匹配?
资源名字匹配
 
效果展示:

Android实现apk插件方式换肤

文章图片

 
【Android实现apk插件方式换肤】 
 
 
步骤:
1.xml加载前换肤,意味着需要将所需要换肤的控件收集起来。因此要监听xml加载的过程。
1 public class BaseActivity extends Activity { 2 3SkinFactory skinFactory; 4 5@Override 6protected void onCreate(@Nullable Bundle savedInstanceState){ 7super.onCreate(savedInstanceState); 8 9//监听xml生成的过程 10skinFactory = new SkinFactory(); 11LayoutInflaterCompat.setFactory(getLayoutInflater(),skinFactory); 12} 13 }

 
2.需要换肤的控件收集到一个容器中并且不更改自己的逻辑直接换肤(例如:不用在每个需要换肤的空间里面加上: “ app:...... ”  自定义控件属性)
思考:
(1)安装的apk的id与皮肤id是否一样?
(2)图片的资源、颜色资源都对应R自动生成的id
(3)皮肤包的资源id、R文件的资源id以及app里R文件的资源的id是否是一样的?——是不一样的
 
3.一个activity有多个控件(SkinView)  一个控件对应多个换肤属性(SkinItem)
Android实现apk插件方式换肤

文章图片

SkinItem来封装这些值:
  • attrName-属性名(background)
  • attrValue-属性值id 十六进制(@color/colorPrimaryDark)
  • attrType--类型(color)
  • Id(R文件的id)
1 class SkinItem{ 2// attrNamebackground 3String attrName; 4 5int refId; 6// 资源名字@color/colorPrimaryDark 7String attrValue; 8//drawable color 9String attrType; 10 11public SkinItem(String attrName, int refId, String attrValue, String attrType) { 12this.attrName = attrName; 13this.refId = refId; 14this.attrValue = https://www.songbingjia.com/android/attrValue; 15this.attrType = attrType; 16} 17 18public String getAttrName() { 19return attrName; 20} 21 22public int getRefId() { 23return refId; 24} 25 26public String getAttrValue() { 27return attrValue; 28} 29 30public String getAttrType() { 31return attrType; 32} 33}

SkinView:
1 class SkinView{ 2private View view; 3private List< SkinItem> list; //收集需要换肤的集合 4 5public SkinView(View view, List< SkinItem> list) { 6this.view = view; 7this.list = list; 8} 9}

 
收集控件:
SkinFactory:
1 package com.example.apk_demo2; 2 3 import android.content.Context; 4 import android.util.AttributeSet; 5 import android.util.Log; 6 import android.view.View; 7 import android.widget.TextView; 8 9 import androidx.core.view.LayoutInflaterFactory; 10 11 import java.lang.reflect.Constructor; 12 import java.lang.reflect.InvocationTargetException; 13 import java.util.ArrayList; 14 import java.util.List; 15 16 // LayoutInflaterFactory接口 17 public class SkinFactory implements LayoutInflaterFactory { 18 19private List< SkinView> cacheList = new ArrayList< > (); 20private static final String TAG = "david" ; 21//补充系统控件的包名 22private static final String[] prefixList={"android.widget.","android.view.","android.webkit."}; // 包名,android.webkit为浏览器包,v4、v7包都可以认为是自定义控件 23 24 25/** 26* xml生成的时候会回调这个方法,返回值为view 27* @param parent 28* @param name控件名 29* @param context 30* @param attrs 31* @return 32*/ 33@Override 34public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { 35Log.i(TAG,"onCreateView:"+name); 36// 需要换肤的控件收集到一个容器中 37 38View view = null; //初始化view 39// 判断自定义与非自定义控件(自定义控件打印时是全报名) 40if(name.contains(".")){ 41// 自定义控件 42view = createView(context,attrs,name); // 获得自定义控件的实例化对象 43}else{ 44// 系统控件 45for(String pre : prefixList){ 46view = createView(context,attrs,pre + name); 47 //Log.i(TAG,"创建view:"+view); 48if(view != null){ 49// 找对包名,实例化成功 50// 解析view 51//如果不为空则说明实例化成功,找对了包名 52break; 53//找对了可以退出循环 54} 55} 56} 57 58if(view != null){ 59//view不为空则说明已经拿到了这个view,这时候开始解析这个view,判断哪些控件需要换肤 60parseSkinView(context,attrs,view); 61//这个方法用于收集需要换肤的view 62} 63return view; 64} 65 66 67/** 68* 收集需要换肤的控件 69* @param context 70* @param attrs 71* @param view 72*/ 73private void parseSkinView(Context context, AttributeSet attrs, View view) { 74List< SkinItem> list = new ArrayList< > (); //将需要换肤的控件添加到这个集合里面 75for(int i = 0; i < attrs.getAttributeCount(); i++){ 76//做一个java bean来封装这些值: 77// attrName-属性名(background)、attrValue-属性值id 十六进制(@color/colorPrimaryDark)、attrType--类型(color)、Id(R文件的id) 78// attrName == background等 时 (属性名) 79String attrName = attrs.getAttributeName(i); 80// 获得控件的id值,eg:@color/colorPrimaryDark(属性值) 81String attrValue = https://www.songbingjia.com/android/attrs.getAttributeValue(i); 82 83if(attrName.equals("background") || attrName.equals("textColor")){ 84// 需要换肤的控件——具备换肤的潜力,并不是一定需要换肤 85 //Log.i(TAG,"parseSkinView:"+attrName); 86int id = Integer.parseInt(attrValue.substring(1)); //引用类型 87 88String entry_name = context.getResources().getResourceEntryName(id); 89 90String typeNme = context.getResources().getResourceTypeName(id); 91 92SkinItem skinItem = new SkinItem(attrName,id,entry_name,typeNme); 93list.add(skinItem); 94} 95} 96 97if(!list.isEmpty()){ 98SkinView skinView = new SkinView(view,list); 99cacheList.add(skinView); 100//应用换肤xml加载过程中换肤 101skinView.apply(); 102 103} 104} 105 106//点击应用 107public void apply() { 108for(SkinView skinView : cacheList){ 109skinView.apply(); 110} 111} 112 113public void remove() { 114for (SkinView skinView : cacheList){ 115//清空集合 116 //cacheList.removeAll(); 117} 118} 119 120/** 121* 一个activity有多个控件 122* 一个控件对应多个换肤属性 123*/ 124class SkinView{ 125private View view; 126private List< SkinItem> list; //收集需要换肤的集合 127 128public SkinView(View view, List< SkinItem> list) { 129Log.i(TAG,"view123:"+view); 130this.view = view; 131this.list = list; 132} 133 134//应用换肤 135public void apply(){ 136//循环需要换肤的SkinItem,应用所有的换肤 137for(SkinItem skinItem : list){ 138Log.i(TAG,"skinItem:"+skinItem.getAttrName()); 139if("textColor".equals(skinItem.getAttrName())){ 140Log.i(TAG,"view_1:"+view); 141//if (!SkinManager.getInstance().getSkinPackage().equals("")){ 142//最开始的时候系统没有资源文件,所以当有没有都运行这行代码是,系统没有获得颜色id,因此为灰色。 143//所以得加一个判断,在没有换肤之前采用系统默认颜色 144if (!SkinManager.getInstance().getSkinPackage().equals("")) { 145((TextView) view).setTextColor(SkinManager.getInstance().getColor(skinItem.getRefId())); 146} 147} 148if("background".equals(skinItem.getAttrName())){ 149if("color".equals(skinItem.getAttrType())){ 150//直接这样设置,没有任何换肤功能,这样加载就是本身默认颜色 151 //view.setBackgroundColor(skinItem.getRefId()); 152 153if (!SkinManager.getInstance().getSkinPackage().equals("")){ 154view.setBackgroundColor(SkinManager.getInstance().getColor(skinItem.getRefId())); 155} 156}else if("drawable".equals(skinItem.getAttrType())){ 157if(!SkinManager.getInstance().getSkinPackage().equals("")){ 158view.setBackgroundDrawable(SkinManager.getInstance().getDrawable(skinItem.getRefId())); 159} 160} 161 162} 163} 164} 165} 166 167/** 168* 封装值 169*/ 170class SkinItem{ 171// attrNamebackground 172String attrName; 173//R里面的id 174int refId; 175// 资源名字@color/colorPrimaryDark 176String attrValue; 177//drawable color 178String attrType; 179 180public SkinItem(String attrName, int refId, String attrValue, String attrType) { 181this.attrName = attrName; 182this.refId = refId; 183this.attrValue = https://www.songbingjia.com/android/attrValue; 184this.attrType = attrType; 185} 186 187public String getAttrName() { 188return attrName; 189} 190 191public int getRefId() { 192return refId; 193} 194 195public String getAttrValue() { 196return attrValue; 197} 198 199public String getAttrType() { 200return attrType; 201} 202} 203 204/** 205* 加载自定义控件 206* @param context 207* @param attrs 208* @param name 209* @return 210*/ 211private View createView(Context context, AttributeSet attrs, String name) { 212try{ 213//运用反射拿到自定义控件的构造方法,没有性能损耗 214Class viewClazz = context.getClassLoader().loadClass(name); 215Constructor< ? extends View> constructor = viewClazz.getConstructor(new Class[]{Context.class,AttributeSet.class}); //通过反射获得自定义控件的构造方法 216return constructor.newInstance(context,attrs); //通过反射而来的构造函数来实例化对象 217} catch (InstantiationException e) { 218e.printStackTrace(); 219} catch (InvocationTargetException e) { 220e.printStackTrace(); 221} catch (NoSuchMethodException e) { 222e.printStackTrace(); 223} catch (IllegalAccessException e) { 224e.printStackTrace(); 225} catch (ClassNotFoundException e) { 226e.printStackTrace(); 227} 228 229return null; 230} 231 }

 
 
 
4.收集完毕后,应用换肤 (xml加载过程中换肤)
 
Android实现apk插件方式换肤

文章图片

 
创建SkinManager去获得皮肤apk,app通过SkinManager获取皮肤apk
(1)加载皮肤包(loadSkin):通过反射获得AsserManager的addAssetpath()方法,再通过这个方法获得皮肤apk,从而实例化skinResource;再通过PackageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES).packageName; 获得皮肤包名
(2)获取颜色(getColor):判断skinResource是否为空;拿到res的名字,eg:通过“colorAccent”去寻找id
 
SkinManager:
1 package com.example.apk_demo2; 2 3 import android.content.Context; 4 import android.content.pm.PackageManager; 5 import android.content.res.AssetManager; 6 import android.content.res.Resources; 7 import android.graphics.drawable.Drawable; 8 import android.util.Log; 9 10 import androidx.core.content.ContextCompat; 11 12 import java.lang.reflect.InvocationTargetException; 13 import java.lang.reflect.Method; 14 15 public class SkinManager { 16private static final String TAG = "yu" ; 17//代表外置卡皮肤app的resource 18private Resources skinResource; 19 20private Context context; 21//皮肤apk包名 22private String skinPackage; 23// 初始化context 24public void init(Context context){ 25// 一定用getApplicationContext()方法获得context,其是一定存在的;(从内存角度上引用全局上下文) 26// 如果是靠参数context,有可能是不存在的(如果activity被销毁了) 27this.context = context.getApplicationContext(); 28} 29private static final SkinManager ourInstance = new SkinManager(); 30 31public static SkinManager getInstance(){ return ourInstance; } 32 33/** 34*加载皮肤包 35* @param path 路径 36*/ 37public void loadSkin(String path){ 38 39// Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) 40// 实例化AssetManager (@hide)AssetManager()是一个系统保护函数,需要通过反射来调用 41try{ 42AssetManager assetManager = AssetManager.class.newInstance(); 43//通过assetManager.addAssetPath(""); 方法获得皮肤apk需反射 44Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class); 45addAssetPath.invoke(assetManager,path); 46 47skinResource = new Resources(assetManager,context.getResources().getDisplayMetrics(), 48context.getResources().getConfiguration()); // 实例化skonResource 49// skinResource.getColor(R.color.colorAccent); 通过这样就可以获得资源文件的皮肤设置 50PackageManager packageManager = context.getPackageManager(); //包管理器 51//获得皮肤包名 52Log.i(TAG,"路径"+path); 53 //Log.i(TAG,"上下文"+context); 54Log.i(TAG,"上下文"+context); 55skinPackage = packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES).packageName; 56Log.i(TAG,"包名"+skinPackage); 57} catch (IllegalAccessException e) { 58e.printStackTrace(); 59} catch (InstantiationException e) { 60e.printStackTrace(); 61} catch (NoSuchMethodException e) { 62e.printStackTrace(); 63} catch (InvocationTargetException e) { 64e.printStackTrace(); 65} catch (Exception e){ 66Log.i(TAG,"上下文"+context); 67Log.i(TAG,"包名"+skinPackage); 68} 69 70} 71 72private SkinManager(){ } 73 74/** 75* 76* @param resId 77* @return 78*/ 79public int getColor(int resId){ 80//判断有没有皮肤包 81if(skinResource == null){ 82return resId; 83} 84 85//能否通过这个方法获得 int skinId = skinResource.getColor(resId); 86//不能,因为R文件的id与皮肤apk的id不一样 87//eg:获得colorAccent 88String resName = context.getResources().getResourceEntryName(resId); 89// public int getIdentifier(String name, String defType, String defPackage) 90int skinId = skinResource.getIdentifier(resName,"color",skinPackage); 91if(skinId == 0){ 92//如果不合法,返回默认xml 93return resId; 94} 95

    推荐阅读