目录
4.2.调用兼容AppCompatActivity代理类AppCompatDelegate实现xml布局到View视图的转换
4.3.AppCompatActivity定义实现不同版本AppCompatDelegate实现类
4.4.setContentView()方法最终调用代理类AppCompatDelegateImplV7中的方法
4.5.在LayoutInflater中实现基于XmlPullParser解析xml布局文件
4.6.xml解析调用createViewFromTag()通过控件名称和属性集合创建视图
4.7.createViewFromTag()真正执行解析xml布局文件以后创建View视图
4.8.mFactory2来源(调试发现最终调用的是AppCompatActivity代理类的onCreateView()方法)
6.1SkinFactory实现Factory2接口,完成视图的创建,视图的缓存,全部视图的换肤操作;
6.2SkinEngine加载插件apk的对应的Resources,(Resources用插件apk的资源)实现更换皮肤的各种方法(例如:getColor等)
实现换肤的方案:
a.静态修改theme主题方式
设置多套皮肤的theme;
styles.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="defaultcolor">@color/defalultcolor</item>
</style>
<style name="NewTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="defaultcolor">@color/newcolor</item>
</style>
</resources>
声明属性需要动态替换的样式属性
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="defaultcolor" format="reference|color" />
</resources>
为控件设置属性样式?attr/defaultcolor
<TextView
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是第一个fragment"
android:textColor="?attr/defaultcolor"
android:textSize="36dp" />
动态设置主题
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if( 1 == 1){
setTheme(R.style.AppTheme);
}else {
setTheme(R.style.NewTheme);
}
setContentView(R.layout.activity_main);
}
缺点是设置主题在setContentView之前,设置以后需要重启Activity;
b.动态修改样式(应用内/插件化-皮肤apk),不需要重启Activity
应用内
采用通过相同名称+不同皮肤后缀来区分不同皮肤,如实现黑白皮肤,文本颜色item_text_color有一套默认皮肤,一套黑色皮肤定义资源item_text_color,item_text_color_black;
String resName = mOutResource.getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);
resName+"_"+不同皮肤后缀
缺点:应用内多套皮肤时可能导致安装包过大;
插件化-皮肤apk
皮肤apk和主apk有相同皮肤资源名称,获取皮肤apk下的资源名称,需要和主apk下资源名称一致,设置在皮肤apk下皮肤资源 ;
String resName = mOutResource.getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);
mOutResource皮肤apk资源Resources;
动态修改视图皮肤需要解决两个问题:
1).缓存获取全部需要换肤的视图,换肤(资源)时执行换肤操作;
2).获取Resources资源(插件apk资源Resources),动态设置皮肤;
1.什么是一键换肤
所谓”一键换肤“就是通过一个接口调用,实现app范围内所有资源文件替换,包括文本,颜色,图片,动画等;
2.界面上那些东西可以换肤
例如TextView文字颜色,字体大小,ImageView的background等等;
res目录所有的资源几乎都可以替换,具体如下:
动画
背景图片
字体
字体颜色
字体大小
音频
视频
3.利用Hook实现一键换肤
什么是hook
如题,我是用hook实现一键换肤。那么什么是hook?
hook,钩子. 安卓中的hook技术,其实是一个抽象概念:对系统源码的代码逻辑进行"劫持",插入自己的逻辑,然后放行。注意:hook可能频繁使用java反射机制···
"一键换肤"中的hook思路
- "劫持"系统创建View的过程,我们自己来创建View
系统原本自己存在创建View的逻辑,我们要了解这部分代码,以便为我所用. - 收集我们需要换肤的View(用自定义view属性来标记一个view是否支持一键换肤),保存到变量中
劫持了 系统创建view的逻辑之后,我们要把支持换肤的这些view保存起来 - 加载外部资源包,调用接口进行换肤
外部资源包,是.apk
后缀的一个文件,是通过gradle
打包形成的。里面包含需要换肤的资源文件,但是必须保证,要换的资源文件,和原工程里面的文件名完全相同
.
4.Android创建视图源码分析
自己定义Activity继承自兼容AppCompatActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
创建Activity经常在onCreate()方法中调用setContentView(R.layout.xxx);设置要显示视图,那么如何将我们创建的xml布局文件转换为要显示View视图呢?
源码执行流程:
4.1.自定义Activity设置要显示的布局文件xml
setContentView(R.layout.activity_main);
4.2.调用兼容AppCompatActivity代理类AppCompatDelegate实现xml布局到View视图的转换
由于Android版本比较分散,需要兼容各个版本Android系统,AppCompatActivity定义实现不同版本AppCompatDelegate实现类;
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
4.3.AppCompatActivity定义实现不同版本AppCompatDelegate实现类
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV7(context, window, callback);
}
}
4.4.setContentView()方法最终调用代理类AppCompatDelegateImplV7中的方法
通过如下实现将layout的xml布局文件转换为View视图添加到父视图上contentParent;
LayoutInflater.from(mContext).inflate(resId, contentParent);
4.5.在LayoutInflater中实现基于XmlPullParser解析xml布局文件
4.6.xml解析调用createViewFromTag()通过控件名称和属性集合创建视图
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
View result = root;
//找到跟节点
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//获取跟节点名字
final String name = parser.getName();
//创建xml的layout布局文件跟视图
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//递归调用创建子视图
rInflateChildren(parser, temp, attrs, true);
// 添加到xml布局文件视图到父视图
if (root != null && attachToRoot) {
root.addView(temp, params);
}
//将xml布局的跟视图添加做为父视图
if (root == null || !attachToRoot) {
result = temp;
}
...部分源码
return result;
}
}
//创建xml布局文件的跟视图
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//递归创建xml布局文件的子视图,最后调用createViewFromTag()方法创建视图
rInflateChildren(parser, temp, attrs, true);
createViewFromTag(parent, name, context, attrs);
4.7.createViewFromTag()真正执行解析xml布局文件以后创建View视图
最终调用mFactory2实现视图创建
view = mFactory2.onCreateView(parent, name, context, attrs);
if(view==null)
通过反射创建View(例如自定义View)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {}
* @param parent:创建视图的父View
* @param name:xml标签使用的视图名称(例如:<TextView/>)
* @param context:上下文
* @param attrs:需要创建的xml标签的属性集;(例如:android:textColor="")
* @param ignoreThemeAttr:true忽略{android:theme}属性为被解析的视图,false相反;
4.8.mFactory2来源(调试发现最终调用的是AppCompatActivity代理类的onCreateView()方法)
AppCompatActivity设置LayoutInflater工厂
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
}
代理类AppCompatDelegateImplV7实现installViewFactory()方法
AppCompatDelegate(Activity代理类)
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV7)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
LayoutInflaterCompat是LayoutInflater兼容类
//LayoutInflater不同版本的实现
static final LayoutInflaterCompatImpl IMPL;
static {
final int version = Build.VERSION.SDK_INT;
if (version >= 21) {
IMPL = new LayoutInflaterCompatImplV21();
} else if (version >= 11) {
IMPL = new LayoutInflaterCompatImplV11();
} else {
IMPL = new LayoutInflaterCompatImplBase();
}
}
public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
IMPL.setFactory(inflater, factory);
}
static class LayoutInflaterCompatImplV11 extends LayoutInflaterCompatImplBase {
@Override
public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) {
LayoutInflaterCompatHC.setFactory(layoutInflater, factory);
}
}
FactoryWrapperHC实现了Factory2接口,包裹LayoutInflaterFactory接口,当调用Factroy2.
onCreateView方法时最后调用LayoutInflaterFactory(AppCompatDelegateImplV9).onCreateView()方法(class
AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
implements MenuBuilder.Callback, LayoutInflaterFactory),
AppCompatDelegateImplV9实现LayoutInflaterFactory接口
static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
final LayoutInflater.Factory2 factory2 = factory != null
? new FactoryWrapperHC(factory) : null;
//设置包裹LayoutInflaterFactory类做为factory2
inflater.setFactory2(factory2);
final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
// The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
// We will now try and force set the merged factory to mFactory2
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
// Else, we will force set the original wrapped Factory2
forceSetFactory2(inflater, factory2);
}
}
static class FactoryWrapperHC extends LayoutInflaterCompatBase.FactoryWrapper
implements LayoutInflater.Factory2 {
FactoryWrapperHC(LayoutInflaterFactory delegateFactory) {
super(delegateFactory);
}
@Override
public View onCreateView(View parent, String name, Context context,
AttributeSet attributeSet) {
return mDelegateFactory.onCreateView(parent, name, context, attributeSet);
}
}
LayoutInfalter设置factory2
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
LayoutInflaterCompat.setFactory(layoutInflater, this);
将代理类AppCompatDelegateImplV7的this设置为mFactory2;
AppCompatDelegateImplV7实现了LayoutInflaterFactory接口,mFactory2调用onCreateView()方法最终调用实现Factory2接口FactoryWrapperHC类,FactoryWrapperHC包裹类LayoutInflaterFactory,最后调用LayoutInflaterFactory(AppCompatDelegateImplV7).onCreateView()方法创建视图;
4.9.AppCompatViewInflater的createView()方法真正创建视图(AppCompatDelegateImplV9.onCreateView()调用AppCompatViewInflater.createView())
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
// We only want the View to inherit its context if we're running pre-v21
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
AppCompatViewInflater
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
创建Android的原生控件都是以AppCompat开头的兼容类例如:AppCompatSeekBar;
整个流程分析完成,我们发现最终调用mFactory2.onCreateView()和反射的方式创建视图;
就可以实现Factory2接口 ,给LayoutInflater重新设置实现自己逻辑Factory2(创建View视图,缓存视图和视图属性便于动态调用修改视图属性-文字颜色,字体大小,背景等)
5.Resources/AssetManager
调用AssetManager.addAssetPath("插件apk路径")指定要加载的皮肤apk,创建执行插件apk的Resources以便获取资源(图片,颜色,尺寸等);
需要动态设置皮肤资源就需要获取相关资源类(颜色,字体,背景等等),AssetManager.addAssetPath(String path)可以实现调用指定资源包;
Android中与资源相关的类主要有Resources、ResourcesImpl、ResourcesManager、Java层的AssetManager和Native层的AssetManager;
Resources:提供了大多数与应用开发直接相关的加载资源的方法,比如getColor(int resId)等。实现上是通过AssetManager来加载资源并进行解析。
AssetManager:Java层的AssetManager是对Native层AssetManager的封装,为上层提供了加载资源的方法。
加载资源包:一般而言,一个App至少会引用两个资源包:系统资源包和App的资源包。一个AssetManager对象可加载多个资源包。App在创建AssetManager的时候,会先加载系统资源包,再加载App资源包。这样,通过一个AssetManager对象就可以访问系统资源包和App资源包的资源了。
AssetManager.addAssetPath(String path)是一个@hide方法,调用这个方法可以加载指定的资源包。
访问资源:由于资源id包含了package id的信息,AssetManager通过解析资源id,即可知道从哪个资源包来加载资源。
6.实现关键类SkinFactory,SkinEngine
6.1SkinFactory实现Factory2接口,完成视图的创建,视图的缓存,全部视图的换肤操作;
/**
* 自定义Factory2,目的是拿到需要换肤的View和View样式
*/
public class SkinFactory implements LayoutInflater.Factory2 {
//预定义一个委托类,他负责按照系统原有逻辑来创建View
private AppCompatDelegate mDelegate;
//缓存要换肤的View
private List<SkinView> lisCacheSkinViews = new ArrayList<>();
/**
* 外部提供一个Set方法
* @param mDelegate
*/
public void setDelegate(AppCompatDelegate mDelegate) {
this.mDelegate = mDelegate;
}
/**
*
* @param parent
* @param name
* @param context
* @param attrs
* @return
*/
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//关键点1:执行系统代码里的创建View的过程,我们只是想加入自己的思想,并不是全盘接管
//系统创建出来的时候有可能为空,你问为啥?请全文搜索 “标记标记,因为” 你会找到你要的答案
View view = mDelegate.createView(parent, name, context, attrs);
if(view == null){//万一系统创建出来是空,那我们来补救
if(-1 == name.indexOf('.')){ //不包含,说明不带包名,那么我门帮他加上包名
view = createViewByPrefix(context, name, prefixs, attrs);
}else {
view = createViewByPrefix(context, name, null, attrs);
}
}
//关键点2:收集需要换肤的View
collectSkinView(context, attrs, view);
return view;
}
/**
* 收集换肤的控件
* 收集的方式是:通过自定义isSupport,从创建出来的很多View中,找到支持换肤的那些,保存到map中
* @param context
* @param attrs
* @param view
*/
private void collectSkinView(Context context, AttributeSet attrs, View view){
//获取我们自己定义的属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Skinable);
boolean isSupport = typedArray.getBoolean(R.styleable.Skinable_isSupport, false);
if(isSupport){//找到支持换肤的View
final int Len = attrs.getAttributeCount();
HashMap<String, String> attrMap = new HashMap<>();
for(int i=0; i< Len; i++){//遍历所有属性
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
attrMap.put(attrName, attrValue);//全部存起来
}
SkinView skinView = new SkinView();
skinView.view = view;
skinView.attrsMap = attrMap;
//将可换肤的View放到listCacheSkinView中
lisCacheSkinViews.add(skinView);
}
}
/**
* 公开对外的换肤接口
*/
public void changeSkin(){
for(SkinView skinView : lisCacheSkinViews){
skinView.changeSkin();
}
}
/**
* Factory2是继承Factory的,所以这次主要重写Factory2的onCreatteView逻辑,就不必理会Factory的重写方法
* @param name
* @param context
* @param attrs
* @return
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
/**
* 所谓hook,要懂源码,懂了之后再劫持系统逻辑,加入自己的逻辑。
* 那么,既然懂了,系统的有些代码,直接拿过来用,也无可厚非。
*/
//*******************************下面一大片,都是从源码里面抄过来的,并不是我自主设计******************************
// 你问我抄的哪里的?到 AppCompatViewInflater类源码里面去搜索:view = createViewFromTag(context, name, attrs);
static final Class<?>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};//
final Object[] mConstructorArgs = new Object[2];//View的构造函数的2个"实"参对象
private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>();//用映射,将View的反射构造函数都存起来
static final String[] prefixs = new String[]{//安卓里面控件的包名,就这么3种,这个变量是为了下面代码里,反射创建类的class而预备的
"android.widget.",
"android.view.",
"android.webkit."
};
private final View createViewByPrefix(Context context, String name, String[] prefixs, AttributeSet attrs){
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
if(constructor == null){
try {
if(prefixs != null && prefixs.length > 0){
for(String prefix : prefixs){
clazz = context.getClassLoader().loadClass(prefix != null ? (prefix+name) : name).asSubclass(View.class);
if(clazz != null) break;
}
}else {
if(clazz == null){
clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
}
}
if(clazz == null){
return null;
}
//拿到构造方法
constructor = clazz.getConstructor(mConstructorSignature);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
} catch (NoSuchMethodException e) {
e.printStackTrace();
return null;
}
constructor.setAccessible(true);
//然后缓存起来,下次再用,就直接从内存中去取
sConstructorMap.put(name, constructor);
}
Object[] args = mConstructorArgs;
try {
args[0] = context;
args[1] = attrs;
//通过反射创建View对象
//执行构造函数,拿到View对下
final View view = constructor.newInstance(args);
return view;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}finally {
args[0] = null;
args[1] = null;
}
return null;
}
static class SkinView{
View view;
HashMap<String, String> attrsMap;
public void changeSkin(){
if (!TextUtils.isEmpty(attrsMap.get("background"))) {//属性名,例如,这个background,text,textColor....
int bgId = Integer.parseInt(attrsMap.get("background").substring(1));//属性值,R.id.XXX ,int类型,
// 这个值,在app的一次运行中,不会发生变化
String attrType = view.getResources().getResourceTypeName(bgId); // 属性类别:比如 drawable ,color
if (TextUtils.equals(attrType, "drawable")) {//区分drawable和color
view.setBackgroundDrawable(SkinEngine.getInstance().getDrawable(bgId));//加载外部资源管理器,拿到外部资源的drawable
} else if (TextUtils.equals(attrType, "color")) {
view.setBackgroundColor(SkinEngine.getInstance().getColor(bgId));
}
}
if (view instanceof TextView) {
if (!TextUtils.isEmpty(attrsMap.get("textColor"))) {
int textColorId = Integer.parseInt(attrsMap.get("textColor").substring(1));
((TextView) view).setTextColor(SkinEngine.getInstance().getColor(textColorId));
}
if (!TextUtils.isEmpty(attrsMap.get("textSize"))) {
int textSizeId = Integer.valueOf(attrsMap.get("textSize").substring(1));
((TextView) view).setTextSize(SkinEngine.getInstance().getTextSize(textSizeId));
}
}
// //那么如果是自定义组件呢
// if (view instanceof ZeroView) {
// //那么这样一个对象,要换肤,就要写针对性的方法了,每一个控件需要用什么样的方式去换,尤其是那种,自定义的属性,怎么去set,
// // 这就对开发人员要求比较高了,而且这个换肤接口还要暴露给 自定义View的开发人员,他们去定义
// // ....
// }
}
}
}
6.2SkinEngine加载插件apk的对应的Resources,(Resources用插件apk的资源)实现更换皮肤的各种方法(例如:getColor等)
/**
* 单例
*/
public class SkinEngine {
private final static SkinEngine instance = new SkinEngine();
public static SkinEngine getInstance(){
return instance;
}
public void init(Context context){
mContext = context.getApplicationContext();
//使用application的目的是,如果万一传进来的是Activity对象
//那么它被静态对象instance所持有,这个Activity就无法释放了
}
private Resources mOutResource; //资源管理器
private Context mContext; //上下文
private String mOutPkgName; //外部资源包packageName
//加载外部资源包
public void load(final String path){ //path 是外部传入的apk文件名
File file = new File(path);
if(!file.exists()){
return;
}
//取得PackageManager引用
PackageManager mPm = mContext.getPackageManager();
//“检索在包归档文件中定义的应用程序包的总体信息”,说人话,外界传入了一个apk的文件路径,这个方法,拿到这个apk的包信息,这个包信息包含什么?
PackageInfo mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
mOutPkgName = mInfo.packageName; //先把包名存起来
AssetManager assetManager;//资源管理器
//关键技术点3 通过反射获取AssetManager 用来加载外面的资源包
try {
//反射创建AssetManager对象,为何要反射?使用反射,是因为他这个类内部的addAssetPath方法是hide状态
assetManager = AssetManager.class.newInstance();
//addAssetPath方法可以加载外部的资源包
//为什么要反射执行这个方法?因为它是hide的,不直接对外开放,只能反射调用
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, path); //反射执行方法
mOutResource = new Resources(assetManager, //资源管理器
mContext.getResources().getDisplayMetrics(), //屏幕参数
mContext.getResources().getConfiguration()); //资源配置
//最终创建出一个 "外部资源包"mOutResource ,它的存在,就是要让我们的app有能力加载外部的资源文件
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* 提供外部资源包里面的颜色
* @param resId
* @return
*/
public int getColor(int resId) {
if (mOutResource == null) {
return resId;
}
String resName = mContext.getResources().getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "color", mOutPkgName);
if (outResId == 0) {
return resId;
}
return mOutResource.getColor(outResId);
}
/**
* 提供外部资源包里的图片资源
* @param resId
* @return
*/
public Drawable getDrawable(int resId) {//获取图片
if (mOutResource == null) {
return ContextCompat.getDrawable(mContext, resId);
}
String resName = mContext.getResources().getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);
if (outResId == 0) {
return ContextCompat.getDrawable(mContext, resId);
}
return mOutResource.getDrawable(outResId);
}
/**
* 提供外部资源包里的字体大小
* @param resId
* @return
*/
public float getTextSize(int resId) {//获取字体大小
if (mOutResource == null) {
return mContext.getResources().getDimension(resId);
}
String resName = mContext.getResources().getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "textSize", mOutPkgName);
if (outResId == 0) {
return mContext.getResources().getDimension(resId);
}
return mOutResource.getDimension(outResId);
}
//..... 这里还可以提供外部资源包里的String,font等等等,只不过要手动写代码来实现getXX方法
}
注意事项:
通过资源id找到资源名称,mContext是当前主app的上下文;
String resName = mContext.getResources().getResourceEntryName(resId);
mOutResource指的是插件apk的Resources获取和主app相同名称的资源替换显示;
int outResId = mOutResource.getIdentifier(resName, "textSize", mOutPkgName);
6.3实例使用
初始化"换肤引擎"指定当前上下文
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
//初始化换肤引擎
SkinEngine.getInstance().init(this);
}
}
在基类Activity为LayoutInflator注入Facotry2类,其他Activity实现基类;
/**
* Activity积类
*/
public class BaseActivity extends AppCompatActivity {
private boolean ifAllowChangeSkin=true;
private SkinFactory mSkinFactory;
private String mCurrentSkin;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// TODO: 关键点1:hook(劫持)系统创建view的过程
if (ifAllowChangeSkin) {
mSkinFactory = new SkinFactory();
mSkinFactory.setDelegate(getDelegate());
LayoutInflater layoutInflater = LayoutInflater.from(this);
layoutInflater.setFactory2(mSkinFactory);//劫持系统源码逻辑
}
super.onCreate(savedInstanceState);
}
}
在设置界面调用换肤操作, changeSkin("apk插件的名字"),具体apk插件存在哪和如何放入存储卡加载插件apk,下面仅仅是插件apk下载完成以后实现的方法;
changeSkin("skinmodule-debug.apk");
protected void changeSkin(String path) {
if (ifAllowChangeSkin) {
File skinFile = new File(Environment.getExternalStorageDirectory(), path);
SkinEngine.getInstance().load(skinFile.getAbsolutePath());//加载外部资源包
mSkinFactory.changeSkin();//执行换肤操作
mCurrentSkin = path;
}
}
6.4主app和插件app设置需要换肤资源用相同的名称
6.5自定义isSupport属性设置View是否支持换肤
<resources><!--TODO: 关键技术点2 通过自定义属性来标识哪些view支持换肤-->
<declare-styleable name="Skinable">
<!--TODO: isSupport=true标识当前控件支持换肤-->
<attr name="isSupport" format="boolean" />
</declare-styleable>
</resources>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:text="第一个"
android:id="@+id/tv_one"
android:gravity="center"
android:textSize="@dimen/tabsize"
app:isSupport="true"
/>
7.显示效果
以下效果实现动态替换字体大小,背景颜色;
已经创建的Activity界面可能需要监听器,监听换肤操作执行换肤,新创建的页面将使用插件apk皮肤资源;
8.注意事项
a.skinmodule创建module只需要管理assets和res下的资源即可,需要替换资源名称要和app一致;
b.皮肤包 skin_plugin module的gradle sdk版本最好和app module的保持完全一致,否则无法保证不会出现奇葩问题;
c.skinmodule生成的具体保存位置自己定义,只需要创建插件Resources时执行插件路径apk即可;
参考:
https://www.jianshu.com/p/4c8d46f58c4f
https://github.com/18598925736/HookSkinDemoFromHank