Android实现动态换肤-原理篇
创始人
2024-01-22 05:11:17
0

学习是一个过程。

文章目录

      • Activity中LayoutInflater加载布局总体时序图
      • LayoutInflater源码讲解(api28)
      • LayoutInflater设置Factory2
    • 实现方式
      • LayoutInflater源码总结

Activity中LayoutInflater加载布局总体时序图

在这里插入图片描述

LayoutInflater源码讲解(api28)

  • onCreate加载布局,是不是都很熟悉。

       @Overrideprotected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_happy);}
    
  • AppCompatActivity的onCreate()方法,注意这不是Activity的onCreate方法。

        @Overrideprotected 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的方法。

      @Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}
  • AppCompatDelegatelmpl的setContentView方法,此方法主要是加载我们自定义的布局,将布局添加到容器中。

      public void setContentView(int resId) {//主要是初始化根布局,用来存放我们自定义的布局。ensureSubDecor();//存放我们自定义布局的View,此View的类型是FrameLayoutViewGroup 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 suppliedparams = 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 constructor = sConstructorMap.get(name);//鉴别构造方法是否失效,主要为了鉴别类加载器。if (constructor != null && !verifyClassLoader(constructor)) {constructor = null;sConstructorMap.remove(name);}Class 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 itclazz = 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 constructorif (mFilter != null) {// Have we seen this name before?Boolean allowedState = mFilterMap.get(name);if (allowedState == null) {// New class -- remember whether it is allowedclazz = 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 subclassfinal 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 contextif (inheritContext && parent != null) {context = parent.getContext();}if (readAndroidTheme || readAppTheme) {// We then apply the theme on the context, if specifiedcontext = 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:onClickcheckOnClickListener(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// tryreturn 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 constructor = sConstructorMap.get(name);try {if (constructor == null) {// Class not found in the cache, see if it's real, and try to add itClass 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// tryreturn null;}}
    

LayoutInflater设置Factory2

  • 在onCreateView中设置Factory2,通过回调函数我们能拿到View的名称(注意,我们连系统的根布局的名称也是可以拿到的),同事也可以拿到其对应的属性值,这样我们就可以根据这些值来创建我们的View,在这里也可以动态的设置我们想要的皮肤。

     @Overrideprotected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {getLayoutInflater().setFactory2(new LayoutInflater.Factory2() {//这里主要负责View的创建。@Overridepublic 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;}@Overridepublic 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资源包,实现动态换肤的功能。

LayoutInflater源码总结

  1. Activity与AppCompatActivity都是调用LayoutInflater进行View的创建,但是AppCompatActivity的View是用Factory2进行创建的,我们可以用这种机制来实现替换动态换肤的实现。
  2. LayoutInflater的Factory2是带有重设校验的,它是不支持重复设置参数的,我们可以有两种方式来设置我们的Factory2。
    • 在Activity的的surper.onCrate()方法调用setFactory2的方法设置Factory2.
    • 通过反射设置Factory2 的值。
  3. 在LayoutInflater里面有个1995闪烁layout,我们可以通过在标签中使用blink标签,来达到闪烁布局的效果。
  4. 创建View是通过反射进行创建的,为了加快构造方法的创建速度,将之前生成的构造方法进行缓存。

相关内容

热门资讯

常州法院2025年前三季度调解... 调解结案16474件、调解成功率24.08%——这是2025年前三季度常州法院交出的司法成绩单。通过...
安徽省政协研究室副主任陈鑫已任... 据铜陵市政府官网消息,11月20日上午,市委举行理论学习中心组学习会议,邀请省委社会工作部副部长高维...
原创 联... 据光明网报道,11月19日,在联合国大会的讨论中,日本企图争取成为安理会常任理事国的梦想再次破灭,令...
南部关于全县规范法律咨询服务机... 一、专项行动时间 自即日起至2025年12月。 二、举报受理范围 社会各界反映强烈的某些法律咨询服务...
“男子持刀入室盗窃”视频引发关... 近日,一段疑似“小偷”入室盗窃被业主家中监控拍下的视频在网上引发关注。11月21日晚,“翠屏公安”微...
绝不允许日本军国主义幽灵复活!... 2025年11月7日,日本首相高市早苗宣称,如果中国大陆对台湾出动军舰并使用武力,可能会构成“存亡危...
【解决】AI法律助手荣获202... 2025全球数字经济大会启幕,搭建国际数字合作高端平台 经国务院批准,由北京市人民政府、国家互联网信...
嘉兴男子与妻争吵,突然将行李箱... 近日,浙江嘉兴一对夫妻因琐事发生争吵,丈夫突然将装满衣物的行李箱从6楼扔到楼下,引发关注。11月22...
三地107家律所齐聚丰台,京津... 11月22日,京津冀律师驿站举办“党建业务深度融合 促进行业规范发展”主题活动,发布“百千万行动计划...