跳到主要内容
版本:0.81

Android 原生 UI 组件

信息

原生模块和原生组件是我们旧架构中使用的稳定技术。 当新架构稳定后,它们将在未来被弃用。新架构使用 Turbo Native Module 和 Fabric Native Components 来实现类似的效果。

这里有大量可供最新应用使用的原生 UI 组件——其中一些属于平台本身,另一些作为第三方库提供,还有一些可能已经存在于你自己的项目中。React Native 已经封装了若干最关键的平台组件,比如 ScrollView 和 TextInput,但并不是全部,更不一定包括你在之前的某个应用里自己写过的组件。幸运的是,我们可以把这些现有组件包装起来,让它们与 React Native 应用无缝集成。

和原生模块指南一样,这也是一份更高级的指南,默认你已经对 Android SDK 编程有一定了解。本指南将向你展示如何构建一个原生 UI 组件,并带你实现 React Native 核心库中现有 ImageView 组件的一个子集。

信息

你也可以通过一条命令设置包含原生组件的本地库。阅读 本地库设置 指南以了解更多详情。

ImageView 示例​

在这个示例中,我们将逐步完成实现要求,以便在 JavaScript 中使用 ImageView。

原生视图通过继承 ViewManager 或更常见的 SimpleViewManager 来创建和操作。SimpleViewManager 在这里很方便,因为它会应用背景色、不透明度和 Flexbox 布局等通用属性。

这些子类本质上是单例——桥接层只会为每个类创建一个实例。它们会把原生视图发送给 NativeViewHierarchyManager,后者再回调它们,按需设置和更新视图属性。ViewManager 通常也充当这些视图的代理,通过桥接层把事件发送回 JavaScript。

要发送一个视图:

  1. 创建 ViewManager 子类。
  2. 实现 createViewInstance 方法
  3. 使用 @ReactProp(或 @ReactPropGroup)注解暴露视图属性的 setter
  4. 在应用的包中通过 createViewManagers 注册该管理器。
  5. 实现 JavaScript 模块

1. 创建 ViewManager 子类​

在这个示例中,我们创建名为 ReactImageManager 的视图管理器类,它继承 SimpleViewManager<ReactImageView>。ReactImageView 是该管理器所管理的对象类型,也就是自定义的原生视图。getName 返回的名称用于在 JavaScript 中引用该原生视图类型。

Java
public class ReactImageManager extends SimpleViewManager<ReactImageView> {

public static final String REACT_CLASS = "RCTImageView";
ReactApplicationContext mCallerContext;

public ReactImageManager(ReactApplicationContext reactContext) {
mCallerContext = reactContext;
}

@Override
public String getName() {
return REACT_CLASS;
}
}

2. 实现 createViewInstance 方法​

视图在 createViewInstance 方法中创建,视图应初始化到默认状态,任何属性都会通过后续对 updateView 的调用来设置。

Java
@Override
public ReactImageView createViewInstance(ThemedReactContext context) {
return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);
}

3. 使用 @ReactProp(或 @ReactPropGroup)注解暴露视图属性的 setter​

需要在 JavaScript 中反映的属性,应作为带有 @ReactProp(或 @ReactPropGroup)注解的 setter 方法暴露出来。setter 方法应以要更新的视图(当前视图类型)作为第一个参数,以属性值作为第二个参数。setter 应为 public 且不返回值(即在 Java 中返回类型应为 void,在 Kotlin 中应为 Unit)。发送给 JS 的属性类型会根据 setter 的值参数类型自动确定。目前支持的值类型(在 Java 中)有:boolean、int、float、double、String、Boolean、Integer、ReadableArray、ReadableMap。Kotlin 中对应类型为 Boolean、Int、Float、Double、String、ReadableArray、ReadableMap。

@ReactProp 注解有一个必需参数 name,类型为 String。分配给与 setter 方法关联的 @ReactProp 注解的名称,会用于在 JS 端引用该属性。

除了 name 之外,@ReactProp 注解还可接受以下可选参数:defaultBoolean、defaultInt、defaultFloat。这些参数应为对应类型(Java 中分别为 boolean、int、float,Kotlin 中分别为 Boolean、Int、Float),并且当 setter 所引用的属性已从组件中移除时,会把提供的值传给 setter 方法。请注意,“默认”值只适用于基本类型;如果 setter 是某种复杂类型,则在对应属性被移除时,会传入 null 作为默认值。

