GitHub 精选 | 有了它,对于 View ,你无所不能!

2022-03-29 19:24:02 浏览数 (1)

大家好,我是路遥,每周五给你推荐一个泛移动端优质 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 的拦截器,可以对 RequestRespone 分别做处理。

下面用 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 创建之后修改了它的文字,添加了一个前缀。

代码语言:javascript复制
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 中初始化,添加拦截器。

代码语言:javascript复制
@Override
public void onCreate() {
    super.onCreate();
    ViewPump.init(ViewPump.builder()
                .addInterceptor(new TextUpdatingInterceptor())
                .addInterceptor(new CustomTextViewInterceptor())
                .build());
    //....
}

这里要注意拦截器的添加顺序。如果先添加 CustomTextViewInterceptorTextView 全都被替换了,导致 TextUpdatingInterceptor 失效。一般情况下,应该把 事前处理 的拦截器放在 事后处理 的拦截器之前。

最后在 ActivityattachBaseContext() 中加上下面的代码:

代码语言:javascript复制
@Override
protected void attachBaseContext(Context newBase) {
    super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase));
}

除了上面的两种简单用法,wiki 里还介绍了几种:

  1. 模拟 AppCompat 的行为
  2. 隐藏没有 contentDescription 的 View(为了促进无障碍的适配)
  3. 高亮特定的 View
  4. View 的各种功能增强,见 android-geocities-theme
  5. 动态修改 string 资源的文字,见 Philology

开动你的脑袋,肯定会有更多的用法。

实现原理

ViewPump.init() 只要是保存了用户添加的适配器,以及一些参数的配置,不详细展开。

重点看 Activity.attachBaseContext() 中添加的代码:

代码语言:javascript复制
@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

代码语言:javascript复制
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 使用了自定义的 FactoryFactory2

代码语言:javascript复制
  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()

代码语言:javascript复制
  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 的吗?

不妨阅读我的一篇译文 关于视图加载的一些奇技淫巧

这一期的介绍就到这里了,我们下周五见。

0 人点赞