本文作者:程磊
最近笔者钻研 Android
中应用自定义 View
提供原生组件给 React Native
(上面对立写成 RN
)端的时候,遇到一些理论问题,在这里从 RN
的一些工作机制动手,分享一下问题的起因和解决方案。
自定义 View 内容不失效
起因
在给 RN
提供自定义 View
的时候发现自定义 View
外部很多 UI
逻辑没有失效。
例如下图,依据逻辑暗藏 / 展现了一些控件,然而应显示控件的地位没有变动。被暗藏控件的地位还是空进去的。很显著整个自定义 View
的 requestLayout
没有执行。
问题的答案就在 RN
根布局 ReactRootView
的 measure
办法外面。
在这个 View 的测量过程中,会判断 measureSpec 是否有更新。
当 measureSpec
有变动,或者宽高有变动的时候,才会触发 updateRootLayoutSpecs
的逻辑。
持续看下 updateRootLayoutSpecs
里做了一些什么事件,跟着源码最初会执行到 UIImplementation
的 dispatchViewUpdates
办法:
最终执行:
这里会从根节点往下始终更新子 View
,执行 View
的 measure
和 layout
。
所以 ReactRootView
在宽高和测量模式都没有变动的状况下,就相当于把子 View
收回的 requestLayout
申请都拦挡了。
解决方案
晓得了起因就十分好解决了,既然你不让我告诉我的根控件须要从新布局,那我就本人给本人从新布局好了。参考了 RN
一些自带的自定义 View
的实现,咱们能够在这个自定义 View
从新布局的时候,注册一个 FrameCallback
去执行本人的 measure
和 layout
办法。
RN 自定义 View 必须在 JS 端设置宽高
实现了自定义 View
之后,在 JSX
外面指定标签之后,会发现这个原生组件并没有显示。通过 IDE 的 Layout Inspect
能够发现此时这个自定义 View
的 width
和 height
都是 0
。如果设置了 width
和 height
的话就能够展现了。
这时候就很奇怪了,为什么我的自定义 View
外面的内容明明是 WRAP_CONTENT
的,很多自定义 View
又是间接继承的 ConstraintLayout
、RelativeLayout
这种 Android
的 ViewGroup
,但还是要指定宽高能力在 RN
中渲染进去呢?
要解决这个纳闷,就须要理解一下 RN
的渲染流程。
RN 是怎么确定 Native View 的宽高的
咱们顺着 RN
更新 View
构造的 UIImplementation#updateViewHierarchy
办法,发现有两处要害的逻辑:calculateRootLayout
中调用了 cssRoot
的布局计算逻辑:
接下来就是 applyUpdatesRecursive
,顾名思义就是递归的更新根节点的所有子节点,在咱们的场景中即整个页面的布局。
须要更新的节点则调用了 dispatchUpdates
办法,执行 enqueueUpdateLayout
, 调用 NativeViewHierarchyManager#updateLayout
逻辑。updateLayout
的外围流程如下:
- 调用
resolveView
办法获取到实在的控件对象。 - 调用这个控件的
measure
办法。
- 调用
updateLayout
,执行这个控件的layout
办法
发现了没有?这里的 width
、height
曾经是固定的值别离传给了 meausre
和 layout
, 也就是说,这些 View
的宽高基本不是 Android
的绘制流程决定的,那么这个 width
和 height
的值是从哪里来的呢?
回头看看就发现了答案:
宽高是 left
、top
、right
、bottom
坐标相减失去的,而这些坐标则是通过 getLayoutWidth
和 getLayoutHeight
失去的:
而这个 layoutWidth
和 layoutHeight
,则都是 Yoga
帮咱们计算好,寄存在 YogoNode
外面的。
对于 Yoga
Yoga 是 Facebook 实现的一个高性能、易用、Flex 的跨端布局引擎。React Native 外部则是应用 Yoga 来布局的。具体内容能够看 Yoga 的官网:https://yogalayout.com/
这里也就解释了为什么自定义 View
须要在 jsx
中指定了 width
和 height
才会渲染进去。因为这些自定义 View
本来在 Android
零碎的 measure
layout
流程都曾经被 RN
给管制住了。
这里能够总结成一句话:
RN 中最终渲染进去的控件的宽高,都由 Yoga 引擎来计算决定,零碎本身的布局流程无奈间接决定这些控件的宽高
然而这时候还是有一个疑难,为什么 RN 本人的一些组件,例如 <Text/>
,没有指定
宽高也能够失常自适应显示呢?
为什么 RN 本人的 Text 是有本人的宽高的
咱们来看一下 RN 是怎么定义渲染进去的 TextView
的,找到对应的 TextView
的 ViewManager
,com.facebook.react.views.text.ReactTextViewManager
咱们关注两个办法:
- createViewInstance
- createShadowNodeInstance
其中,ReactTextView
其实就是实现了一个一般的 Android TextView
, ReactTextShadowNode
则示意了这个 TextView
对应的 YogaNode
的实现。
在它的实现中,咱们能够看到一个成员变量,从名字上看是负责这个 YogaNode
的 measure
工作。YogaNodeJNIBase
会调用这个 JNI 的办法,给 JNI 的逻辑注册这样一个回调函数。
这个 YogaMeasureFunction
的具体实现:
这里截个图,能够看到这里调用了 Android
中 Text
绘制的 API
来确定的文本的宽高。函数返回的是
这里是应用了 YogaMeasureOutput.make
把 Layout
算进去的宽高转成肯定格局的二进制回调给 Yoga
引擎,这也是为什么 RN
本人的 Text
标签是能够自适应宽高展现的。
这里咱们也能够失去一个论断:如果 Android
端封装的自定义 View
能够是确定宽高或者外部的控件是十分固定能够通过 measure
和 layout
就能算出宽高的,咱们能够通过注册 measureFunction
回调的形式通知 Yoga
咱们 View
的宽高。
然而在理论业务中,咱们很多业务组件是封装在 ConstraintLayout
、RelativeLayout
等 ViewGroup 中,所以咱们还须要其余的办法来解决组件宽高设置的问题。
解决方案
那么这个问题能够重写 View
的 onMeasure
和 layout
办法来解决吗?看起来是这个做法是能够解决 View
宽高为 0
渲染不进去的问题。然而如果 jsx
这样形容布局的时候:
这时候 AndroidView
和 Text
会同时显示,并且 AndroidView
被 Text
遮住。
略微思考一下就能失去起因:对于 Yoga
引擎来说,AndroidView
所代表的的节点依然是没有宽高的,YogaNode
外面的 width
、height
依然是 0
,那么当重写 onMeasure
和 onLayout
的逻辑失效后,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
外面,有一个叫做 updateNodeSize
的 api
:
这个 api
会更新 View
对应的 cssNode
的大小,而后散发刷新 View
的逻辑。这个逻辑是须要保障在后盾音讯队列外面执行的,所以须要把这个刷新的音讯发送到 nativeModulesQueueThread
外面去执行。
咱们在 ViewManager
外面保留这个 Manager
对应的 View
和 ReactNodeImpl
实例。例如 Android
端封装了一个 LinearLayout
,对应的 node
是 MyLinearLayoutNode
。
重写自定义 View
的 onMeasure
,让本人是 wrap_content
的布局:
在 requestLayout
中依据本人实在的宽高布局并触发以下逻辑:
不过下面这个计划尽管能够解决 View
的 wrap_content
显示的问题,然而存在一些毛病:
刷新 YogaNode
理论是在 requestLayout
的时候触发的,这就相当于 requestLayout
这种比拟消耗性能的操作会双倍的执行。对于一些可能会频繁触发 requestLayout
的业务场景来说须要慎重考虑。如果遇到这种场景,还是须要依据本人的需要来灵便抉择解决形式。
本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!