跳到主要内容
新架构实战课 实操 + 基建 + 原理全维度包揽,抢先掌握 React Native 新架构精髓 立即查看 >

Android 原生UI组件

提示

原生模块和原生组件是我们传统架构中使用的稳定技术。 当新架构稳定后,它们将被弃用。新架构使用TurboModuleFabric 组件来实现类似的功能。

在如今的 App 中,已经有成千上万的原生 UI 部件了——其中的一些是平台的一部分,另一些可能来自于一些第三方库,而且可能你自己还收藏了很多。React Native 已经封装了大部分最常见的组件,譬如ScrollViewTextInput,但不可能封装全部组件。而且,说不定你曾经为自己以前的 App 还封装过一些组件,React Native 肯定没法包含它们。幸运的是,在 React Naitve 应用程序中封装和植入已有的组件非常简单。

和原生模块向导一样,本向导也是一个相对高级的向导,我们假设你已经对 Android 编程颇有经验。本向导会引导你如何构建一个原生 UI 组件,带领你了解 React Native 核心库中ImageView组件的具体实现。

info

您还可以通过一个命令来配置生成包含原生组件的本地库模板。阅读本地库设置指南以获取更多详细信息。

ImageView 示例

在这个例子里,我们来看看为了让 JavaScript 中可以使用 ImageView,需要做哪些准备工作。

原生视图需要被一个ViewManager的派生类(或者更常见的,SimpleViewManager的派生类)创建和管理。一个SimpleViewManager可以用于这个场景,是因为它能够包含更多公共的属性,譬如背景颜色、透明度、Flexbox 布局等等。

这些子类本质上都是单例——React Native 只会为每个管理器创建一个实例。它们创建原生的视图并提供给NativeViewHierarchyManager,NativeViewHierarchyManager 则会反过来委托它们在需要的时候去设置和更新视图的属性。ViewManager还会代理视图的所有委托,并给 JavaScript 发回对应的事件。

提供原生视图很简单:

  1. 创建一个 ViewManager 的子类。
  2. 实现createViewInstance方法。
  3. 导出视图的属性设置器:使用@ReactProp(或@ReactPropGroup)注解。
  4. 把这个视图管理类注册到应用程序包的createViewManagers里。
  5. 实现 JavaScript 模块。

1. 创建ViewManager的子类

在这个例子里我们创建一个视图管理类ReactImageManager,它继承自SimpleViewManager<ReactImageView>ReactImageView是这个视图管理类所管理的对象类型,也就是我们自定义的原生视图。getName方法返回的名字会用于在 JavaScript 端引用。

class ReactImageManager(
private val callerContext: ReactApplicationContext
) : SimpleViewManager<ReactImageView>() {

override fun getName() = REACT_CLASS

companion object {
const val REACT_CLASS = "RCTImageView"
}
}

2. 实现方法createViewInstance

视图在createViewInstance中创建,且应当把自己初始化为默认的状态。所有属性的设置都通过后续的updateView来进行。

  override fun createViewInstance(context: ThemedReactContext) =
ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, callerContext)

3. 通过@ReactProp(或@ReactPropGroup)注解来导出属性的设置方法。

要导出给 JavaScript 使用的属性,需要申明带有@ReactProp(或@ReactPropGroup)注解的设置方法。方法的第一个参数是要修改属性的视图实例,第二个参数是要设置的属性值。方法的返回值类型必须为void,在 Kotlin 中是 Unit,而且访问控制必须被声明为public。JavaScript 所得知的属性类型会由该方法第二个参数的类型来自动决定。支持的类型有:boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap。Kotlin 中对应的则是 Boolean, Int, Float, Double, String, ReadableArray, ReadableMap.

@ReactProp注解必须包含一个字符串类型的参数name。这个参数指定了对应属性在 JavaScript 端的名字。

除了name@ReactProp注解还接受这些可选的参数:defaultBoolean, defaultInt, defaultFloat。这些参数必须是对应的基础类型的值(也就是对应 Java 中的 boolean, int, float, 或是 Kotlin 中的 Boolean, Int, Float),这些值会被传递给 setter 方法,以免 JavaScript 端某些情况下在组件中移除了对应的属性。注意这个"默认"值只对基本类型生效,对于其他的类型而言,当对应的属性删除时,null会作为默认值提供给方法。

使用@ReactPropGroup来注解的设置方法和@ReactProp不同。请参见@ReactPropGroup注解类源代码中的文档来获取更多详情。

