Android换肤框架Debug 7.1.1源码一步步写

大家好,我是徐爱卿。博客地址:flutterall.com

这个SkinAPPDemo是很早的时候就写好的,今天才来总结,实在惭愧。–

成果

其实,Android换肤这个功能呢从v7包中谷歌就跟我们做了一个很好的示范。同时呢,谷歌也给我们提供了一个针对View去做自定义操作的接口。说了这么多,不如来点实际的。

本篇博客的的demo中的build.gradle配置是:

compileSdkVersion 25
buildToolsVersion "25.0.0"

然后进行debug源码时,也是基于Android7.1.1的源码进行的。后面一大波debug来袭,请留神。

这篇文章我们会从最简单的XML文件开始聊起。个人觉得知识点有以下:

  • 不断的debug到Android源码的内部能够加深我们对知识的了解
  • 不断的debug,可以知道那些报错的error信息生成的原因,以便于我们更好的解决问题
  • 学习Google的编码方式
  • 加深对LayoutInflater的理解
  • 看源码实现换肤

思考什么是换肤?

说白了,就是改变控件的背景以及颜色或者其本身的颜色。比如:更换TextView的字体颜色、背景颜色等等;在比如:更换LinearLayout的的背景以及颜色,等等。这些都属于换肤的范围。
###思考如何换肤?
两个切入点:

  • 方法一:view初始完成后,通过findViewById等方法拿到当前的控件,然后根据主题样式,设置其颜色、背景等。
  • 方法二:在view的构建初期,直接更改其颜色、背景等。

孰好孰坏,不言而喻。我呢,就要使用方法二。这个方法听起来不错,如何实现呢?其实,Google已经告诉我们了。天下文章一大抄,看你会抄不会抄。我们直接分析Google的实现逻辑,然后再写我们需要的逻辑。

有人问了,Android源码在哪里实现了?哥们别急,开讲了。

#引入
我们写一个简单的页面,里面就一个TextView,如下:

创建一个TextView

然后我们打印java Log.d(TAG, "tv instanceof AppCompatTextView ? -> "+(tv instanceof AppCompatTextView) +"");这句话,如下:(注意红色框中的,就可以了)

tv instanceof AppCompatTextView

看到结果不知道大家有没有些许疑问?在Android 7.1.1上运行的TextView竟然是AppCompatTextView的实例。我明明在XML中写的是TextView,在这里怎么就是AppCompatTextView的实例了呢?很明显,是在解析XML之后构建View对象的初期,看到是TextView标签直接使用AppCompatTextView构建这个对象。轮廓流程如下:
xml转化成View

我们关键看最后一步,看下如何“创建AppCompatTextView”,当我们知道了如何偶从一个XML文件变身为一个View对象后,我们就可以比葫芦画瓢,创建我们的自己的属性的View了。

分析LayoutInflater

从这里开始,我们全部使用debug结果一部部分析并且来验证了,免得空口说大话了。

我这里,创建一个SelectThemeActivity,继承自AppCompatActivity 我们先从最简单的 setContentView(R.layout.activity_select_theme);开始。

起初,执行的是startActivity(new Intent(this, SelectThemeActivity.class));],然后这个东东再向后调用ActivityManagerProxy#startActivity, ActivityManagerProxy是ActivityManager的一个远程代理,不用管它。然后通过ActivityThread的内部Handler类执行performLaunchActivity,最后调用Instrumentation#callActivityOnCreate(Activity activity, Bundle icicle)

这一块的流程如下:
从startActivity到init Factory
一定要记得下面这张图,这是一个关键点。
系统的Factory
这里注意一点layoutInflater.getFactory(),返回的是LayoutInflater的一个内部接口Factory

layoutInflater.getFactory()高能注意

在这里默认没有代码干预的情况下,我们不设置Factory的情况下,layoutInflater.getFactory()等于null,系统会自己创建一个Factory去处理XML到View的转换。但是!!!他这个。反之,如果我们设置了自己的Factory,那么系统就会走我们Factory的onCreateView,他会返回一个我们定制化的View。下面详细讲解。

Factory定义如下:

public interface Factory {
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         * 
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         * 
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         * 
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

Factory

Factory是一个很强大的接口。当我们使用inflating一个XML布局时,可以使用这个类进行拦截解析到的XML中的标签属性-AttributeSet和上下文-Context,以及标签名称-name(例如:TextView)。然后我们根据这些属性可以创建对应的View,设置一些对应的属性。

比如:我读取到XML中的TextView标签,这时,我就创建一个AppCompatTextView对象,它的构造方法中就是我读取到的XML属性。然后,将构造好的View返回即可。

默认情况下,从context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)得到LayoutInflater,然后通过layoutInflater.getFactory()刚开始是null,然后执行LayoutInflaterCompat.setFactory(layoutInflater, this);方法。
看下这个方法。

  * Attach a custom Factory interface for creating views while using
     * this LayoutInflater. This must not be null, and can only be set once;
     * after setting, you can not change the factory.
     *
     * @see LayoutInflater#setFactory(android.view.LayoutInflater.Factory)
     */
    public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
   
   
        IMPL.setFactory(inflater, factory);
    }

