聊聊 RN 中 Android 提供 View 的那些坑

本文作者: 程磊

最近笔者研究 Android 中使用自定义 View 提供原生组件给 React Native(下面统一写成 RN ) 端的时候,遇到一些实际问题,在这里从 RN 的一些工作机制入手,分享一下问题的原因和解决方案。

自定义 View 内容不生效

原因

在给 RN 提供自定义 View 的时候发现自定义 View 内部很多 UI 逻辑没有生效。
例如下图,根据逻辑隐藏/展示了一些控件,但是应显示控件的位置没有变化。被隐藏控件的位置还是空出来的。很明显整个自定义 ViewrequestLayout 没有执行。

这里会从根节点往下一直更新子 View ,执行 Viewmeasurelayout
所以 ReactRootView 在宽高和测量模式都没有变化的情况下,就相当于把子 View 发出的 requestLayout 请求都拦截了。

解决方案

知道了原因就非常好解决了,既然你不让我通知我的根控件需要重新布局,那我就自己给自己重新布局好了。参考了 RN 一些自带的自定义 View 的实现,我们可以在这个自定义 View 重新布局的时候,注册一个 FrameCallback 去执行自己的 measurelayout 方法。

RN 自定义View 必须在JS端设置宽高

实现了自定义 View 之后,在 JSX 里面指定标签之后,会发现这个原生组件并没有显示。通过 IDE 的 Layout Inspect 可以发现此时这个自定义 Viewwidthheight 都是 0 。如果设置了 widthheight 的话就可以展示了。
这时候就很奇怪了, 为什么我的自定义 View 里面的内容明明是 WRAP_CONTENT 的,很多自定义 View 又是直接继承的 ConstraintLayoutRelativeLayout 这种 AndroidViewGroup ,但还是要指定宽高才能在 RN 中渲染出来呢?
要解决这个疑惑,就需要了解一下 RN 的渲染流程。

RN 是怎么确定 Native View的宽高的

我们顺着 RN 更新 View 结构的 UIImplementation#updateViewHierarchy 方法,发现有两处关键的逻辑:

updateLayout 的核心流程如下:

  • 调用 resolveView 方法获取到真实的控件对象。
  • 调用这个控件的 measure 方法。

  • 调用updateLayout,执行这个控件的 layout方法


而这个 layoutWidthlayoutHeight,则都是 Yoga 帮我们计算好,存放在 YogoNode里面的。
关于 Yoga

Yoga 是 Facebook 实现的一个高性能、易用、 Flex 的跨端布局引擎。
React Native 内部则是使用 Yoga 来布局的。
具体内容可以看 Yoga 的官网:https://yogalayout.com/

这里也就解释了为什么自定义 View 需要在 jsx 中指定了 widthheight 才会渲染出来。因为这些自定义 View 原本在 Android系统的 measure layout 流程都已经被 RN 给控制住了。
这里可以总结成一句话:
RN 中最终渲染出来的控件的宽高,都由 Yoga 引擎来计算决定,系统自身的布局流程无法直接决定这些控件的宽高
但是这时候还是有一个疑问,为什么RN自己的一些组件,例如 ,没有指定
宽高也可以正常自适应显示呢?

为什么 RN 自己的 Text 是有自己的宽高的

我们来看一下RN是怎么定义渲染出来的 TextView 的,找到对应的 TextViewViewManager,
com.facebook.react.views.text.ReactTextViewManager
我们关注两个方法:

  1. createViewInstance

  1. createShadowNodeInstance


这里是使用了 YogaMeasureOutput.makeLayout 算出来的宽高转成一定格式的二进制回调给 Yoga 引擎,这也是为什么 RN 自己的 Text 标签是可以自适应宽高展示的。
这里我们也可以得到一个结论:如果 Android 端封装的自定义 View 可以是确定宽高或者内部的控件是非常固定可以通过 measurelayout 就能算出宽高的,我们可以通过注册 measureFunction 回调的方式告诉 Yoga 我们 View 的宽高。
但是在实际业务中,我们很多业务组件是封装在 ConstraintLayoutRelativeLayout 等 ViewGroup 中,所以我们还需要其他的方法来解决组件宽高设置的问题。

解决方案

那么这个问题可以重写 ViewonMeasurelayout 方法来解决吗?看起来是这个做法是可以解决 View 宽高为 0 渲染不出来的问题。但是如果 jsx 这样描述布局的时候:

这时候 AndroidViewText 会同时显示,并且 AndroidViewText 遮住。
稍微思考一下就能得到原因:对于 Yoga 引擎来说,AndroidView 所代表的的节点仍然是没有宽高的,YogaNode 里面的 widthheight 仍然是 0,那么当重写 onMeasureonLayout 的逻辑生效后,View 显示的左上方顶点是 (0,0) 的坐标。
Yoga 引擎自己计算出 Text 的宽高后, Text 的左上方顶点坐标肯定也是 (0,0) ,所以这时候2个 View 会显示在同一个位置(重叠或者覆盖)。
所以这时候问题就变成了,我们想通过 Android 自己的布局流程来确定并刷新这个自定义控件,但是 Yoga 引擎并不知道。
所以想要解决这个问题,可行的有两条路:

  • 改变 UI 层级和自定义 View 的粒度
  • Native 测量出实际需要的宽高后同步给Yoga 引擎
增加自定义控件的粒度

举一个自定义控件的例子:

这时候内部的两个控件会自己去进行布局。最终展示出来的就是左右都是 Wrap_Content 的。

Native 测量出实际需要的宽高后同步给Yoga引擎

但是控制自定义 View 的粒度的方式总归是不够灵活,开发的时候也往往会让人犹豫是否拆分。接着之前的内容,既然这个问题的矛盾点在于 Yoga 不知道 Android 可以自己再次调用 measure 来确定宽高,那如果能把最新的宽高传给 Yoga,不就可以解决我们的问题吗?
具体怎么触发 YogaNode 的刷新呢?通过阅读源码可以找到解决方法。在 UIManage里面,有一个叫做 updateNodeSizeapi:

不过上面这个方案虽然可以解决 Viewwrap_content 显示的问题,但是存在一些缺点:
刷新 YogaNode 实际是在 requestLayout 的时候触发的,这就相当于 requestLayout 这种比较耗费性能的操作会双倍的执行。对于一些可能会频繁触发 requestLayout 的业务场景来说需要慎重考虑。如果遇到这种场景,还是需要根据自己的需求来灵活选择解决方式。

本文发布自 网易云音乐大前端团队,文章未经授权禁止任何形式的转载。我们常年招收前端、iOS、Android,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!

你可能感兴趣的:(android,reactnative,webview)