重要! 在 ReactJS 里,修改一个属性会引发一次对设置方法的调用。有一种修改情况是,移除掉之前设置的属性。在这种情况下设置方法也一样会被调用,并且“默认”值会被作为参数提供(对于基础类型来说可以通过defaultBooleandefaultFloat@ReactProp的属性提供,而对于复杂类型来说参数则会设置为null

  @ReactProp(name = "src")
fun setSrc(view: ReactImageView, sources: ReadableArray?) {
view.setSource(sources)
}

@ReactProp(name = "borderRadius", defaultFloat = 0f)
override fun setBorderRadius(view: ReactImageView, borderRadius: Float) {
view.setBorderRadius(borderRadius)
}

@ReactProp(name = ViewProps.RESIZE_MODE)
fun setResizeMode(view: ReactImageView, resizeMode: String?) {
view.setScaleType(ImageResizeMode.toScaleType(resizeMode))
}

4. 注册ViewManager

最后一步就是把视图控制器注册到应用中。这和原生模块的注册方法类似,唯一的区别是我们把它放到createViewManagers方法的返回值里。

  override fun createViewManagers(
reactContext: ReactApplicationContext
) = listOf(ReactImageManager(reactContext))

完成上面这些代码后,请一定记得要重新编译!(运行yarn android命令)

5. 实现对应的 JavaScript 模块

整个过程的最后一步就是创建 JavaScript 模块并且定义 Java 和 JavaScript 之间的接口层。我们建议你使用 TypeScript 来规范定义接口的具体结构,或者至少用注释说明清楚(老版本的 RN 使用propTypes来规范接口定义,这一做法已不再支持)。

ImageView.tsx
import { requireNativeComponent } from 'react-native';

/**
* Composes `View`.
*
* - src: Array<{url: string}>
* - borderRadius: number
* - resizeMode: 'cover' | 'contain' | 'stretch'
*/
export default requireNativeComponent('RCTImageView');

requireNativeComponent目前只接受一个参数,即原生视图的名字。如果你还需要做一些复杂的逻辑譬如事件处理,那么可以把原生组件用一个普通 React 组件封装。后文的MyCustomView例子里演示了这种用法。

事件

现在我们已经知道了怎么导出一个原生视图组件,并且我们可以在 JS 里很方便的控制它了。不过我们怎么才能处理来自用户的事件,譬如缩放操作或者拖动?当一个原生事件发生的时候,它应该也能触发 JavaScript 端视图上的事件,这两个视图会依据getId()而关联在一起。

class MyCustomView(context: Context) : View(context) {
...
fun onReceiveNativeEvent() {
val event = Arguments.createMap().apply {
putString("message", "MyMessage")
}
val reactContext = context as ReactContext
reactContext
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, "topChange", event)
}
}

要把事件名topChange映射到 JavaScript 端的onChange回调属性上,需要在你的ViewManager中覆盖getExportedCustomBubblingEventTypeConstants方法,并在其中进行注册:

class ReactImageManager : SimpleViewManager<MyCustomView>() {
...
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return mapOf(
"topChange" to mapOf(
"phasedRegistrationNames" to mapOf(
"bubbled" to "onChange"
)
)
)
}
}

这个回调会传递一个原生事件对象,一般来说我们会在封装组件里进行处理以便外部使用:

MyCustomView.tsx
import React, { useCallback } from 'react';

const MyCustomView = ({ onChangeMessage, ...props }) => {
const onChange = useCallback((event) => {
if (!onChangeMessage) {
return;
}
onChangeMessage(event.nativeEvent.message);
}, [onChangeMessage]);

return (
<RCTMyCustomView
{...props}
onChange={onChange}
/>
);
};


const RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`);

与 Android Fragment 的整合实例

为了将现有的原生 UI 元素整合到 React Native 应用中,你可能需要使用 Android Fragments 来对本地组件进行更精细的控制,而不是从 ViewManager 返回一个 View。如果你想在生命周期方法的帮助下添加与视图绑定的自定义逻辑,如onViewCreatedonPauseonResume,你会用得到它。下面的步骤将告诉你如何做到这一点:

1. 创建一个自定义视图

首先,我们创建一个继承自FrameLayoutCustomView类(这个视图的内容可以是您想要渲染的任何视图)

CustomView.kt
// replace with your package
package com.mypackage

import android.content.Context
import android.graphics.Color
import android.widget.FrameLayout
import android.widget.TextView

class CustomView(context: Context) : FrameLayout(context) {
init {
// set padding and background color
setPadding(16,16,16,16)
setBackgroundColor(Color.parseColor("#5FD3F3"))

// add default text view
addView(TextView(context).apply {
text = "Welcome to Android Fragments with React Native."
})
}
}

2. 创建一个 Fragment

MyFragment.kt
// replace with your package
package com.mypackage

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

// replace with your view's import
import com.mypackage.CustomView

class MyFragment : Fragment() {
private lateinit var customView: CustomView

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
customView = CustomView(requireNotNull(context))
return customView // this CustomView could be any view that you want to render
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// do any logic that should happen in an `onCreate` method, e.g:
// customView.onCreate(savedInstanceState);
}

override fun onPause() {
super.onPause()
// do any logic that should happen in an `onPause` method
// e.g.: customView.onPause();
}

override fun onResume() {
super.onResume()
// do any logic that should happen in an `onResume` method
// e.g.: customView.onResume();
}

override fun onDestroy() {
super.onDestroy()
// do any logic that should happen in an `onDestroy` method
// e.g.: customView.onDestroy();
}
}

3. 创建 ViewManager 子类

MyViewManager.kt
// replace with your package
package com.mypackage

import android.view.Choreographer
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.fragment.app.FragmentActivity
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactPropGroup

class MyViewManager(
private val reactContext: ReactApplicationContext
) : ViewGroupManager<FrameLayout>() {
private var propWidth: Int? = null
private var propHeight: Int? = null

override fun getName() = REACT_CLASS

/**
* Return a FrameLayout which will later hold the Fragment
*/
override fun createViewInstance(reactContext: ThemedReactContext) =
FrameLayout(reactContext)

/**
* Map the "create" command to an integer
*/
override fun getCommandsMap() = mapOf("create" to COMMAND_CREATE)

/**
* Handle "create" command (called from JS) and call createFragment method
*/
override fun receiveCommand(
root: FrameLayout,
commandId: String,
args: ReadableArray?
) {
super.receiveCommand(root, commandId, args)
val reactNativeViewId = requireNotNull(args).getInt(0)

when (commandId.toInt()) {
COMMAND_CREATE -> createFragment(root, reactNativeViewId)
}
}

@ReactPropGroup(names = ["width", "height"], customType = "Style")
fun setStyle(view: FrameLayout, index: Int, value: Int) {
if (index == 0) propWidth = value
if (index == 1) propHeight = value
}

/**
* Replace your React Native view with a custom fragment
*/
fun createFragment(root: FrameLayout, reactNativeViewId: Int) {
val parentView = root.findViewById<ViewGroup>(reactNativeViewId)
setupLayout(parentView)

val myFragment = MyFragment()
val activity = reactContext.currentActivity as FragmentActivity
activity.supportFragmentManager
.beginTransaction()
.replace(reactNativeViewId, myFragment, reactNativeViewId.toString())
.commit()
}

fun setupLayout(view: View) {
Choreographer.getInstance().postFrameCallback(object: Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
manuallyLayoutChildren(view)
view.viewTreeObserver.dispatchOnGlobalLayout()
Choreographer.getInstance().postFrameCallback(this)
}
})
}

/**
* Layout all children properly
*/
private fun manuallyLayoutChildren(view: View) {
// propWidth and propHeight coming from react-native props
val width = requireNotNull(propWidth)
val height = requireNotNull(propHeight)

view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))

view.layout(0, 0, width, height)
}

companion object {
private const val REACT_CLASS = "MyViewManager"
private const val COMMAND_CREATE = 1
}
}

4. 注册 ViewManager

MyPackage.kt
// replace with your package
package com.mypackage

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class MyPackage : ReactPackage {
...
override fun createViewManagers(
reactContext: ReactApplicationContext
) = listOf(MyViewManager(reactContext))
}

5. 注册 Package

MainApplication.kt
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(MyAppPackage())
}

6. 执行 JavaScript 模块

I. MyViewManager.tsx

MyViewManager.tsx
import { requireNativeComponent } from 'react-native';
export const MyViewManager =
requireNativeComponent('MyViewManager');

II. MyView.tsx 调用 create 方法

MyView.tsx
import React, { useEffect, useRef } from 'react';
import { UIManager, findNodeHandle } from 'react-native';
import { MyViewManager } from './my-view-manager';
const createFragment = viewId =>
UIManager.dispatchViewManagerCommand(
viewId,
// we are calling the 'create' command
UIManager.MyViewManager.Commands.create.toString(),
[viewId],
);

export const MyView = ({ style }) => {
const ref = useRef(null);

useEffect(() => {
const viewId = findNodeHandle(ref.current);
createFragment(viewId!);
}, []);

return (
<MyViewManager
style={{
...(style || {}),
height: style && style.height !== undefined ? style.height || '100%',
width: style && style.width !== undefined ? style.width || '100%'
}}
ref={ref}
/>
);
};

如果您想使用公开属性设置器 @ReactProp (or @ReactPropGroup) 详见上面的 ImageView 示例