大致意识是:将一个自定义的Factory接口绑定到创建View的LayoutInflatr。这个接口的实现不能为空,同时只能设置一次(在代码中会有mFactorySet的boolean值(默认是false)标记是否已经设置过,如果重复设置,会抛异常)

在这里我们关注传入的LayoutInflaterFactory的实例,最终这个设置的LayoutInflaterFactory传入到哪里了呢?,我们向下debug,进入LayoutInflater中的下面:
LayoutInflater#setFactory2
给mFactory = mFactory2 = factory执行了,进行mFactory和mFactory2的赋值。

到这里走的路程,初始化好了LayoutInflater和LayoutInflaterFactory。

这里,我们就走完了SelectThemeActivity#onCreate中的super.onCreate(savedInstanceState);下面开始走setContentView(R.layout.activity_select_theme);

setContentView(int resId)

setContentView会走到LayoutInflate的下面这里:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }
			//在这里将Resource得到layout的XmlResourceParser对象
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

再向下就到了LayoutInflate重点:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            .....
            //将上面给我的XmlPullParser转换为对应的View的属性AttributeSet供View的构造方法或其他方法使用
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            ....
          try {
          if{
                ....
             } else {
             //默认布局会走到这里,Temp是XML文件的根布局
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
						...

                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);
						....
						//添加解析到的根View
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
						....
					}

            } catch (XmlPullParserException e) {
               ....
            return result;
        }
    }

进入到createViewFromTag方法之中,会进入到LayoutInflate的View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)中。
到这里开始,我们开始学习源码中是如何使用Factory的。会走到下面这里:
LayoutInflate#createViewFromTag
这里的name传入的就是就是解析到的标签值LinearLayout。

@Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // First let the Activity's Factory try and inflate the view
        先试着进行解析布局
        final View view = callActivityOnCreateView(parent, name, context, attrs);
        if (view != null) {
            return view;
        }

        // If the Factory didn't handle it, let our createView() method try
        return createView(parent, name, context, attrs);
    }

很遗憾, callActivityOnCreateView返回的总是null:

@Override
    View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // On Honeycomb+, Activity's private inflater factory will handle calling its
        // onCreateView(...)
        return null;
    }

然后进入到下面的,createView(parent, name, context, attrs);中。高潮来了》》》》》,我期盼已久的看看Google源码是如何创建View的。

从XML到View的华丽转身

根据标签+属性创建对象

public 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 = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        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 it's android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

看到木有,它是拿标签名称进行switch的比较,是哪一个就进入到哪一个中进行创建View。
有人说啦,这里没有LinearLayout对应的switch啊。的确。最终返回null。

AppCompatViewInflater#createView并没有对布局进行创建对象

这里回到最初,由于Line769返回null,同时name值LinearLayout不包含".",进入到Line785onCreateView(parent, name, attrs)

二次解析布局标签

到这里,我们知道这个标签是LinearLayout了,那么开始创建这个对象了。问题来了,我们知道这个对象名称了,但是它属于哪个包名?如何创建呢?

根据标签名称创建对象

我们知道Android控件中的包名总共就那么几个:android.widget.]android.webkit.]android.app.],既然就这么几种,那么我干脆挨个用这些字符串进行如下拼接:
android.widget.LinearLayout]android.webkit.LinearLayout]android.app.LinearLayout],然后挨个创建对象,一旦创建成功即说明这个标签所在的包名是对的,返回这个对象即可。那么,从上面debug会进入到如下源码:
进入二次解析布局标签

sClassPrefixList的定义如下:

private static final String[] sClassPrefixList = {
   
   
        "android.widget.",
        "android.webkit.",
        "android.app."
    };

注意:是final的

创建Android布局标签对象

继续向下debug,进入到真正的创建Android布局标签对象的实现。在这个方法中,才是“android.widget.”包下的,LinearLayout、RelativeLayout等等的具体实现

创建LinearLayout

name=“LinearLayout”
prefix=“android.widget.”

下面分析下这段代码(下面的方法中去掉了一些无用代码):

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
   
   
//step1 :sConstructorMap是<标签名称:标签对象>的map,用来缓存对象的。第一次进入时,这个map中是空的。
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
   
   
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
   
   
//step2:在map缓存中没有找到对应的LinearLayout为key的对象,则创建。
            if (constructor == null) {
   
   
                // Class not found in the cache, see if it's real, and try to add it
  
//step3:【关键点,反射创建LinearLayout对象】,根据"prefix + name"值是"android.widget.LinearLayout"加载对应的字节码文件对象。
              clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                if (mFilter !=
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值