跳到主要内容
版本:0.82

Android 原生 UI 组件

信息

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

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

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

信息

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

ImageView 示例

在这个示例中,我们将逐步讲解如何实现允许在 JavaScript 中使用 ImageView 的要求。

原生视图通过扩展 ViewManager,或者更常见地通过扩展 SimpleViewManager 来创建和操作。对于这种情况,SimpleViewManager 很方便,因为它会应用诸如背景色、透明度和 Flexbox 布局等通用属性。

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

要发送一个视图:

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

1. 创建 ViewManager 子类

在这个示例中,我们创建一个视图管理器类 ReactImageManager,它继承 ReactImageView 类型的 SimpleViewManagerReactImageView 是由该管理器管理的对象类型,它将是自定义原生视图。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 中)有:booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap。Kotlin 中对应的类型是 BooleanIntFloatDoubleStringReadableArrayReadableMap

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

除了 name 之外,@ReactProp 注解还可以接受以下可选参数:defaultBooleandefaultIntdefaultFloat。这些参数应为对应类型(Java 中分别是 booleanintfloat,Kotlin 中分别是 BooleanIntFloat),并且当 setter 所引用的属性已从组件中移除时,会把提供的值传给 setter 方法。注意,“默认”值只适用于原始类型;如果 setter 是某种复杂类型,那么当对应属性被移除时,会提供 null 作为默认值。

对于带有 @ReactPropGroup 注解的方法,setter 声明要求与 @ReactProp 不同,请参考 @ReactPropGroup 注解类文档了解更多信息。重要! 在 ReactJS 中,更新属性值会触发 setter 方法调用。注意,我们更新组件的一种方式是移除之前已设置的属性。在这种情况下,setter 方法也会被调用,以通知视图管理器该属性已发生变化。此时会提供“默认”值(对于原始类型,可以通过 @ReactProp 注解的 defaultBooleandefaultFloat 等参数指定“默认”值;对于复杂类型,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,对原生组件拥有更细粒度的控制。如果你希望借助 生命周期方法(例如 onViewCreatedonPauseonResume)为视图添加自定义逻辑,就需要这样做。下面的步骤将展示如何实现:

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

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

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("欢迎使用 React Native 与 Android Fragments。");
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;
}

/**
* 返回一个 FrameLayout,后续将用于承载 Fragment
*/
@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 示例