大家好,我是路遥,每周五给你推荐一个泛移动端优质 Github 项目。
今天的主角是 ViewPump,可以直接介入布局文件中 View 的创建过程。上能修改 TextView 文字,字体,下能移花接木,替换各种 View。发挥你的想象力,它可以做到更多事情!
Organization | https://github.com/InflationX |
Url | https://github.com/InflationX/ViewPump |
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 日。
使用方法
添加依赖:
代码语言:javascript复制dependencies {
implementation 'io.github.inflationx:viewpump:2.0.3'
}
ViewPump
基于责任链模式,让用户自由实现 Interceptor
,可以在 View 创建前和创建后做一些自定义的处理。不理解的话,直接类比 Okhttp
的拦截器,可以对 Request
和 Respone
分别做处理。
下面用 Readme 中的两个简单例子说明一下使用方法。
第一个,在 View 创建之前直接进行替换。下面的例子中,直接将布局文件中的 TextView 在运行时替换为 CustomTextView 。
代码语言:javascript复制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 进行了增强:
代码语言:javascript复制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 原理 部分:https://juejin.cn/post/6844903974294781965#heading-34 。
最后
其实,介入布局文件 View 创建流程的方法并不止这一种。
你知道 AppCompat 是如何把 TextView 变成 AppCompatTextView 的吗?
你知道 MaterialComponent 是如何把 Button 变成 MaterialButton 的吗?
不妨阅读我的一篇译文 关于视图加载的一些奇技淫巧 。
这一期的介绍就到这里了,我们下周五见。