前言
Factory2
是直接继承于Factory
,继续跟踪下Factory
的源码,比Factory
的功能更加强大。 当我们新建 Activity 的时候,大部分情况是继承 AppCompatActivity
。提供了向后兼容性。
本文将深入探索 AppCompatActivity
的 视图加载,探索将 xml 布局文件中的 TextView
替换成 AppCompatTextView
的全过程,并由浅入深介绍了Factory2 的一些奇技淫巧,帮助各位Android 开发者简化开发,提高效率。
一、Factory2
在 Android 中,我们经常在 xml 文件中书写布局。这些文件被打包进 app(因为性能原因由 aapt/2 转换为二进制 xml),并且在运行时由 LayoutInflater
加载。
在 LayoutInflater
中有两个方法 setFactory
和 setFactory2
,文档中是这样描述的:
当使用 LayoutInflater 创建 View 的时候,绑定一个自定义的 factory 实例。不能为 null,并且只能设置一次,设置之后无法修改,当 xml 中每一个元素名字被解析的时候调用。若 factory 返回一个 View,将被添加到视图层级中;若返回 null,factory 的下一个默认方法
onCreateView(View, String, AttributeSet)
将被调用。
注意,Factory2 implements Factory
,所以对于 Api 11+ 的应用来说,应该使用 setFactory2
。这就相当于给了我们介入 xml 中每一个 View 元素的创建过程的机会。让我们看一个实际使用:
上面的代码中,我们仅仅为当前 Context
的 LayoutInflater
设置了一个 Factory2
。这样只要发现了 TextView
,都会被替换为我们自己的实现类 RedTextView
。
RedTextView
是 TextView
的子类,提供了 setBackgroundColor
方法,将背景置为红色:
布局文件 factory.xml
是这样的:
运行应用并使用 Layout Inspector,我们发现所有的 TextView
都变成了 RedTextView
。棒极了!
二、Appcompat Activity 和 Factory2
如果把上面的 FactoryActivity
修改为继承 AppCompatActivity
,我们会看到 TextView
确实变成了 RedTextView
。但是我们添加的 Button
仍然是 Button,并没有变成 AppCompatButton
,这是为什么?
AppCompatActivity
的 onCreate
方法的前两行是:
getDelegate()
根据 api 版本的不同返回对应的代理类(AppCompatDelegateImplV14
, AppCompatDelegateImplV23
, AppCompatDelegateImplN
等等)。
下一行代码 delegate.installViewFactory()
,当 layoutInflater.getFactory()
为空的时候,会调用 setFactory2
。如果不为空,什么都不会做。
所以 Button
没有发生变化的原因是,已经设置过了 Factory,导致 AppcompatActivity
自己的 factory 没有被 install。
注意,FactoryActivity
的 setFactory2()
方法是在 super.onCreate
之前调用的。如果不是的话,当父类是 AppcompatActivity
,setFactory2
会抛出异常。因为 AppCompatActivity
设置了自己的 Factory 。文档中是这样描述的:它不能为空,且只能被设置一次;在设置之后,你不能对 Factory 进行改变 。
三、如何兼容 AppCompatActivity 的 Factory2
如何既能使用自己的 Factory2,又能让 AppCompatActivity
保留自己的 Facotory 呢?下面给出几种解决方法。
(一)代理给 AppCompatDelegate
在 AppCompatDelegate
内部有一个 createView
方法,不要和 Factory
、Factory2
的 onCreateView
混淆。
我们仅仅只需要修改 setFactory2
,将不需要处理的情况代理给 AppCompatDelegate
:
运行一下,TextView
变成了 RedTextView
,Button
变成了 AppCompatButton
,成功!
(二)重写 viewInflaterClass
我们看一下 AppCompatDelegate
的 createView
方法,当 AppCompatViewInflater
没有初始化时,会通过反射创建。要初始化的类由 R.styleable.AppCompatTheme_viewInflaterClass
指定,默认就是 AppCompatViewInflater
。
对 FactoryActivity
的 theme 进行如下修改:
就可以让 AppCompatDelegate
使用我们自定义的 AppCompatViewInflater
的子类 CustomViewInflater
:
Google 的 Material Design Components 实际上就是使用这种方法来将 Button
修改为对应的 MaterialButton
,在 这里 可以看到 。
这个方法很强大,它可以让你的 App 使用 Material Design Components 这样的类库,却仅仅只需要设置合适的主题。
注意 AppCompatViewInflater
还提供了一个可以被重写的 createView()
方法,用来处理默认情况下没有被处理的新的组件。当 AppCompatViewInflater
没有处理特定的组件类型,就可以使用这个方法。
(三)自定义 LayoutInflater
第三种方法是重写 Activity
的 attachBaseContext
,改写 ContextThemeWrapper
的 getSystemService
方法,返回自定义的 LayoutInflater
。自定义的 LayoutInflater
可以重写 setFactory2
方法,加入自己的处理逻辑。这个方法是我从 ViewPump 学到的。
四、一些小细节
下面介绍了 AppCompatDelegate 在进行视图加载过程中的几个小细节。
(一)onCreateView
我们希望 Factory2
的 onCreateView
方法直接调用 createView
(代理给 AppCompatDelegate
那一小节中提到过) 。事实上,的确也是这么做的。但是代码中还多了一点东西 - 调用了 callActivityOnCreateView
。在 AppCompatDelegateImplV14
中是这样的:
看一下 LayoutInflater
的 源码 , createViewFromTag
尝试通过 factory 获取 view 。如果没有获取到,会使用 mPrivateFactory
。如果依旧没有获取到,会通过视图标签去创建 view 。mPrivateFactory
是在 Activity 中设置的。
有意思的是, mPrivateFactory
的作用是解析 fragment
标签。
在 API 14 之前,LayoutInflater
并没有提供 mPrivateFactory
让 Activity 可以有个兜底方案来创建 View 。因此,callActivityOnCreateView
在低版本中提供了这一功能。但这现在都没有关系了,反正 AppCompat 目前只兼容 Api 14+ 。
另一个有意思的知识点是 Window.Callback 。Window.Callback
是一个回调,让调用者可以拦截 key 的分发,面板,菜单等等。它让 AppCompatActivity 可以处理一些特定时间,例如菜单键,返回键等。
(二)createView
总的来说,AppCompatDelegateImplV9
做了两件事。首先,创建了 AppCompatViewInflater
或者在 theme 中指定的其他子类。第二,通过 inflater 创建 View 。
AppCompatViewInflater
的 createView
使用了正确的 Context
(考虑到支持 app:theme
和 android:theme
,需要对 Context 进行包装),根据组件名称创建对应的 AppCompat 组件(例如,如果是 TextView
,就调用 createTextView
方法返回 AppCompatTextView
)。
(三)支持 app:theme
从 Android 5.0 开始,可以给 View 设置 app:theme
以覆盖特定 View 及其子类的属性。AppCompat 通过继承父 View 的 context 在 Android 5.0 之前复制这一行为。
在 AppCompat 加载 View 之前,它先拿到父 View 的 Context,然后尝试创建一个 ContextThemeWrapper(android:theme
或者 app:theme
),保证使用正确的 context 来加载组件。
另外,如果开发者明确声明需要在资源中使用矢量图,AppCompat 在 Android 5.0 之前还提供了 TintContextWrapper
来包装 Context 。
(四)View 的创建和兜底
通过这些信息,系统已经准备好如何创建 View 了。
遍历支持的组件列表,对于通用的 View,如 TextView
, ImageView
,直接生成对应的 AppCompat 子类。如果是未知类型的 View,将使用正确的 Context 调用 createView
,默认返回 null,但一般会被 AppCompatViewInflater 的子类重写。
如果这时候 view 仍然是 null,会检查 view 的原始 context 是否和父 View 的 context 一致。这种情况会发生在子 View 的 android:theme
和 父 View 不一致。
在检查 android:onClick
之后,view 就被返回了。
五、总结和使用实例
总结一下,AppCompatActivity
通过给 LayoutInflater
设置 Factory2
来介入 View 的创建过程,以提供向后兼容性(为组件提供 tint,处理 android:theme
等)。它也保证了可扩展性,开发者可以进行一些定制处理。
除了 Appcompat,这一技巧被用来完成了更多有意思的事情。Probe (现已废弃) 提供了 OvermeasureInterceptor 来记录 View 的测量次数,LayoutBoundsInterceptor 来高亮 View 的边界。
Calligraphy 使用这一技巧方便的为 TextView 添加字体。它使用了ViewPump 库,在 wiki 中提供了一些可能的使用方式。
最后,Google 的 Material Components for Android 通过自定义 AppCompatViewInflater
将 Button
替换为 MaterialButton
。
原文作者:ahmed el-helw
AppCompat View Inflation
标签:Factory2,LayoutInflater,视图,theme,Android,AppCompatActivity,TextView,View From: https://blog.51cto.com/u_16163453/6534426