常用的app中,许多都带有了换肤功用,换肤是为了换资源文件,也便是res下边的资源。
<ImageView
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="@drawable/toolbar" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="skinSelect"
android:text="挑选皮肤"
android:textColor="?colorAccent" />
咱们换肤,比如像上边的imageView和Button,首要便是要替换他的布景或许Color,这就需求了解资源的加载流程了。
资源的加载流程终究都会走到xml解析:
public void setContentView:(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
// 递归解析资源
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
+ Integer.toHexString(resource) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
终究都会走到这个办法里面android.view.LayoutInflater#createViewFromTag(android.view.View, java.lang.String, android.content.Context, android.util.AttributeSet, boolean):
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
try {
// 这儿测验创立View,咱们换肤需求着重重视这儿。这是由于tryCreateView运用了mFactory2来创立View,咱们能够经过这儿来完成自己的逻辑。
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
@UnsupportedAppUsage(trackingBug = 122360734)
@Nullable
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;
// 重点重视mFactory2。
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;
}
mFactory2是何时创立的呢?在Activity的onCreate的时分,会调用这个办法:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
之后体系在创立View的时分就会走这段代码了。
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
// 省略无关代码。。。。。
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* 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 */
);
}
咱们能够看到,终究走到了androidx.appcompat.app.AppCompatViewInflater#createView(android.view.View, java.lang.String, android.content.Context, android.util.AttributeSet, boolean, boolean, boolean, boolean) 这儿面会依据不同的View来完成对应的实例化,这也是为什么咱们在xml中声明的TextView,会在初始化之后变成 AppCompatTextView。
public final View createView(@Nullable View parent, @NonNull 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;
}
已然谷歌能够替换掉TextView为AppCompatTextView,是不是咱们也能够完成相似的作用?便是把TextView换成咱们的TextView。
LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
// 完成了TextView被Button替换的作用。
if (TextUtils.equals(name, "TextView")) {
Button button = new Button(context);
button.setText("AAAA");
return button;
}
return null;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return null;
}
});
可是需求留意,Factory2只能设置一次,上边的逻辑需求在onCreate的super之前调用。或许经过反射修正mFactorySet
的值。
public void setFactory2(Factory2 factory) {
// mFactorySet有值就会抛出反常。
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);
}
}
Activity发动进程中资源的加载流程
performLaunchActivity
–>createBaseContextForActivity(r)
–> ContextImpl.createActivityContext()
–> context.setResources
–> createResources
–> ResourceImpl impl = findOrCreateResourcesImplForKeyLocked
–> impl = createResourcesImpl(key)
–> assets = createAssetManager(key);
–> builder.addApkAssets(loadApkAssets(key.mResDir, false)
AssetManager 加载资源–>资源途径–>默许传入的资源途径key.mResDir(app下面的res,咱们能够反射改成皮肤包的资源途径—-Resources AssetManager皮肤包的)
不能改变原有的资源加载。独自创立一个AssetManager–>专门加载皮肤包的资源,hide的api是能够直接反射的。
换肤的整体思路
(1)知道xml的View怎样解析?
(2)怎么阻拦体系的创立流程(setFactory2()完成,这样咱们就能自己控制对创立的View的操作了);
(3)阻拦后怎样做?重写体系创立进程的代码(仿制即可);
(4)搜集View以及对应的特点(一切的页面都需求);
(5)拿到皮肤包(也便是apk),进行替换;
(6)怎么运用?运用插件化技能,只用插件的resource。体系的资源是经过Resource和AssetManager进行的,当然,Resource终究也是走的AssetManager.
咱们选用换肤的终究的办法:
—>体系的资源怎么加载?Resource、AssetManager
—>经过Hook技能,创立一个AssetManager。不能用同一个,不影响原流程,由于会形成资源ID抵触,专门加载皮肤包的资源。
—>经过反射addAssetPath 放入皮肤包的途径,从而得到 加载皮肤包资源AssetManager
。
—>经过app的资源ID—>找到app的资源Name—>皮肤包的资源ID(为什么要这么做?由于咱们换肤的时分给的资源的名肯定是相同的,可是这两个资源的ID在编译之后,在app和皮肤包里面一般是不同的,所以咱们需求经过这种办法来获取到皮肤包的资源ID然后设置给app)
AssetManager 加载资源–>资源途径–>默许传入的资源途径key.mResDir(app下面的res,改成皮肤包的资源途径—-Resources AssetManager皮肤包的)
代码完成
首先咱们需求自界说一个 SkinLayoutInflaterFactory
来阻拦创立View的流程,由于咱们需求记载每一个View的特点,以便后续换肤的时分来对特点进行修正。这儿之所以还需求完成Observer,是由于咱们换肤之后,需求运用观察者形式告诉一切的界面进行更新。因而每一个界面都应该注册一个观察者。
public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2, Observer {
因而,咱们需求界说一个ApplicationActivityLifecycle,用来在Activity创立的时分,将每一个观察者都传递进去,也便是每一个SkinLayoutInflaterFactory,这样每一个界面都适当于是观察者。
public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {
那么被观察者是谁呢?咱们界说一个被观察者,在加载新皮肤之后,会告诉一切的观察者,也便是一切的页面,去更新。
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);
}
}
别的,咱们还需求一个类,用来解析一切的View的特点解析,同时也供给更新特点的办法,以便确定咱们对哪些特点来进行替换,一般的,咱们不需求对一切的特点都进行替换:
/**
* 这儿面放了一切要换肤的view所对应的特点
*/
public class SkinAttribute {
/**
* 只要这些特点,咱们才需求进行替换,其他的不必。
*/
private static final List<String> mAttributes = new ArrayList<>();
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
/**
* 记载换肤需求操作的View与特点信息
*/
private final List<SkinView> mSkinViews = new ArrayList<>();
/**
* 记载下一个VIEW身上哪几个特点需求换肤textColor/src,每一个页面的view都需求搜集。
*
* @param view
* @param attrs
*/
public void look(View view, AttributeSet attrs) {
List<SkinPair> mSkinPars = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//获得特点名 textColor/background
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
// 获取特点值
String attributeValue = attrs.getAttributeValue(i);
// 比如color 以#最初表明写死的颜色 不行用于换肤
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
// 以 ?最初的表明运用 特点
if (attributeValue.startsWith("?")) {
int attrId = Integer.parseInt(attributeValue.substring(1));
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
// 正常以 @ 最初
resId = Integer.parseInt(attributeValue.substring(1));
}
// 保存特点名以及对应的ID,每一个特点都有对应的ID,为后边映射皮肤包ID做准备。
SkinPair skinPair = new SkinPair(attributeName, resId);
mSkinPars.add(skinPair);
}
}
if (!mSkinPars.isEmpty() || view instanceof SkinViewSupport) {
SkinView skinView = new SkinView(view, mSkinPars);
// 假如挑选过皮肤 ,调用 一次 applySkin 加载皮肤的资源
skinView.applySkin();
mSkinViews.add(skinView);
}
}
/*
对一切的view中的一切的特点进行皮肤修正
*/
public void applySkin() {
// 第一层循环,找到一切的View
for (SkinView mSkinView : mSkinViews) {
mSkinView.applySkin();
}
}
static class SkinView {
View view;
//这个View的能被 换肤的特点与它对应的id 集合
List<SkinPair> skinPairs;
public SkinView(View view, List<SkinPair> skinPairs) {
this.view = view;
this.skinPairs = skinPairs;
}
/**
* 对一个View中的一切的特点进行修正
*/
public void applySkin() {
applySkinSupport();
// 第二层循环,找到一切的view对应的特点,然后进行设置,完成真实的作用更改。
for (SkinPair skinPair : skinPairs) {
Drawable left = null, top = null, right = null, bottom = null;
switch (skinPair.attributeName) {
case "background":
// 这一行就完成了:经过app的资源ID--->找到app的资源Name--->皮肤包的资源ID
Object background = SkinResources.getInstance().getBackground(skinPair.resId);
//布景可能是 @color 也可能是 @drawable
if (background instanceof Integer) {
view.setBackgroundColor((int) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src":
// 这一行就完成了:经过app的资源ID--->找到app的资源Name--->皮肤包的资源ID
background = SkinResources.getInstance().getBackground(skinPair.resId);
if (background instanceof Integer) {
((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
background));
} else {
((ImageView) view).setImageDrawable((Drawable) background);
}
break;
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPair.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
}
}
}
private void applySkinSupport() {
if (view instanceof SkinViewSupport) {
((SkinViewSupport) view).applySkin();
}
}
}
static class SkinPair {
/**
* 特点名
*/
String attributeName;
/**
* 对应的资源id
*/
int resId;
public SkinPair(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
}
总结
(1)经过自界说 LayoutInflater.Factory2
来完成可控制的View创立进程,让咱们能够对一切的创立的View,搜集他们需求换肤的特点,以及特点对应的ID;
(2)界说观察者与被观察者,观察者便是每一个页面,被观察者便是换肤类。当换肤类触发了换肤之后,会经过状态分发让一切页面更新,假如还没有发动的Activiyt是否就不能更新了呢?当然不会,由于咱们的能够直接调用 SkinManager.getInstance().loadSkin(skinPkg),来完成未发动的页面的更新。
(3)经过app的资源ID—>找到app的资源Name—>皮肤包的资源ID(为什么要这么做?由于咱们换肤的时分给的资源的名肯定是相同的,可是这两个资源的ID在编译之后,在app和皮肤包里面一般是不同的,所以咱们需求经过这种办法来获取到皮肤包的资源ID然后设置给app),这也是为什么咱们在最初解析的时分需求运用下边的代码:
static class SkinPair {
/**
* 特点名
*/
String attributeName;
/**
* 对应的资源id
*/
int resId;
public SkinPair(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
几个问题
1、自界说View的制作流程:
1、View几个结构办法的区别?
public class CustomView extends View {
/**
* 一般在直接new一个view的时分运用
* @param context
*/
public CustomView(Context context) {
super(context);
}
/**
* 一般在layout文件中运用的时分回调用,关于它的特点(包括自界说特点)都会在attrs中传递进来。
* @param context
* @param attrs
*/
public BigView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
// <com.example.MyView
// android:layout_width="wrap_content"
// android:layout_height="wrap_content"
// custom:myAttribute="value" />
//假如在这儿这样写就会调用到三个参数的结构办法
//this(context, attrs,0);
}
/**
* @param context
* @param attrs
* @param defStyleAttr 默许的style,指的是当期application或许activity所用的theme中默许的style,
* 且只要清晰调用的时分才会收效,
* 如 this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
* 留意:即便在view中运用了style这个特点,也不会调用这个结构办法,所以这个结构办法
* 也不考虑。
*/
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
}
/**
* android5.0今后的api才有
* 假如第三个参数为0或许没有界说defStyleAttr时,第四个参数才起作用,它是style的引证.
* 不同于`defStyleAttr`,`defStyleAttr`是在当前主题中查找样式,
* 而`defStyleRes`直接引证一个清晰的样式资源。这个结构办法供给了更多的灵活性来处理视图的样式。
* @param context
* @param attrs
* @param defStyleAttr
* @param defStyleRes
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BigView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
3、 咱们自界说View的时分,在运用的时分需求加上全途径,为什么体系内置的LinearLayout,Relativelayout这些不必呢?请看这儿:
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
由于他们在创立的时分把前缀加上去了。
4、 换肤的进程中,比如Color中直接经过#FF00000,这种办法, 是无法替换的,由于这种只要ID,而没有name。无法映射到皮肤包中。