登录

去注册

登录

注册

去登录

注册

每日一问 AppCompatTextView 与 TextView 1/3

xiaoyang   2019-08-07   收藏

  1. compat库是如何将TextView替换为AppCompatTextVew的?
  2. 为什么要进行替换?
  3. 根据替换相关原理,我们可以做哪些事情?
7

刚准备发的时候看到 @18972789642 同学也已经说的很详细了,本来我想删掉我自己写的,后来想了想,既然都已经写了,就算了吧(^__^) 嘻嘻……

compat库是如何将TextView替换为AppCompatTextVew的?

TextView在运行时被替换成AppCompatTextVew的前提是:该Activity必须继承自AppCompatActivity。
它是怎么替换的呢?
我们给Activity设置布局一般会使用setContentView方法,打开AppCompatActivity,可以看到它已经重写了这个方法:

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

调用的是getDelegate方法返回的对象的setContentView方法。
getDelegate方法返回的是一个AppCompatDelegate对象,这个AppCompatDelegate是一个抽象类,由AppCompatDelegateImpl去实现,也就是说:getDelegate方法最终返回的是AppCompatDelegateImpl的实例。

那现在来看看它的setContentView方法是怎么实现的:

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

乍一看好像没什么特别的,就是把我们传进去的布局ID,给inflate出来并且把它添加到contentParent上而已。

回到AppCompatActivity那边,看看它的onCreate方法:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ......
        getDelegate().installViewFactory();
        ......
    }

点进去:

    @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        }
        ......
    }

可以看到AppCompatDelegateImpl在这个方法中会给LayoutInflater设置一个Factory2,并且传的是this,说明它是实现了Factory2的。
我们知道,当LayoutInflater在inflate布局的时候,会优先调用Factory2的onCreateView方法
那现在来看看AppCompatDelegate的onCreateView方法:

    @Override
    public View createView(View parent, String name, Context context, AttributeSet attrs) {
        ......
        ......
        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP,  true,  VectorEnabledTintResources.shouldBeUsed());
    }

可以看到它把createView的工作交给了AppCompatViewInflater,来看看它是怎么实现的:

    final View createView(View parent, final String name, Context context, AttributeSet attrs, boolean inheritContext,
                                        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            ......
            ......
        }
        return view;
    }

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

emmm,在这个方法中,会把常用的View(ViewGroup除外)替换成对应的AppCompat开头的View,除了TextView,还有ImageView、EditText、SeekBar等等。

为什么要替换成AppCompat开头的一系列View呢?

我们来观察一下各个AppCompat开头的组件,可以发现他们的共同点:都实现了一个叫TintableBackgroundView的接口。
看看它里面有哪些方法:

    void setSupportBackgroundTintList(ColorStateList tint);
    ColorStateList getSupportBackgroundTintList();

    void setSupportBackgroundTintMode(PorterDuff.Mode tintMode);
    PorterDuff.Mode getSupportBackgroundTintMode();

那现在可以大概猜到,替换成AppCompat系列的,就是为了能够让旧版本(5.0以下)能够兼容一个叫BackgroundTint的东西,中文翻译为 背景着色。
这个东西有什么作用呢?
看看这张图就懂了:
preview
哈哈,item被选中后的变色效果,就是用BackgroundTint来做的!

根据替换相关原理,还可以做哪些事情?

最最广为人知的,就是主题替换了。
还可以通过 “替换”,来做出动态控制View属性的效果,这也跟主题替换差不多,但相比于一般的图片,颜色,背景替换,我们还可以做出更加惊喜的效果。

回复
3
  1. 首先 在 AppCompatActivity 的onCreate里
    AppCompatDelegate delegate = this.getDelegate();
    
    获得到AppCompatDelegateImpl 的实例
    然后调用 AppCompatDelegateImpl 的 installViewFactory函数
    delegate.installViewFactory();
    
    这个函数做了一件事
    LayoutInflaterCompat.setFactory2(layoutInflater, this);
    
    这个AppCompatDelegateImpl 实现了 Factory2 的接口 ,也就实现了 onCreateView函数,这个this 也就是把AppCompatDelegateImpl实例传进去了 (伏笔)

