学习是一个过程。

onCreate加载布局,是不是都很熟悉。
@Override
protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_happy);
}
AppCompatActivity的onCreate()方法,注意这不是Activity的onCreate方法。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
//注意此方法是做换肤的关键。
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
AppCompatDelegatelmpl的installViewFactory方法,AppCompatDelegatelmpl实现了Factory2接口。
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
//AppCompatDelegatelmpl实现了Factory2接口
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");
}
}
}
LayoutInflater中的setFactory2方法,此方法不允许重复设置值,如果设置值会产生异常,所以如果做动态换肤设置Factory2时,要放在super.onCreate()方法之前,防止异常退出。
public void setFactory2(Factory2 factory) {
//从这可以看出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;
//mFactory与mFactory2一块赋值,mFractory2是按照扩展的方法进行开发的。
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
继续分析setContentView()方法,AppCompatActivity中的setContentView调用的是AppCompatDelegatelmpl的方法。
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
AppCompatDelegatelmpl的setContentView方法,此方法主要是加载我们自定义的布局,将布局添加到容器中。
public void setContentView(int resId) {
//主要是初始化根布局,用来存放我们自定义的布局。
ensureSubDecor();
//存放我们自定义布局的View,此View的类型是FrameLayout
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//加载自定义布局,将布局添加到contentParent中。
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
LayoutInflater的inflate方法,此方法中有以后插件化用到的关键代码,此处先留意一下,以后有机会再进行分享插件化相关的技术。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
//此处是做插件化的关键,activity自定义getResources()方法,用来生产插件对应的资源。
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
//继续分析
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
继续分析inflate方法,其重要流程是创建根布局,然后创建
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
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();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
//处理 merge 标签
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//自定义View的根布局,就是自己写的布局的根布局。
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
//把自定义的xml所有除根布局之外的控件全部实例化然后添加进根布局
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.添加到根布局中
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
LayoutInflater的createViewFromTag方法,注意这里有一个BlinkLayout的闪烁小彩蛋,用来闪烁布局。
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();
}
``//闪烁的菜单,是为了庆祝??? 1995年庆祝什么节日?
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
//如果是AppCompatActivity在这初始化,这个可以自己创建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);
}
//如果是Activity在这初始化。
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//不带包路径的View,最终都会调用到createView的这个方法
if (-1 == name.indexOf('.')) {
//这个最终会调用createView(name,“android.view.”,attrs),携带android.view前缀。
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
我们跟踪一下Layoutlnflater的createView方法,
//利用反射创建对象,为啥不直接new对象呢?因为有些不能访问到?
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
//鉴别构造方法是否失效,主要为了鉴别类加载器。
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
//为了增加效率,增加了缓存。
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassCastException e) {
// If loaded class is not a View subclass
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// If loadClass fails, we should propagate the exception.
throw e;
} catch (Exception e) {
final InflateException ie = new InflateException(
attrs.getPositionDescription() + ": Error inflating class "
+ (clazz == null ? "" : clazz.getName()), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
继续分析mFactory2.onCreateView()的方法,其最终会调用到AppCompatDelegatelmpl的createView(),此函数主要对mAppCompatViewInflater进行初始化,然后调用其createView()方法。
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName =
a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if ((viewInflaterClassName == null)
|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
// Either default class name or set explicitly to null. In both cases
// create the base inflater (no reflection)
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
try {
Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
Log.i(TAG, "Failed to instantiate custom view inflater "
+ viewInflaterClassName + ". Falling back to default.", t);
mAppCompatViewInflater = new AppCompatViewInflater();
}
}
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
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 */
);
}
AppCompatViewInflater的createView()来创建View,其最终都转换为AppCompat对应的组件,
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
//这里对View进行转换,自动转换为AppCompat对应的View。
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);
}
//如果不是以上对应的View,则调用以下方法进行创建。
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);
}
return view;
}
AppCompatViewInflater的createViewFromTag()方法,其与LayoutInflater的createViewFromTag方法有些类似,
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
if (-1 == name.indexOf('.')) {
//尝试增加不同的前缀进行创建,用反射,如果 反射失败则加载不成功,再重新尝试。
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createViewByPrefix(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
尝试用不同的前缀反射创建View,到了这里View就创建成功了。
private View createViewByPrefix(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
Class<? extends View> clazz = Class.forName(
prefix != null ? (prefix + name) : name,
false,
context.getClassLoader()).asSubclass(View.class);
constructor = clazz.getConstructor(sConstructorSignature);
//为了增加速度,这里也尝试了缓存的方式。
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
在onCreateView中设置Factory2,通过回调函数我们能拿到View的名称(注意,我们连系统的根布局的名称也是可以拿到的),同事也可以拿到其对应的属性值,这样我们就可以根据这些值来创建我们的View,在这里也可以动态的设置我们想要的皮肤。
@Override
protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
getLayoutInflater().setFactory2(new LayoutInflater.Factory2() {//这里主要负责View的创建。
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
Log.e(TAG, "name: " + name);
int attributeCount = attrs.getAttributeCount();
for (int i = 0; i < attributeCount; i++) {
String attributeName = attrs.getAttributeName(i);
Log.e(TAG, "attributeName: " + attributeName);
}
Log.e(TAG, "------------------------------------divide line------------------- ");
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_happy);
}
日志打印:
name: LinearLayout
attributeName: orientation
attributeName: fitsSystemWindows
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
name: ViewStub
attributeName: theme
attributeName: id
attributeName: layout
attributeName: inflatedId
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
name: FrameLayout
attributeName: id
attributeName: layout_width
attributeName: layout_height
attributeName: foreground
attributeName: foregroundGravity
attributeName: foregroundInsidePadding
------------------------------------divide line-------------------
name: androidx.appcompat.widget.FitWindowsLinearLayout
attributeName: orientation
attributeName: id
attributeName: fitsSystemWindows
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
name: androidx.appcompat.widget.ViewStubCompat
attributeName: id
attributeName: layout
attributeName: inflatedId
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
name: androidx.appcompat.widget.ContentFrameLayout
attributeName: id
attributeName: layout_width
attributeName: layout_height
attributeName: foreground
attributeName: foregroundGravity
------------------------------------divide line-------------------
name: LinearLayout
attributeName: orientation
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
我们可以以apk的方式来打包资源,这个apk资源包中只存在资源文件,然后我们通过动态加载技术来加载apk资源包,实现动态换肤的功能。
public class SkinResourceManager {
private static SkinResourceManager skinManager = new SkinResourceManager();
private Context context;
private Resources resources;
//获取到资源包中的包名
private String skinPackageName;
private SkinResourceManager(){}
public static SkinManager getInstance(){
return skinManager;
}
public void init(Context context){
this.context = context;
}
/**
* 根据资源包的 存储路劲去加载资源
* @param path
*/
public void loadSkinApk(String path){
//真正的目的是得到资源包的资源对象
try {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path,
PackageManager.GET_ACTIVITIES);
skinPackageName = packageArchiveInfo.packageName;
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
//在assetManager中执行addAssetPath方法
addAssetPath.invoke(assetManager,path);
//创建一个资源对象 管理资源包里面的资源
resources = new Resources(assetManager,context.getResources().getDisplayMetrics(),
context.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
//写匹配各种资源的方法
/**
* 去匹配颜色
* @param resId 需要去匹配ID
* @return 匹配到的资源ID(在资源包中的资源ID)
*/
public int getColor(int resId){
if(resourceIsNull()){
return resId;
}
//获取到资源名字和资源类型
String resourceTypeName = context.getResources().getResourceTypeName(resId);
String resourceEntryName = context.getResources().getResourceEntryName(resId);
//去资源包的资源对象中去匹配
int identifier = resources.getIdentifier(resourceEntryName,
resourceTypeName, skinPackageName);
if(identifier == 0){
return resId;
}
return resources.getColor(identifier);
}
/**
* 从资源包中拿到drawable的资源id
*/
public Drawable getDrawable(int id){
if(resourceIsNull()){
//获取到这个id在当前应用中的Drawable对象
return ContextCompat.getDrawable(context,id);
}
//获取到资源id的类型
String resourceTypeName = context.getResources().getResourceTypeName(id);
//获取到的就是资源id的名字
String resourceEntryName = context.getResources().getResourceEntryName(id);
//就是colorAccent这个资源在外置APK中的id
int identifier = resources.getIdentifier(resourceEntryName, resourceTypeName, skinPackageName);
if(identifier == 0){
return ContextCompat.getDrawable(context,id);
}
return resources.getDrawable(identifier);
}
/**
* 从资源包中拿到drawable的资源id
*/
public int getDrawableId(int id){
if(resourceIsNull()){
return id;
}
//获取到资源id的类型
String resourceTypeName = context.getResources().getResourceTypeName(id);
//获取到的就是资源id的名字
String resourceEntryName = context.getResources().getResourceEntryName(id);
//就是colorAccent这个资源在外置APK中的id
int identifier = resources.getIdentifier(resourceEntryName, resourceTypeName, skinPackageName);
if(identifier == 0){
return id;
}
return identifier;
}
public boolean resourceIsNull(){
if(resources == null){
return true;
}
return false;
}
public Resources getResources() {
return resources;
}
}
View的工厂方法,主要用来生成View。
public class SkinFactory implements LayoutInflater.Factory2 {
//当Activity继承自AppcompatActivity的时候 去通过这个对象来实例化控件
private AppCompatDelegate delegate;
private static final String[] prxfixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
//缓存构造方法的map
private HashMap<String, Constructor<? extends View>> sConstructorMap =
new HashMap<String, Constructor<? extends View>>();
private Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
List<SkinView> skinViews = new ArrayList<>();
public SkinFactory(AppCompatDelegate delegate){
this.delegate = delegate;
}
@Override
public View onCreateView(View view, String s, Context context, AttributeSet attributeSet) {
if(s.contains("LinearLayout")){
Log.e("MN-------->","1111111111");
}
//实例化每个控件
View crrentView = null;
if(delegate !=null){
crrentView = delegate.createView(view, s, context, attributeSet);
}
//兼顾了两种情况 第一种情况就是delegate为空 没有去实例化控件
// 第二种情况就是delegate不为空 但是它没有替我们去实例化控件
if(crrentView == null){
//如果是Activity的情况下 通过反射去讲控件实例化的
//1.带包名 2.不带包名
if(s.contains(".")){
crrentView = onCreateView(s,context,attributeSet);
}else{
for (String s1 : prxfixList) {
String className = s1+s;
crrentView = onCreateView(className,context,attributeSet);
if(crrentView!=null){
break;
}
}
}
}
//收集需要换肤的控件
if(crrentView!=null){
paserView(context,crrentView,attributeSet);
}
return crrentView;
}
public void apply(){
for (SkinView skinView : skinViews) {
skinView.apply();
}
}
/**
* 收集需要换肤的控件
* @param context
* @param view
* @param attributeSet
*/
private void paserView(Context context, View view, AttributeSet attributeSet) {
TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.Skinable);
boolean isKin = typedArray.getBoolean(R.styleable.Skinable_is_skin, false);
if(!isKin){
return;
}
//再去找到这个控件需要换肤的属性
List<SkinItem> skinItems = new ArrayList<>();
for(int x=0;x<attributeSet.getAttributeCount();x++){
//属性的名字 类似 textColor src
String attributeName = attributeSet.getAttributeName(x);
//如果说符合条件 句证明这条属性是需要换肤的
if(attributeName.contains("textColor") || attributeName.contains("src")
|| attributeName.contains("background")){
//属性的名字 ID 类型
String resIdStr = attributeSet.getAttributeValue(x);
int resId = Integer.parseInt(resIdStr.substring(1));
String resourceTypeName = context.getResources().getResourceTypeName(resId);
String resourceEntryName = context.getResources().getResourceEntryName(resId);
SkinItem skinItem = new SkinItem(attributeName,resourceTypeName,
resourceEntryName,resId);
skinItems.add(skinItem);
}
}
if(skinItems.size()>0){
SkinView skinView = new SkinView(skinItems,view);
skinViews.add(skinView);
}
}
@Override
public View onCreateView(String s, Context context, AttributeSet attributeSet) {
View view = null;
try {
//获取到控件的class对象
Class aClass = context.getClassLoader().loadClass(s);
Constructor<? extends View> constructor;
constructor = sConstructorMap.get(s);
if(constructor == null){
constructor = aClass.getConstructor(mConstructorSignature);
sConstructorMap.put(s,constructor);
}
view = constructor.newInstance(context,attributeSet);
} catch (Exception e) {
e.printStackTrace();
}
return view;
}
public List<SkinView> getSkinViews() {
return skinViews;
}
}
对应的实体类
/**
* 每条属性的封装对象
*/
public class SkinItem {
//属性的名字 textColor text background
String name;
//属性的值的类型 color mipmap
String typeName;
//属性的值的名字 colorPrimary
String entryName;
//属性的资源ID
int resId;
public SkinItem(String name, String typeName, String entryName, int resId) {
this.name = name;
this.typeName = typeName;
this.entryName = entryName;
this.resId = resId;
}
public String getName() {
return name;
}
public String getTypeName() {
return typeName;
}
public String getEntryName() {
return entryName;
}
public int getResId() {
return resId;
}
}
//----------------------------divide line-----------------
public class SkinView {
//这个控件需要换肤的属性对象的集合
List<SkinItem> skinItems;
View view;
public SkinView(List<SkinItem> skinItems, View view) {
this.skinItems = skinItems;
this.view = view;
}
public void apply(){
for (SkinItem skinItem : skinItems) {
//判断这条属性是background吗?
if(skinItem.getName().equals("background")){
//1. 设置的是color颜色 2.设置的是图片
if(skinItem.getTypeName().equals("color")){
//将资源ID 传给ResouceManager 去进行资源匹配 如果匹配到了 就直接设置给控件
// 如果没有匹配到 就把之前的资源ID 设置控件
view.setBackgroundColor(SkinResourceManager.getInstance().getColor(skinItem.getResId()));
}else if(skinItem.getTypeName().equals("drawable") || skinItem.getTypeName().equals("mipmap")){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.JELLY_BEAN){
view.setBackground(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));
}else{
view.setBackgroundDrawable(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));
}
}
}else if(skinItem.getName().equals("src")){
if(skinItem.getTypeName().equals("drawable") || skinItem.getTypeName().equals("mipmap")){
//将资源ID 传给ResouceManager 去进行资源匹配 如果匹配到了 就直接设置给控件
// 如果没有匹配到 就把之前的资源ID 设置控件
((ImageView)view).setImageDrawable(SkinResourceManager.getInstance().getDrawable(skinItem.getResId()));
}else if(skinItem.getTypeName().equals("color")){
// ((ImageView)view).setImageResource(SkinManager.getInstance().getColor(skinItem.getResId()));
}
}else if(skinItem.getName().equals("textColor")){
((TextView)view).setTextColor(SkinResourceManager.getInstance().getColor(skinItem.getResId()));
}
}
}
public View getView() {
return view;
}
public void setView(View view) {
this.view = view;
}
public List<SkinItem> getSkinItems() {
return skinItems;
}
public void setSkinItems(List<SkinItem> skinItems) {
this.skinItems = skinItems;
}
}
使用,
注意:
public class SkinActivity extends AppCompatActivity {
public SkinFactory skinFactory;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
skinFactory = new SkinFactory(getDelegate());
getLayoutInflater().setFactory2(skinFactory);
super.onCreate(savedInstanceState);
}
public void apply(){
skinFactory.apply();
}
@Override
protected void onResume() {
super.onResume();
skinFactory.apply();
}
}