Android从源码角度分析换肤功能实现

内置换肤核心思路

换肤功能说得直白点就是改变View的样式,例如textColor,backgroundColor等。我们知道View的加载都是通过LayoutInflater类来实现的,如果我们能在View被创建前拿到View的相关信息(名称,布局属性等),那么我们就能自己去改变View被创建的逻辑,更改View相关的属性值来实现换肤功能。

Activity的onCreate方法有两个一定会用到的方法:

  • super.onCreate(savedInstanceState);
  • setContentView(getContentView());

View的创建也一定会在这两个方法中完成,所以这是一个很明显的切入点,下面我们来着重分析这两个方法

注:源码分析基于android 9.0,其它版本大同小异

1. super.onCreate(savedInstanceState);

在Activity的onCreate方法中我们可以看到调用了deletgate.installViewFactory();

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }

其中installViewFactory()是一个很重要的方法,该方法存在于AppCompatDelegateImpl类中,系统通过调用LayoutInflaterCompat.setFactory2()为LayoutInflater中的mFactory2属性赋值,下面我们来看一下具体实现。

@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            //这里的第二个参数就是Factory2,之所以传递参数为this是因为AppCompatDelegateImpl类本身实现了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");
            }
        }
    }

那么Factory2又是什么呢?

Factory2是LayoutInflater中的一个内部接口,用于创建view

public interface Factory2 extends Factory {
        /**
         * Version of {@link #onCreateView(String, Context, AttributeSet)}
         * that also supplies the parent that the view created view will be
         * placed in.
         *
         * @param parent 被创建View的父布局
         * @param View的名称,例如TextView,Buttong等
         * @param context 
         * @param View的布局属性
         *
         * @return 返回被创建的view,如果返回值为null则走其它创建View的逻辑
         */
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }

这里有一点很重要,那就是通过Factory2创建的View有可能为null,这时候回去走其它创建View的逻辑,因为该接口是public修饰的,开发人员可以进行自由实现,返回值不可控。

2. setContentView(int layoutResId)

setContentView的具体实现依然在AppCompatDelegateImpl类,来看一下具体的代码:

@Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }

我们需要关注的就是inflate方法,这一看就是加载布局的关键实现,点开inflate方法并一步一步跟踪,具体流程如下

  1. inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
  2. inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
  3. createViewFromTag(View parent, String name, Context context, AttributeSet attrs)
  4. createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)

重点关注第4个方法,View的创建逻辑也在这里得到了实现。(以上4个方法全部在LayoutInflater中)

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
    
        ........................ 省略部分代码

        try {
            View view;
            if (mFactory2 != null) {
                //由于Activity在调用onCreate的时候就为mFactory2赋值了,所以肯定会走到这里
                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);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        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;
        }
    }

由于系统在onCreate之前就为mFactory赋值了,所以一定会调用mFactory2.onCreateView(parent, name, context, attrs);

那么mFactory2.onCreateView的具体实现在哪里呢?其实上面的代码已经给出了答案:

在AppCompatDelegateImpl -> installViewFactory()方法中有这样一句代码

LayoutInflaterCompat.setFactory2(layoutInflater, this);

第二个参数类型是Factory2,而this指代了AppCompatDelegateImpl,所以mFactory2.onCreateView的具体实现一定在AppCompatDelegateImpl。下面来看一下具体的代码:

/**
     * From {@link LayoutInflater.Factory2}.
     */
    @Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        return createView(parent, name, context, attrs);
    }
public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        //创建一个AppCompatViewInflater对象    
        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)) {
                mAppCompatViewInflater = new AppCompatViewInflater();
            } else {
                Class viewInflaterClass = Class.forName(viewInflaterClassName);
                mAppCompatViewInflater =
                    (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                    .newInstance();
            }
        }

        ............... 省略部分代码

		//通过调用AppCompatViewInflater的createView来创建View
        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 */
        );
    }

好家伙,追踪了半天代码,这createView的重担由交给了AppCompatViewInflater,那么再继续追下去吧:

AppCompatViewInflater -> createView()

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
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Spinner":
                view = createSpinner(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageButton":
                view = createImageButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckBox":
                view = createCheckBox(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RadioButton":
                view = createRadioButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "CheckedTextView":
                view = createCheckedTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "AutoCompleteTextView":
                view = createAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "MultiAutoCompleteTextView":
                view = createMultiAutoCompleteTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "RatingBar":
                view = createRatingBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "SeekBar":
                view = createSeekBar(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ToggleButton":
                view = createToggleButton(context, attrs);
                verifyNotNull(view, name);
                break;
            default:
                // The fallback that allows extending class to take over view inflation
                // for other tags. Note that we don't check that the result is not-null.
                // That allows the custom inflater path to fall back on the default one
                // later in this method.
                view = createView(context, name, attrs);
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check its android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

这次真的没跑了,View的创建最具体的实现就是在这个方法,我们也不需要再追下去了。

 

3. 思考如何换肤

通过前面两步我们知道了系统如何通过Factory2来实现View的创建,那么Factory2如何才能为我所用,让创建View的逻辑都由我接管,达到View创建由我不由系统的目的?

3.1 因为Factory2的赋值操作是由系统完成的,并最终由AppCompatDelegateImpl来完成Factory2的具体实现,具体的代码再贴一次

@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            //这里的第二个参数就是Factory2,之所以传递参数为this是因为AppCompatDelegateImpl类本身实现了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");
            }
        }
    }

给mFactory2赋值的时候有一个很重要的先决条件,那就是if (layoutInflater.getFactory() == null),如果我们提前给mFactory2赋值,并且在Activity中实现Factory2接口,那么创建View的逻辑自然而然地被我们接管了

仿佛android系统知道我们要这么干,所以一早就为我们实现了Factory2接口

Android从源码角度分析换肤功能实现_第1张图片

3.2 在onCreate(@Nullable Bundle savedInstanceState)方法调用super.onCreate(savedInstanceState);之前我们先给mFactory2赋值

@Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflater mInflater = LayoutInflater.from(this);
        // Activity也实现了LayoutInflater.Factory2接口
        LayoutInflaterCompat.setFactory2(mInflater, this);
        super.onCreate(savedInstanceState);
        setContentView(getContentView());
    }

3.3 在Activity里面重写Factory2的onCreateView(View parent, String name, Context context, AttributeSet attrs)方法,实现自己创建View的逻辑,下面给出伪代码

@Nullable
    @Override
    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        if (需要接管View的创建过程) {
            //TODO 实现自己的创建逻辑,返回值可以为null,这样就会走非Factory2的View创建逻辑
            View view = null;
            switch (name) {
                case "TextView":
                    view = 自定义TextView;
                    break;
                case "Button":
                    view = 自定义Button;
                    break;
                ....
                    
            }

            return view;
        }
        return super.onCreateView(parent, name, context, attrs);
    }

由于具体的实现逻辑比较简单,这里就不展示代码了。大家可以根据自己项目的需求进行扩展!

至此内置换肤的逻辑就分析完成了,欢迎大家指出不足!

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Android从源码角度分析换肤功能实现)