镜头回到 AppCompatActivity的setContentView,这个函数其实调用的是 AppCompatDelegateImpl的 setContentView,然后这个函数有这么一行 常见的代码

LayoutInflater.from(this.mContext).inflate(resId, contentParent);

但是我们不能放过他,继续深入,看到一行代码,注释说 界面时从 xml来的,有点可疑

                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

继续看createViewFromTag,之前 AppCompatDelegateImpl 又把自己 赋值给mFactory2,以下代码会执行他的onCreateView,

            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            }

镜头再给到 AppCompatDelegateImpl的createView ,最后这个函数执行了 AppCompatViewInflater 的 createView函数,有这么一段代码

        switch(name.hashCode()) {

        case -938935918:
            if (name.equals("TextView")) {
                var12 = 0;
            }
            break;

    switch(var12) {
        case 0:
            view = this.createTextView(context, attrs);
            this.verifyNotNull((View)view, name);
            break;


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

大家差不多,看出来,这个些代码把 xml解析出的View,如果符合条件,替换相应的 AppCompat开头的视图

2。我觉得是谷歌开发者不想让 旧项目 把 控件一个个 加个AppCompat开头 ,为了省功夫。

3.我们参考这样的机制可以 替换我们一些自己写的 旧控件,比如 一个远程依赖升级了,但是包名变了,我们使用了这个旧依赖的视图,我们就是全局替换,省功夫

回复
https://jianpanwuzhe.blog.csdn.net/ : @https://jianpanwuzhe.blog.csdn.net/ 

我们要自定义替换 要复写 delegate.createView() 即可

2019-08-05 回复
1

第二问:

先从第二问开始吧,AppCompatTextView继承自TextView,是对TextView的一种扩展,因为在5.0中首次推出了MaterialDesign这种设计风格,但是众所周知的,5.0推出不可能所有的设备全都一下子更新到最新版本,为了在早期版本上实现新的功能(这些新功能比如从源码注释中解读到比如backgroundTint属性,根据文本内容自适应大小等),即为了新特性同样可以兼容老版本,framework在创建TextView实例的时候,自动帮我们进行了替换。其它的AppCompatXXX与XXX的关系也是如此。

第一问:

然后第一问,如何完成替换的,我们这里只拿最直观的流程举例,且尽可能的简化源码过程,在讨论这个问题之前,先了解几个预备知识:

View是怎么被解析创建出来的:

1.LayoutInflater:将布局XML文件实例化为其对应的View对象,我们在Activity中通过setContentView传入一个Layout的资源文件id,最终该方法最终会调用到PhoneWindow的setContentView方法,这个方法里面有调用到

mLayoutInflater.inflate(layoutResID, mContentParent);

2.inflate方法,该方法的作用是将指定的XML文件填充到View的层次结构中去,最终无论通过什么途径调用到inflate方法,都会走到三个参数的重载方法这里:

return inflate(parser, root, attachToRoot);

parser你可以认为持有将Layout.XML解析后的数据。后两个参数的意义如下:

  • root为null,attchToRoot无意义,inflate返回的是当前XML对应的根布局。
  • root不为null且attachToRoot为true,则整个XML对应的布局就设置了根布局是root。
  • root不为null且attachToRoot为false,则会将root的layoutParames设置给当前XML的布局。

知道了LayoutInflate.inflate做了什么,再往下,inflate中会调用到createViewFromTag,从方法名就能知道,继续往下走,我们离答案越来越近了。

createViewFromTag做的事情非常有意思:

先看到787行这个if-else,条件是name中有没有"."字符,如果有我们会执行onCreateView,如果没有会执行createView。name啥时候有点?自定义控件的时候。当是系统控件的时候,createView会有一个填充了第二个参数的调用:createView(name, "android.view.", attrs);补上了View控件的全路径名,而自定义控件则不需要,因为传入的name就是一个全路径名。

为什么要全路径名?因为View控件对象的创建是通过反射来实现的:

clazz = mContext.getClassLoader().loadClass(
    prefix != null ? (prefix + name) : name).asSubclass(View.class);
...
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
// ...
args[1] = attrs;
final View view = constructor.newInstance(args);

下面对这几步做一个总结:

XML中保存了ViewTree的结构和View的相关标签信息(包括View的类型和一些属性值),然后这些信息会在后面通过反射的方式(如果没有Factory2和Factory的话)创建实例对象,如果创建的是ViewGroup,则会对它的子View遍历重复创建步骤,创建完View对象后,会add到对应的ViewGroup中。其中相关方法的调用流程是:inflate->rInflate->createViewFromTag->createView。

好像还是没有看到替换?

还是上一张图,我们只解释了后半部分,没有解释前半部分,那么什么是Factory?

继续往下看:

createViewFromTag中会先判断有没有Factory或者Factory2的对象,如果有,则调用Factory的onCreateView方法。这两个类都是接口,其中Factory2是Factory的子接口,都只有唯一一个onCreateView方法。不同之处在于Factory2的onCreateView方法传入了parentView。
该方法的作用就是你可以借助它来改造XML中已经存在了的Tag的值。所以Factory2可以达到改造parentView的目的。

但是我们在日常中根本就没有任何地方接触到了Factory(2)呀,那么它是不是就直接是null呢?到这里又是一番源码调来调去,为了便于理解,只需要知道,这个东西(Factory2),在最开始AppCompatActivity(为了兼容低版本,我们现在Activity默认都是继承自它)中的onCreate方法中就已经通过层层调用被设置好了。

既然现在Factory2不为空,那么就应该去走它的onCreateView方法了,这里又是层层调用,最终来到了AppCompatViewInflater 的 createView 方法:

答案就在这里:

如果创建的是非兼容控件(系统控件那么多,实现兼容的只是常用的一些控件),那么就会是143行,在146中通过反射创建View对象。

啰里啰唆扯了一大堆,还是没回答第一个问题:compat库是如何将TextView替换为AppCompatTextVew的?

个人对这个的理解:在将XML文件解析成包含ViewTree信息之后,开始利用这些信息去创建每一个View节点,在创建View对象的时候,如果发现这个节点是属于支持兼容的控件比如TextView,那么就会去调用到new AppCompatTextView()来创建一个兼容的View对象,也就是在创建的时候,及已经实现了替换。

第三问:

根据替换相关原理,我们可以做哪些事情?

整个替换从图一所示的源码中可以看到,能够被替换的关键是Factory(2)存在,那么我觉得,其实问题问的是Factory(2)可以用来做什么吧?

那么这个时候,就适合去问站长大人了:https://blog.csdn.net/lmj623565791/article/details/51503977

回复
0

嘿嘿,之前不懂,看源码查找资料又 get 一波。

  1. 首先必须继承 AppCompatActivity,在 AppCompatActivity onCreate 中使用了 AppCompatDelegate 对 Layoutinflater 设置了 Factory2 ,在 LayoutInflater 加载 View 时如果设置了 Factory2 就会调用 Factory2 的方法创建 View ,而 AppCompatDelegateImpl 就是 Factory2 的实现者,在这里调用了 LayoutInflaterCompat 中的 onCreateView 创建 View ,而在次方法中依据 View 的 Name 分别对应的返回 AppCompatXXXView。
  2. 为了进行向下兼容,是的高版本的特性也能在低版本上使用。
  3. 同意换字体,换肤,全局替换 View。
回复

删除留言

确认删除留言,会导致相关评论丢失?

取消 确定