跳到主要内容

Android 原生 UI 组件

信息

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

现在有大量的原生 UI 控件可以用在最新的应用中——它们有的属于平台自带组件,有的作为第三方库提供,还有的可能就是你自己之前某个项目里用过的。React Native 已经封装了几个最关键的平台组件,比如 ScrollViewTextInput,但并不是所有的原生组件都内置,也不包括你之前写过的自定义组件。幸运的是,我们可以将这些现有组件包装起来,实现与 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,它继承自泛型类型为 ReactImageViewSimpleViewManagerReactImageView 是该管理器管理的对象类型,即自定义的原生视图。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)。属性类型会根据 setter 的参数类型自动确定。Java 目前支持的类型包括:booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap。对应 Kotlin 类型为:BooleanIntFloatDoubleStringReadableArrayReadableMap

@ReactProp 注解必须带一个类型为 String 的必填参数 name,它指定该 setter 方法对应 JS 端的属性名。

除了 name 以外,@ReactProp 还可以带以下可选参数:defaultBooleandefaultIntdefaultFloat。这几个参数应该分别是对应类型(Java 为 booleanintfloat;Kotlin 为 BooleanIntFloat),且在对应属性被移除时,setter 会被传入这些默认值。注意,默认值只适用于基本类型,若 setter 参数是复杂类型,属性被移除时该属性值会传 null

@ReactPropGroup 注解的 setter 方法声明要求与 @ReactProp 不同,详情请参考 @ReactPropGroup 注解类文档。重要! ReactJS 中更新属性值会导致调用对应的 setter 方法。其中一种更新组件的方式是移除已设置的属性,在这种情况下,也会调用 setter,传入默认值(基本类型默认值由 @ReactProp 的相关参数指定,复杂类型传入 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,方式和原生模块一样,通过应用包的成员函数 createViewManagers 完成。

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

5. 实现 JavaScript 模块

最后就是创建 JavaScript 模块,用于定义 Java/Kotlin 与 JavaScript 之间的接口层。建议使用 TypeScript、Flow 或 JavaScript 注释对组件接口进行文档说明。

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 中自由控制的原生视图组件,但如何处理用户的事件呢,比如双指缩放或平移?当原生事件发生时,原生代码应该向 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();
}
}

这个回调会携带原始事件,我们通常在包装组件中处理它,简化接口:

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 集成示例

为了将已有的原生 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.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 Fragment。");
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.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);
}

/**
* 处理 JS 调用的 "create" 命令,调用 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 的属性
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.add(new MyReactNativePackage());
packages.add(new MyAppPackage());
return packages;
}

6. 实现 JavaScript 模块

I. 先创建自定义 ViewManager:

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

export const MyViewManager =
requireNativeComponent('MyViewManager');

II. 然后实现调用 create 命令的自定义 View:

MyView.tsx
import React, {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)注解暴露属性 setter,请参考上面的ImageView 示例