关于android:聊聊-RN-中-Android-提供-View-的那些坑

65次阅读

共计 4201 个字符,预计需要花费 11 分钟才能阅读完成。

本文作者:程磊

最近笔者钻研 Android 中应用自定义 View 提供原生组件给 React Native(上面对立写成 RN)端的时候,遇到一些理论问题,在这里从 RN 的一些工作机制动手,分享一下问题的起因和解决方案。

自定义 View 内容不失效

起因

在给 RN 提供自定义 View 的时候发现自定义 View 外部很多 UI 逻辑没有失效。
例如下图,依据逻辑暗藏 / 展现了一些控件,然而应显示控件的地位没有变动。被暗藏控件的地位还是空进去的。很显著整个自定义 ViewrequestLayout 没有执行。

问题的答案就在 RN 根布局 ReactRootViewmeasure 办法外面。

在这个 View 的测量过程中,会判断 measureSpec 是否有更新。

measureSpec 有变动,或者宽高有变动的时候,才会触发 updateRootLayoutSpecs 的逻辑。
持续看下 updateRootLayoutSpecs 里做了一些什么事件,跟着源码最初会执行到 UIImplementationdispatchViewUpdates 办法:

最终执行:

这里会从根节点往下始终更新子 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 办法,发现有两处要害的逻辑:

calculateRootLayout 中调用了 cssRoot 的布局计算逻辑:

接下来就是 applyUpdatesRecursive,顾名思义就是递归的更新根节点的所有子节点,在咱们的场景中即整个页面的布局。

须要更新的节点则调用了 dispatchUpdates 办法,执行 enqueueUpdateLayout, 调用 NativeViewHierarchyManager#updateLayout 逻辑。

updateLayout 的外围流程如下:

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

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


发现了没有?这里的 widthheight 曾经是固定的值别离传给了 meausrelayout, 也就是说,这些 View 的宽高基本不是 Android 的绘制流程决定的,那么这个 widthheight 的值是从哪里来的呢?
回头看看就发现了答案:

宽高是 lefttoprightbottom坐标相减失去的,而这些坐标则是通过
getLayoutWidthgetLayoutHeight 失去的:

而这个 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 本人的一些组件,例如 <Text/>,没有指定
宽高也能够失常自适应显示呢?

为什么 RN 本人的 Text 是有本人的宽高的

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

  1. createViewInstance

  1. createShadowNodeInstance


其中,ReactTextView 其实就是实现了一个一般的 Android TextView, ReactTextShadowNode 则示意了这个 TextView 对应的 YogaNode 的实现。

在它的实现中,咱们能够看到一个成员变量,从名字上看是负责这个 YogaNodemeasure 工作。

YogaNodeJNIBase 会调用这个 JNI 的办法,给 JNI 的逻辑注册这样一个回调函数。

这个 YogaMeasureFunction 的具体实现:

这里截个图,能够看到这里调用了 AndroidText 绘制的 API 来确定的文本的宽高。函数返回的是

这里是应用了 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 引擎
减少自定义控件的粒度

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

咱们心愿把这个图上第一行的控件拆分成粒度较低的自定义 View 交给 RN 来布局实现布局动静配置的能力。然而这类场景的左右两边控件都是自适应宽度。这时候在 JS 端其实没有方法提供一个适合的宽度。思考到更多场景下同一个方向轴上的自适应宽度控件是有地位上的依赖性的,所以能够不拆分这两个局部,间接都定义在同一个自定义 View 内:

提供给 JS 端应用,没有宽高的话,就把整个 SingHeaderView 的宽度设置成

这时候外部的两个控件会本人去进行布局。最终展现进去的就是左右都是 Wrap_Content 的。

Native 测量出理论须要的宽高后同步给 Yoga 引擎

然而管制自定义 View 的粒度的形式总归是不够灵便,开发的时候也往往会让人犹豫是否拆分。接着之前的内容,既然这个问题的矛盾点在于 Yoga 不晓得 Android 能够本人再次调用 measure 来确定宽高,那如果能把最新的宽高传给 Yoga,不就能够解决咱们的问题吗?
具体怎么触发 YogaNode 的刷新呢?通过浏览源码能够找到解决办法。在 UIManage外面,有一个叫做 updateNodeSizeapi:

这个 api 会更新 View 对应的 cssNode 的大小,而后散发刷新 View 的逻辑。这个逻辑是须要保障在后盾音讯队列外面执行的,所以须要把这个刷新的音讯发送到 nativeModulesQueueThread 外面去执行。
咱们在 ViewManager 外面保留这个 Manager 对应的 ViewReactNodeImpl 实例。例如 Android 端封装了一个 LinearLayout,对应的 nodeMyLinearLayoutNode

重写自定义 ViewonMeasure,让本人是 wrap_content 的布局:

requestLayout 中依据本人实在的宽高布局并触发以下逻辑:


不过下面这个计划尽管能够解决 Viewwrap_content 显示的问题,然而存在一些毛病:
刷新 YogaNode 理论是在 requestLayout 的时候触发的,这就相当于 requestLayout 这种比拟消耗性能的操作会双倍的执行。对于一些可能会频繁触发 requestLayout 的业务场景来说须要慎重考虑。如果遇到这种场景,还是须要依据本人的需要来灵便抉择解决形式。

本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0