• Android实现动态换肤-原理篇


    学习是一个过程。

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

    在这里插入图片描述

    LayoutInflater源码讲解(api28)

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

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

          @Override
          protected void onCreate(@Nullable Bundle savedInstanceState) {
              final AppCompatDelegate delegate = getDelegate();
              //注意此方法是做换肤的关键。
              delegate.installViewFactory();
              delegate.onCreate(savedInstanceState);
              super.onCreate(savedInstanceState);
          }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • 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");
                  }
              }
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 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);
              }
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
    • 继续分析setContentView()方法,AppCompatActivity中的setContentView调用的是AppCompatDelegatelmpl的方法。

        @Override
          public void setContentView(@LayoutRes int layoutResID) {
              getDelegate().setContentView(layoutResID);
          }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 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();
          }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    • 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();
              }
          }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
    • 继续分析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;
              }
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
    • 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;
              }
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
    • 我们跟踪一下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);
              }
          }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
    • 继续分析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 */
              );
          }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
    • 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;
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
    • 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;
              }
          }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
    • 尝试用不同的前缀反射创建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;
              }
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25

    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);
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26

      日志打印:

      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------------------- 
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48

    实现方式

    我们可以以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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 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;
          }
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
    • 对应的实体类

      
      
      /**
       * 每条属性的封装对象
       */
      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;
          }
      }
      
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
    • 使用,

      注意:

      • 需要在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();
          }
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

    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是通过反射进行创建的,为了加快构造方法的创建速度,将之前生成的构造方法进行缓存。
  • 相关阅读:
    前端实现大屏缩放自适应屏幕
    R语言处理数量很大(千万级及以上)的数据时的拆分策略-案例一
    Vue页面快速使用阿里巴巴矢量图标库
    Kubernetes API 基础
    Flutter:环境搭建、项目创建
    基于SSM的住院病人监测预警信息管理系统毕业设计源码021054
    ubunut 远程备份整个系统到其他磁盘中
    源码分析之上下文构建
    数百个下载能够传播 Rootkit 的恶意 NPM 软件包
    深度学习:张量 介绍
  • 原文地址:https://blog.csdn.net/chendeshan330/article/details/127878144