对于使用 @ReactPropGroup 注解的方法,setter 声明要求与 @ReactProp 不同;更多信息请参阅 @ReactPropGroup 注解类文档。重要! 在 ReactJS 中,更新属性值会触发 setter 方法调用。请注意,我们更新组件的一种方式是移除之前已设置的属性。在这种情况下,setter 方法也会被调用,以通知视图管理器该属性已发生变化。此时会提供“默认”值(对于基本类型,可以使用 @ReactProp 注解的 defaultBoolean、defaultFloat 等参数指定“默认”值;对于复杂类型,setter 会在值为 null 时被调用)。

Java
@ReactProp(name = "src")
public void setSrc(ReactImageView view, @Nullable ReadableArray sources) {
view.setSource(sources);
}

@ReactProp(name = "borderRadius", defaultFloat = 0f)
public void setBorderRadius(ReactImageView view, float borderRadius) {
view.setBorderRadius(borderRadius);
}

@ReactProp(name = ViewProps.RESIZE_MODE)
public void setResizeMode(ReactImageView view, @Nullable String resizeMode) {
view.setScaleType(ImageResizeMode.toScaleType(resizeMode));
}

4. 注册 ViewManager​

最后一步是将 ViewManager 注册到应用中,这与 Native Modules 类似,通过应用的包成员函数 createViewManagers 来完成。

Java
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactImageManager(reactContext)
);
}

5. 实现 JavaScript 模块​

最后一步是创建 JavaScript 模块,用于定义 Java/Kotlin 与 JavaScript 之间供新视图用户使用的接口层。建议你在这个模块中为组件接口编写文档(例如使用 TypeScript、Flow 或最普通的注释)。

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

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

requireNativeComponent 函数接收原生视图的名称。请注意,如果你的组件需要做更复杂的事情(例如自定义事件处理),你应该将原生组件再包装到另一个 React 组件中。下面的 MyCustomView 示例展示了这一点。

事件​

到这里我们已经知道如何暴露可由 JS 自由控制的原生视图组件,但对于来自用户的事件,比如捏合缩放或平移,又该如何处理呢?当原生事件发生时,原生代码应向该 View 的 JavaScript 表示发出一个事件,而这两个视图通过 getId() 方法返回的值关联起来。

Java
class MyCustomView extends View {
...
public void onReceiveNativeEvent() {
WritableMap event = Arguments.createMap();
event.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext)getContext();
reactContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(getId(), "topChange", event);
}
}

要将 topChange 事件名称映射到 JavaScript 中的 onChange 回调属性,请通过重写 ViewManager 中的 getExportedCustomBubblingEventTypeConstants 方法来注册它:

Java
public class ReactImageManager extends SimpleViewManager<MyCustomView> {
...
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.builder().put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")
)
).build();
}
}

这个回调会接收原始事件,我们通常会在包装组件中对其进行处理,以提供更简单的 API:

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

const RCTMyCustomView = requireNativeComponent('RCTMyCustomView');

export default function MyCustomView(props: {
// ...
/**
* 当用户正在拖动地图时持续调用的回调。
*/
onChangeMessage: (message: string) => unknown;
}) {
const onChange = useCallback(
event => {
props.onChangeMessage?.(event.nativeEvent.message);
},
[props.onChangeMessage],
);

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

Android Fragment 示例集成​

为了将现有的 Native UI 元素集成到你的 React Native 应用中,你可能需要使用 Android Fragment,从而比直接从 ViewManager 返回一个 View 能更细粒度地控制你的原生组件。如果你想借助 生命周期方法(例如 onViewCreated、onPause、onResume)添加与视图绑定的自定义逻辑,就需要这样做。下面的步骤将展示如何实现:

1. 创建一个示例自定义视图​

首先,让我们创建一个继承自 FrameLayout 的 CustomView 类(这个视图的内容可以是你希望渲染的任何视图)

CustomView.java
// 替换为你的包名
package com.mypackage;

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

import androidx.annotation.NonNull;

public class CustomView extends FrameLayout {
public CustomView(@NonNull Context context) {
super(context);
// 设置内边距和背景色
this.setPadding(16,16,16,16);
this.setBackgroundColor(Color.parseColor("#5FD3F3"));

// 添加默认文本视图
TextView text = new TextView(context);
text.setText("Welcome to Android Fragments with React Native.");
this.addView(text);
}
}

2. 创建一个 Fragment​

MyFragment.java
// 替换为你的包名
package com.mypackage;

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

// 替换为你的视图的导入
import com.mypackage.CustomView;

public class MyFragment extends Fragment {
CustomView customView;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
super.onCreateView(inflater, parent, savedInstanceState);
customView = new CustomView(this.getContext());
return customView; // 这个 CustomView 可以是你想要渲染的任何视图
}

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 执行任何应该在 `onCreate` 方法中发生的逻辑,例如:
// customView.onCreate(savedInstanceState);
}

