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

简介: GitHub 精选 #3| 有了它,对于 View ,你无所不能!

今天的主角是 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 的拦截器,可以对 RequestRespone 分别做处理。


下面用 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());
    //....
}
复制代码


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


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

@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() 中添加的代码:

@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 使用了自定义的 FactoryFactory2

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 的吗?

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

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

如果你有好的项目推荐,欢迎给我留言。



相关文章
|
XML Android开发 数据格式
简单使用BottomSheetBehavior实现底部弹窗
这次带来的是BottomSheetBehavior的简单使用,BottomSheetBehavior是Android Support Library23.2中引入的,它可以轻松实现底部动作条功能。
5354 0
|
Java API Spring
【Springboot】springboot 多线程@EnableAsync和@Async
【Springboot】springboot 多线程@EnableAsync和@Async
761 0
|
JavaScript Java Spring
@Async异步失效的9种场景
在Spring中,启用@Async异步功能需要在启动类或配置类上使用`@EnableAsync`。若未使用此注解,@Async将无效。另外,内部方法调用(如在一个类的方法中调用另一个被@Async注解的方法)会导致异步功能失效,因为这不涉及Spring的AOP代理。此外,@Async方法必须是public,返回类型为void或Future,不能是static或final,且其所在的类需被@Service等注解以使Spring管理。如果使用@ComponentScan,确保正确扫描包含@Async类的包路径。
724 1
|
Java Android开发
Invoke-customs are only supported starting with Android,Static interface methods are only supported
Invoke-customs are only supported starting with Android,Static interface methods are only supported
363 2
|
资源调度 JavaScript 前端开发
阿珊详解Vue路由的两种模式:hash模式与history模式
阿珊详解Vue路由的两种模式:hash模式与history模式
|
Java
(详解)java:无效的目标发行版: 11
(详解)java:无效的目标发行版: 11
808 1
|
Java Spring
SpringBoot - @Async异步任务与线程池
SpringBoot - @Async异步任务与线程池
1439 1
|
SQL 缓存 监控
AliSQL
AliSQL
625 0
|
前端开发 NoSQL Java
Spring Boot系列之@Async异步调用
Spring Boot系列之@Async异步调用
4262 0