一、前言:
参考:https://github.com/ximsfei/Android-skin-support#toc14
换肤流程.png换肤的思路: (观察者模式、aop、Hook技术)
- 知道xml的View 怎么解析的?? ---》
- 如何拦截系统的创建流程? setFactory2 可以拦截 --- aop的思路去实现
- 拦截后怎么做?? 重写系统的创建过程的代码(复制)
- 收集View以及属性,每个Activity的View 及属性都需要收集
- 创建皮肤包 -- apk(只有资源文件的皮肤包)
- 如何使用??只用插件的res(插件的 java、res)
1. 系统的资源是如何加载的? Resources、Assetmanager
2. 通过Hook技术,创建一个Assetmanager 专门加载皮肤包的资源
3. 通过 反射 addAssetPath 方法放入皮肤包的路径 从而得到 加载皮肤包资源的 Assetmanager
4. 首先通过 app的资源id --》 找到 app的资源name --》 皮肤包的资源id
注意:
- Hook技术 --- 反射、动态代理的使用
- 通过反射、动态代理等技术 改变代码的原有流程
二、布局创建流程
1、布局加载思路
1、Activity类setContentView()->调用到AppCompatDelegateImpl类的setContentView()方法;
2、setContentView()方法中 调用 LayoutInflater.from(mContext).inflate(resId,contentParent)加载布局;
3、点击inflate方法跳转到LayoutInflater.java类中的inflate()方法
4、可以看到inflate()方法中有 XmlResouerceparser解析布局的方法,再通过inflate()解析;
5、在这个inflate()方法中root view 通过createViewfromTag()创建,子view通过rInflateChildren()方法创建;
6、点击进入createViewFromTag()方法,里面通过name.indexOf('.')来说明是否是自定义View,系统 的view加上前缀;
7、点你里面的onCreateView()方法,会加上 “android.view”的前缀;
8、一直往下点击onCreateView(),可以看到使用全类名,反射获取对象,获得对应控件;
9、在第6步的时候,view有可能不为null,看到上方调用了 tryCreateView()方法,后面的view不为null,不往下执行了。
10、在tryCreateView()方法中,mFactory2方法,用它进行换肤;
2、正常view的流程
1、特别注意:在第6步的时候,正常流程,不是直接往下执行的,先执行了tryCreateView()方法;
2、Activity中点击onCreate()->AppCompatActivity.java类中(新版本在initDelegate()方法中调用了delete.installViewFactory()方法),delete.installViewFactory()
3、进入AppCompatDelegateImpl.java中,查看installViewFactory()方法的实现,看到里创建了mFactory2类;
4、在onCreateView()方法中,会创建AppCompateViewInflater();
5、在AppCompatViewInflater类中,会创建TextView、ImageView等控件;
//tryCreateView
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
return view;
}
//AppCompatViewInflater类中,创建对应的控件
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 = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
case "ToggleButton":
view = createToggleButton(context, attrs);
verifyNotNull(view, name);
break;
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
}
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 its android:onClick
checkOnClickListener(view, attrs);
backportAccessibilityAttributes(context, view, attrs);
}
return view;
}
三、换肤
1、整体流程(获取拦截)
1、换肤使用Application.ActivityLifecycleCallbacks 接口,来实现换肤,对每个Activity或者Fragment更改view;
2、ActivityLifecycleCallbacks是在super.onCreate()方法时调用;
3、点击进入Application类,看到ActivityLifecycleCallbacks的onActivitypreCreated()方法;
4、在Activity.java中dispatchActivityPreCreate()方法中,调用Application.ActivityLifecycleCallbacks类的onActivityPreCreated()方法;
2、怎么写代码
public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {
private Observable mObserable;
private ArrayMap<Activity, SkinLayoutInflaterFactory> mLayoutInflaterFactories = new
ArrayMap<>();
public ApplicationActivityLifecycle(Observable observable) {
mObserable = observable;
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
/**
* 更新状态栏
*/
SkinThemeUtils.updateStatusBarColor(activity);
/**
* 更新布局视图
*/
//获得Activity的布局加载器
LayoutInflater layoutInflater = activity.getLayoutInflater();
try {
//Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
//如设置过抛出一次
//设置 mFactorySet 标签为false
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
//使用factory2 设置布局加载工程
SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory
(activity);
LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory);
mLayoutInflaterFactories.put(activity, skinLayoutInflaterFactory);
mObserable.addObserver(skinLayoutInflaterFactory);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
SkinLayoutInflaterFactory observer = mLayoutInflaterFactories.remove(activity);
SkinManager.getInstance().deleteObserver(observer);
}
}
/**
* 用来接管系统的view的生产过程
*/
public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2, Observer {
private static final String[] mClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app.",
"android.view."
};
//记录对应VIEW的构造函数
private static final Class<?>[] mConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final HashMap<String, Constructor<? extends View>> mConstructorMap =
new HashMap<String, Constructor<? extends View>>();
// 当选择新皮肤后需要替换View与之对应的属性
// 页面属性管理器
private SkinAttribute skinAttribute;
// 用于获取窗口的状态框的信息
private Activity activity;
public SkinLayoutInflaterFactory(Activity activity) {
this.activity = activity;
skinAttribute = new SkinAttribute();
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//换肤就是在需要时候替换 View的属性(src、background等)
//所以这里创建 View,从而修改View属性
View view = createSDKView(name, context, attrs);
if (null == view) {
view = createView(name, context, attrs);
}
//这就是我们加入的逻辑
if (null != view) {
//加载属性
skinAttribute.look(view, attrs);
}
return view;
}
private View createSDKView(String name, Context context, AttributeSet
attrs) {
//如果包含 . 则不是SDK中的view 可能是自定义view包括support库中的View
if (-1 != name.indexOf('.')) {
return null;
}
//不包含就要在解析的 节点 name前,拼上: android.widget. 等尝试去反射
for (int i = 0; i < mClassPrefixList.length; i++) {
View view = createView(mClassPrefixList[i] + name, context, attrs);
if (view != null) {
return view;
}
}
return null;
}
private View createView(String name, Context context, AttributeSet
attrs) {
Constructor<? extends View> constructor = findConstructor(context, name);
try {
return constructor.newInstance(context, attrs);
} catch (Exception e) {
}
return null;
}
private Constructor<? extends View> findConstructor(Context context, String name) {
Constructor<? extends View> constructor = mConstructorMap.get(name);
if (constructor == null) {
try {
Class<? extends View> clazz = context.getClassLoader().loadClass
(name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
mConstructorMap.put(name, constructor);
} catch (Exception e) {
}
}
return constructor;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
//如果有人发送通知,这里就会执行
@Override
public void update(Observable o, Object arg) {
SkinThemeUtils.updateStatusBarColor(activity);
skinAttribute.applySkin();
}
}
四、更换资源
1、流程思路
1、Resourse Assermanager->Context
2、ActivityThread.java类中 preformLaunchActivity()方法中,创建ContextImpl类
3、ContextImpl类中创建createActivityCotext()-》new ContextImpl()
4、然后cotext.serResourse()加载资源,比如:getResDir()
5、ContextImpl类中调用createBaseTokenResourse()
6、进入ResourcesManager.java里面创建了resourcesKey,下面会调用createResources()方法;
7、在createResources()方法中调用findOrCreateResourcesImplForKeyLocked()方法
8、进入findCreateResourcesImplForKeyLocaked()方法中创建createResourcesImpl(key)
9、点击createResourcesImp方法,可以看到里面创建了AssetManager类;
10、进入AssetManager类,看到builder.addApkAssets()
- AssertManager 加载资源 --》 资源路径 --》 默认传入的资源路径 key.mResDir,app下面的res
(改成皮肤包的资源路径 ---Resources AssertManager 皮肤包的)
- Hook的思路:不能改变原有的资源加载,单独创建一个AssertManager--> 专门加载皮肤包的资源
首先通过 app的资源id --》 找到 app的资源name --》 皮肤包的资源id
// app的resId
String resName=mAppResources.getResourceEntryName(resId); // 通过app的resId 找到 resName
String resType=mAppResources.getResourceTypeName(resId);// 通过app的resId 找到 类型,layout、drawable
// 获取对应皮肤包的资源Id
int skinId=mSkinResources.getIdentifier(resName,resType,mSkinPkgName);
public class SkinManager extends Observable {
private volatile static SkinManager instance;
/**
* Activity生命周期回调
*/
private ApplicationActivityLifecycle skinActivityLifecycle;
private Application mContext;
/**
* 初始化 必须在Application中先进行初始化
*/
public static void init(Application application) {
if (instance == null) {
synchronized (SkinManager.class) {
if (instance == null) {
instance = new SkinManager(application);
}
}
}
}
private SkinManager(Application application) {
mContext = application;
//共享首选项 用于记录当前使用的皮肤
SkinPreference.init(application);
//资源管理类 用于从 app/皮肤 中加载资源
SkinResources.init(application);
//注册Activity生命周期,并设置被观察者
skinActivityLifecycle = new ApplicationActivityLifecycle(this);
application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
//加载上次使用保存的皮肤
loadSkin(SkinPreference.getInstance().getSkin());
}
public static SkinManager getInstance() {
return instance;
}
/**
* 记载皮肤并应用
*
* @param skinPath 皮肤路径 如果为空则使用默认皮肤
*/
public void loadSkin(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
//还原默认皮肤
SkinPreference.getInstance().reset();
SkinResources.getInstance().reset();
} else {
try {
//反射创建AssetManager 与 Resource
AssetManager assetManager = AssetManager.class.newInstance();
//资源路径设置 目录或压缩包
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager, skinPath);
//宿主app的 resources;
Resources appResource = mContext.getResources();
//根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建Resources
Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics(),
appResource.getConfiguration());
//获取外部Apk(皮肤包) 包名
PackageManager mPm = mContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
String packageName = info.packageName;
SkinResources.getInstance().applySkin(skinResource, packageName);
//记录路径
SkinPreference.getInstance().setSkin(skinPath);
} catch (Exception e) {
e.printStackTrace();
}
}
//通知采集的View 更新皮肤
//被观察者改变 通知所有观察者
setChanged();
notifyObservers(null);
}
}
四、手动换肤的一个小例子
package com.leo.lsn2;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.LayoutInflaterCompat;
public class Factory2Activity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 必须在 super 之前调用
LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
// if (TextUtils.equals(name, "TextView")) {
// Button btn = new Button(Factory2Activity.this);
// btn.setText("我是一个按钮");
// return btn;
// }
return null;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return null;
}
});
super.onCreate(savedInstanceState);
// 在super之后调用,反射 设置mFactorySet = false;
setContentView(R.layout.activity_factory2);
TextView tv = findViewById(R.id.tv);
Log.e("leo", "tv: " + tv);
TextView tv2 = new TextView(this);
Log.e("leo", "tv2: " + tv2);
}
}
网友评论