今天的主角是 ViewPump,可以直接介入布局文件中 View 的创建过程。上能修改 TextView 文字,字体,下能移花接木,替换各种 View。发挥你的想象力,它可以做到更多事情!
Organization | github.com/InflationX |
Url | github.com/InflationX/… |
Language | Kotlin/Java |
Star | 757 |
Fork | 39 |
Issue | 22 Open/17 Closed |
Commits | 54 |
Last Update | 8 Jun 2019 |
License | Apache-2.0 |
以上数据截止至 2022 年 3 月 3 日。
使用方法
添加依赖:
dependencies { implementation 'io.github.inflationx:viewpump:2.0.3' } 复制代码
ViewPump
基于责任链模式,让用户自由实现 Interceptor
,可以在 View 创建前和创建后做一些自定义的处理。不理解的话,直接类比 Okhttp
的拦截器,可以对 Request
和 Respone
分别做处理。
下面用 Readme 中的两个简单例子说明一下使用方法。
第一个,在 View 创建之前直接进行替换。下面的例子中,直接将布局文件中的 TextView 在运行时替换为 CustomTextView 。
public class CustomTextViewInterceptor implements Interceptor { @Override public InflateResult intercept(Chain chain) { InflateRequest request = chain.request(); if (request.name().endsWith("TextView")) { CustomTextView view = new CustomTextView(request.context(), request.attrs()); return InflateResult.builder() .view(view) .name(view.getClass().getName()) .context(request.context()) .attrs(request.attrs()) .build(); } else { return chain.proceed(request); } } } 复制代码
第二个,在 View 创建之后做一些修改。下面的例子中,在 TextView
创建之后修改了它的文字,添加了一个前缀。
public class TextUpdatingInterceptor implements Interceptor { @Override public InflateResult intercept(Chain chain) { InflateResult result = chain.proceed(chain.request()); if (result.view() instanceof TextView) { // Do something to result.view() // You have access to result.context() and result.attrs() TextView textView = (TextView) result.view(); textView.setText("[Prefix] " + textView.getText()); } return result; } } 复制代码
别忘了在 Application
中初始化,添加拦截器。
@Override public void onCreate() { super.onCreate(); ViewPump.init(ViewPump.builder() .addInterceptor(new TextUpdatingInterceptor()) .addInterceptor(new CustomTextViewInterceptor()) .build()); //.... } 复制代码
这里要注意拦截器的添加顺序。如果先添加 CustomTextViewInterceptor
,TextView
全都被替换了,导致 TextUpdatingInterceptor
失效。一般情况下,应该把 事前处理 的拦截器放在 事后处理 的拦截器之前。
最后在 Activity
的 attachBaseContext()
中加上下面的代码:
@Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase)); } 复制代码
除了上面的两种简单用法,wiki 里还介绍了几种:
- 模拟 AppCompat 的行为
- 隐藏没有 contentDescription 的 View(为了促进无障碍的适配)
- 高亮特定的 View
- View 的各种功能增强,见 android-geocities-theme
- 动态修改 string 资源的文字,见 Philology
开动你的脑袋,肯定会有更多的用法。
实现原理
ViewPump.init()
只要是保存了用户添加的适配器,以及一些参数的配置,不详细展开。
重点看 Activity.attachBaseContext()
中添加的代码:
@Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase)); } 复制代码
基于装饰器模式对原来的 Context 进行了增强:
class ViewPumpContextWrapper private constructor(base: Context) : ContextWrapper(base) { private val inflater: `-ViewPumpLayoutInflater` by lazy(NONE) { `-ViewPumpLayoutInflater`( LayoutInflater.from(baseContext), this, false) } override fun getSystemService(name: String): Any? { // 返回自定义的 LayoutInflater if (Context.LAYOUT_INFLATER_SERVICE == name) { return inflater } return super.getSystemService(name) } ... } 复制代码
重写了 getSystemService()
方法,当获取的服务名称是 layout_inflater 时,返回自定义的 ViewPumpLayoutInflater
。
internal class `-ViewPumpLayoutInflater`( original: LayoutInflater, newContext: Context, cloned: Boolean ) : LayoutInflater(original, newContext), `-ViewPumpActivityFactory` { ... init { setUpLayoutFactories(cloned) } // 使用自定义的 Factory/Factory2 private fun setUpLayoutFactories(cloned: Boolean) { if (cloned) return // If we are HC+ we get and set Factory2 otherwise we just wrap Factory1 if (factory2 != null && factory2 !is WrapperFactory2) { // Sets both Factory/Factory2 factory2 = factory2 } // We can do this as setFactory2 is used for both methods. if (factory != null && factory !is WrapperFactory) { factory = factory } } ... } 复制代码
ViewPumpLayoutInflater 使用了自定义的 Factory
和 Factory2
。
private class WrapperFactory(factory: LayoutInflater.Factory) : LayoutInflater.Factory { private val viewCreator: FallbackViewCreator = WrapperFactoryViewCreator(factory) override fun onCreateView(name: String, context: Context, attrs: AttributeSet?): View? { return ViewPump.get() .inflate(InflateRequest( name = name, context = context, attrs = attrs, fallbackViewCreator = viewCreator )) .view } } private open class WrapperFactory2(factory2: LayoutInflater.Factory2) : LayoutInflater.Factory2 { private val viewCreator = WrapperFactory2ViewCreator(factory2) override fun onCreateView(name: String, context: Context, attrs: AttributeSet?): View? { return onCreateView(null, name, context, attrs) } override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet? ): View? { return ViewPump.get() .inflate(InflateRequest( name = name, context = context, attrs = attrs, parent = parent, fallbackViewCreator = viewCreator )) .view } } 复制代码
Factory/Factory2 的 onCreateView
方法最后都指向 ViewPump.get().inflate()
。
fun inflate(originalRequest: InflateRequest): InflateResult { val chain = `-InterceptorChain`(interceptorsWithFallback, 0, originalRequest) return chain.proceed(originalRequest) } 复制代码
对应责任链模式的拦截器实现。
自定义的 LayoutInflater
,自定义的 Factory/Factory2
,难怪 ViewPump 可以为所欲为。
不熟悉 xml 布局文件加载流程的同学,可能还不不大能理解实现原理,推荐阅读蓝师傅的 《总结UI原理和高级的UI优化方式》 一文中的 LayoutInflater 原理 部分: juejin.cn/post/684490… 。
最后
其实,介入布局文件 View 创建流程的方法并不止这一种。
你知道 AppCompat 是如何把 TextView 变成 AppCompatTextView 的吗?
你知道 MaterialComponent 是如何把 Button 变成 MaterialButton 的吗?
不妨阅读我的一篇译文 关于视图加载的一些奇技淫巧 。
这一期的介绍就到这里了,我们下周五见。
如果你有好的项目推荐,欢迎给我留言。