@Override
public void onPause() {
super.onPause();
// 执行任何应该在 `onPause` 方法中发生的逻辑
// 例如:customView.onPause();
}

@Override
public void onResume() {
super.onResume();
// 执行任何应该在 `onResume` 方法中发生的逻辑
// 例如:customView.onResume();
}

@Override
public void onDestroy() {
super.onDestroy();
// 执行任何应该在 `onDestroy` 方法中发生的逻辑
// 例如:customView.onDestroy();
}
}

3. 创建 ViewManager 子类​

MyViewManager.java
// 替换为你的包名
package com.mypackage;

import android.view.Choreographer;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ThemedReactContext;

import java.util.Map;

public class MyViewManager extends ViewGroupManager<FrameLayout> {

public static final String REACT_CLASS = "MyViewManager";
public final int COMMAND_CREATE = 1;
private int propWidth;
private int propHeight;

ReactApplicationContext reactContext;

public MyViewManager(ReactApplicationContext reactContext) {
this.reactContext = reactContext;
}

@Override
public String getName() {
return REACT_CLASS;
}

/**
* 返回一个稍后将承载 Fragment 的 FrameLayout
*/
@Override
public FrameLayout createViewInstance(ThemedReactContext reactContext) {
return new FrameLayout(reactContext);
}

/**
* 将 "create" 命令映射为一个整数
*/
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of("create", COMMAND_CREATE);
}

/**
* 处理 "create" 命令(由 JS 调用)并调用 createFragment 方法
*/
@Override
public void receiveCommand(
@NonNull FrameLayout root,
String commandId,
@Nullable ReadableArray args
) {
super.receiveCommand(root, commandId, args);
int reactNativeViewId = args.getInt(0);
int commandIdInt = Integer.parseInt(commandId);

switch (commandIdInt) {
case COMMAND_CREATE:
createFragment(root, reactNativeViewId);
break;
default: {}
}
}

@ReactPropGroup(names = {"width", "height"}, customType = "Style")
public void setStyle(FrameLayout view, int index, Integer value) {
if (index == 0) {
propWidth = value;
}

if (index == 1) {
propHeight = value;
}
}

/**
* 用自定义 Fragment 替换你的 React Native 视图
*/
public void createFragment(FrameLayout root, int reactNativeViewId) {
ViewGroup parentView = (ViewGroup) root.findViewById(reactNativeViewId);
setupLayout(parentView);

final MyFragment myFragment = new MyFragment();
FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
activity.getSupportFragmentManager()
.beginTransaction()
.replace(reactNativeViewId, myFragment, String.valueOf(reactNativeViewId))
.commit();
}

public void setupLayout(View view) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
manuallyLayoutChildren(view);
view.getViewTreeObserver().dispatchOnGlobalLayout();
Choreographer.getInstance().postFrameCallback(this);
}
});
}

/**
* 正确布局所有子视图
*/
public void manuallyLayoutChildren(View view) {
// propWidth 和 propHeight 来自 react-native props
int width = propWidth;
int height = propHeight;

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

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

4. 注册 ViewManager​

MyPackage.java
// 替换为你的包名
package com.mypackage;

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

import java.util.Arrays;
import java.util.List;

public class MyPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new MyViewManager(reactContext)
);
}

}

5. 注册 Package​

MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// 不能自动链接的 Packages 目前可以手动添加,例如:
// packages.add(new MyReactNativePackage());
packages.add(new MyAppPackage());
return packages;
}

6. 实现 JavaScript 模块​

I. 从自定义 View manager 开始:

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

export const MyViewManager =
requireNativeComponent('MyViewManager');

II. 然后实现调用 create 方法的自定义 View:

MyView.tsx
import {useEffect, useRef} from 'react';
import {
PixelRatio,
UIManager,
findNodeHandle,
} from 'react-native';

import {MyViewManager} from './my-view-manager';

const createFragment = viewId =>
UIManager.dispatchViewManagerCommand(
viewId,
// 我们正在调用 'create' 命令
UIManager.MyViewManager.Commands.create.toString(),
[viewId],
);

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

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

return (
<MyViewManager
style={{
// 将 dpi 转换为 px,提供期望的高度
height: PixelRatio.getPixelSizeForLayoutSize(200),
// 将 dpi 转换为 px,提供期望的宽度
width: PixelRatio.getPixelSizeForLayoutSize(200),
}}
ref={ref}
/>
);
};

如果你想使用 @ReactProp(或 @ReactPropGroup)注解暴露属性设置器,请参见上面的 ImageView 示例。