美文网首页
TextView变身AppCompatTextView

TextView变身AppCompatTextView

作者: 昵称不唯一 | 来源:发表于2019-08-28 15:13 被阅读0次

今天在翻看bugly的时候,看到了一个奇怪的bug日志,如下:

java.lang.NullPointerException
Attempt to invoke virtual method 'android.text.TextPaint android.widget.TextView.getPaint()' on a null object reference
android.support.v7.widget.AppCompatTextViewAutoSizeHelper.setRawTextSize(AppCompatTextViewAutoSizeHelper.java:603)
android.support.v7.widget.AppCompatTextViewAutoSizeHelper.setTextSizeInternal(AppCompatTextViewAutoSizeHelper.java:599)
android.support.v7.widget.AppCompatTextHelper.setTextSizeInternal(AppCompatTextHelper.java:373)
android.support.v7.widget.AppCompatTextHelper.setTextSize(AppCompatTextHelper.java:355)
android.support.v7.widget.AppCompatTextView.setTextSize(AppCompatTextView.java:191)
android.widget.TextView.setTextSize(TextView.java:2914)
...

哪里奇怪呢?
第8行->第7行:TextView.setTextSize -> AppCompatTextView.setTextSize
刚看到这里的时候我就有点怀疑自己了,因为我根本没有用到AppCompatTextView,那这玩意儿时从哪里冒出来的呢?见鬼了。本着相信科学的心态,我打印了我的TextView对象,结果可想而之,真的神奇般的变成了AppCompatTextView,由于View都是由LayoutInflater加载的(setContentView -> LayoutInflater.inflater),所以我去查看了LayoutInflater源码:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            try {
                ...
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                        // 此处省略N多行代码
                    ......
                }
            } 
            ...
            return result;
        }
    }

下面我们来看createViewFromTag方法:

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ...
        try {
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

           ...
            return view;
        } 
        ...
    }

我们看到了mFactory2,这个对象是什么时候被初始化的呢?我们继续追踪溯源,来看看代码:

    // AppCompatActivity.java -> onCreate
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        if (delegate.applyDayNight() && mThemeId != 0) {
            // If DayNight has been applied, we need to re-apply the theme for
            // the changes to take effect. On API 23+, we should bypass
            // setTheme(), which will no-op if the theme ID is identical to the
            // current theme ID.
            if (Build.VERSION.SDK_INT >= 23) {
                onApplyThemeResource(getTheme(), mThemeId, false);
            } else {
                setTheme(mThemeId);
            }
        }
        super.onCreate(savedInstanceState);
    }
    
    // AppCompatDelegateImplV9.java -> installViewFactory
    @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }
    
    // LayoutInflater.java -> setFactory2
    public void setFactory2(Factory2 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;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }

好了现在已经看到mFactory2原来就是AppCompatDelegateImplV9,我们进入它的createView方法中:

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        // 此处省略N多行
        ...

        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.java的createView方法:

final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        // 此处省略N多行
        ...

        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;
            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);
        }
        // 此处省略N多行
        ...

        return view;
    }
    
    @NonNull
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }

代码看到这里,一切都变得清晰了,在使用AppCompatActivity的时候,api会自动将我们写的TextView替换为AppCompatTextView,不仅仅是TextView,通过上面代码,我们还可以看到像ImageView、Button等,其它一些控件都会被替换为对应的AppCompatView类。

那么Google大神为什么要做这样的操作呢?

相信接触过AppCompatView的你肯定会知道AppCompatXXXView继承XXXView,在此基础上,添加了XXXTint属性、文本自适应大小属性等许多新的特性,framework在创建View实例的时候,自动帮我们转换为新特性版本的View,这样新特性就能兼容老版本来使用了,是不是很方便呢

~~ 好了文章到这里就结束了,让我们在下篇文章中再相见 ~~

相关文章

网友评论

      本文标题:TextView变身AppCompatTextView

      本文链接:https://www.haomeiwen.com/subject/mftmectx.html