跳至主内容
版本:0.77

Android 原生 UI 组件

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

信息

原生模块(Native Module)和原生组件(Native Components)是旧架构中使用的稳定技术。它们将在新架构稳定后被弃用。新架构采用 Turbo 原生模块Fabric 原生组件 来实现类似功能。

当前有大量现成的原生 UI 组件可供最新应用使用——其中部分属于平台内置组件,部分来自第三方库,还有些可能就存在于你自己的组件库中。React Native 已封装了若干关键平台组件(如 ScrollViewTextInput),但并未覆盖全部组件,尤其不包括你可能为旧应用编写的自定义组件。幸运的是,我们可以将这些现有组件封装起来,实现与 React Native 应用的无缝集成。

与原生模块指南类似,本文也是面向已具备 Android SDK 编程基础的中高级开发者指南。本文将指导你构建原生 UI 组件,通过实现 React Native 核心库中现有 ImageView 组件的部分功能进行演示。

信息

你还可以通过单条命令设置包含原生组件的本地库。更多细节请参阅本地库设置指南

ImageView 示例

本示例将逐步讲解如何在 JavaScript 中使用 ImageView 所需的实现步骤。

原生视图通过扩展 ViewManager 或更常用的 SimpleViewManager 来创建和操作。SimpleViewManager 在此场景中非常实用,因为它能自动处理常见属性(如背景色、透明度和 Flexbox 布局)。

这些子类本质上是单例——桥接层(bridge)仅会创建每个类的单个实例。它们将原生视图发送至 NativeViewHierarchyManager,该管理器会委托它们设置和更新视图属性。ViewManagers 通常也作为视图的代理,通过桥接层将事件回传至 JavaScript。

创建视图的流程:

  1. 创建 ViewManager 子类

  2. 实现 createViewInstance 方法

  3. 使用 @ReactProp(或 @ReactPropGroup)注解公开视图属性设置器

  4. 在应用程序包的 createViewManagers 中注册管理器

  5. 实现 JavaScript 模块

1. 创建 ViewManager 子类

本示例中,我们创建视图管理器类 ReactImageManager,它继承自 SimpleViewManager,类型为 ReactImageViewReactImageView 是该管理器操作的对象类型,即自定义原生视图。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)注解公开属性设置器

需要在 JavaScript 中映射的属性必须通过 @ReactProp(或 @ReactPropGroup)注解的公共设置器方法公开。设置器方法的第一个参数应为待更新的视图对象(当前视图类型),第二个参数为属性值。该方法必须为 public 且无返回值(Java 中返回类型为 void,Kotlin 中为 Unit)。属性类型根据设置器参数值类型自动推断,目前支持以下 Java 类型:booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap;Kotlin 对应类型为:BooleanIntFloatDoubleStringReadableArrayReadableMap

注解 @ReactProp 必须包含一个 String 类型的 name 参数。分配给 @ReactProp 注解(与设置器方法相关联)的名称用于在 JS 端引用对应属性。

除了 name 之外,@ReactProp 注解还可接受以下可选参数:defaultBooleandefaultIntdefaultFloat。这些参数必须是对应类型(Java 中分别为 booleanintfloat;Kotlin 中为 BooleanIntFloat)。当 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 注册到应用程序,注册方式与 原生模块 类似,通过应用程序包的 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';

/**
* Composes `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();
}
}

该回调接收原始事件,通常在封装组件中处理以简化 API:

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

const RCTMyCustomView = requireNativeComponent('RCTMyCustomView');

export default function MyCustomView(props: {
// ...
/**
* Callback that is called continuously when the user is dragging the map.
*/
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)。若需要借助 生命周期方法(如 onViewCreated/onPause/onResume)添加与视图相关的自定义逻辑,可按以下步骤操作:

1. 创建示例自定义视图

首先创建继承 FrameLayoutCustomView 类(视图内容可以是任何需要渲染的元素)

CustomView.java
// replace with your package
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);
// set padding and background color
this.setPadding(16,16,16,16);
this.setBackgroundColor(Color.parseColor("#5FD3F3"));

// add default text view
TextView text = new TextView(context);
text.setText("Welcome to Android Fragments with React Native.");
this.addView(text);
}
}

2. 创建 Fragment

MyFragment.java
// 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;

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; // this CustomView could be any view that you want to render
}

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

@Override
public void onPause() {
super.onPause();
// do any logic that should happen in an `onPause` method
// e.g.: customView.onPause();
}

@Override
public void onResume() {
super.onResume();
// do any logic that should happen in an `onResume` method
// e.g.: customView.onResume();
}

@Override
public void onDestroy() {
super.onDestroy();
// do any logic that should happen in an `onDestroy` method
// e.g.: customView.onDestroy();
}
}

3. 创建 ViewManager 子类

MyViewManager.java
// 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.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;
}

/**
* Return a FrameLayout which will later hold the Fragment
*/
@Override
public FrameLayout createViewInstance(ThemedReactContext reactContext) {
return new FrameLayout(reactContext);
}

/**
* Map the "create" command to an integer
*/
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of("create", COMMAND_CREATE);
}

/**
* Handle "create" command (called from JS) and call createFragment method
*/
@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;
}
}

/**
* Replace your React Native view with a custom fragment
*/
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);
}
});
}

/**
* Layout all children properly
*/
public void manuallyLayoutChildren(View view) {
// propWidth and propHeight coming from 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
// replace with your package
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 that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new MyAppPackage());
return packages;
}

6. 实现 JavaScript 模块

I. 从自定义视图管理器开始:

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

export const MyViewManager =
requireNativeComponent('MyViewManager');

II. 然后通过调用 create 方法实现自定义视图:

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,
// we are calling the 'create' command
UIManager.MyViewManager.Commands.create.toString(),
[viewId],
);

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

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

return (
<MyViewManager
style={{
// converts dpi to px, provide desired height
height: PixelRatio.getPixelSizeForLayoutSize(200),
// converts dpi to px, provide desired width
width: PixelRatio.getPixelSizeForLayoutSize(200),
}}
ref={ref}
/>
);
};

如需使用 @ReactProp(或 @ReactPropGroup)注解公开属性设置器,请参考上文的 ImageView 示例