跳至主内容
版本:0.79

Android 原生 UI 组件

非官方测试版翻译

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

非官方测试版翻译

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

信息

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

当前有大量现成的原生 UI 组件可供最新应用使用——其中部分属于平台内置组件,部分来自第三方库,还有些可能就存在于你自己的组件库中。React Native 已封装了若干关键平台组件(如 ScrollView 和 TextInput),但并未覆盖全部组件,尤其不包括你可能为旧应用编写的自定义组件。幸运的是,我们可以将这些现有组件封装起来,实现与 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,类型为 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)注解公开属性设置器​

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

注解 @ReactProp 必须包含一个 String 类型的 name 参数。分配给 @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 注册到应用程序,注册方式与 原生模块 类似,通过应用程序包的 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. 创建示例自定义视图​

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

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 示例。