学习是一个过程。
文章目录
Activity中LayoutInflater加载布局总体时序图
LayoutInflater源码讲解(api28)
-
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("<merge /> 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 ? "<unknown>" : 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; } }
LayoutInflater设置Factory2
-
在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资源包,实现动态换肤的功能。
- 加载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; } }
-
使用,
注意:
- 需要在Application初始化SkinResourceManager以及加载资源包
- 需要在需要换肤的属性上面注册is_skin="true"属性,这个属于自定义属性,很简单就不在这赘述了。
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(); } }
LayoutInflater源码总结
- Activity与AppCompatActivity都是调用LayoutInflater进行View的创建,但是AppCompatActivity的View是用Factory2进行创建的,我们可以用这种机制来实现替换动态换肤的实现。
- LayoutInflater的Factory2是带有重设校验的,它是不支持重复设置参数的,我们可以有两种方式来设置我们的Factory2。
- 在Activity的的surper.onCrate()方法调用setFactory2的方法设置Factory2.
- 通过反射设置Factory2 的值。
- 在LayoutInflater里面有个1995闪烁layout,我们可以通过在标签中使用blink标签,来达到闪烁布局的效果。
- 创建View是通过反射进行创建的,为了加快构造方法的创建速度,将之前生成的构造方法进行缓存。