跳到主要内容

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) 注解暴露视图属性设置器
  4. 在应用程序包的 createViewManagers 中注册管理器。
  5. 实现 JavaScript 模块

1. 创建 ViewManager 子类

在此示例中,我们创建了扩展 ReactImageView 类型 SimpleViewManager 的视图管理器类 ReactImageManagerReactImageView 是由管理器管理的对象类型,这将是自定义原生视图。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)。发送到 JS 的属性类型根据设置器的值参数类型自动确定。目前支持以下类型的值(在 Java 中):booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap。Kotlin 中对应的类型是 BooleanIntFloatDoubleStringReadableArrayReadableMap

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

除了 name@ReactProp 注解还可以接受以下可选参数:defaultBooleandefaultIntdefaultFloat。这些参数应为相应类型(在 Java 中分别为 booleanintfloat,在 Kotlin 中为 BooleanIntFloat),如果设置器引用的属性已从组件中删除,则提供的值将传递给设置器方法。请注意,“默认”值仅提供给原始类型,如果设置器是某种复杂类型,则在相应属性被删除时将提供 null 作为默认值。

使用 @ReactPropGroup 注解的方法的设置器声明要求与 @ReactProp 不同,有关更多信息,请参阅 @ReactPropGroup 注解类的文档。重要提示! 在 ReactJS 中更新属性值将导致调用设置器方法。请注意,更新组件的一种方法是删除之前已设置的属性。在这种情况下,也会调用设置器方法以通知视图管理器属性已更改。在这种情况下,将提供“默认”值(对于原始类型,“默认”值可以使用 @ReactProp 注解的 defaultBooleandefaultFloat 等参数指定,对于复杂类型,设置器将以 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 回调 prop,请通过覆盖 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={props.onChange} />;
}

与 Android Fragment 示例的集成

为了将现有的原生 UI 元素集成到您的 React Native 应用程序中,您可能需要使用 Android Fragment,以便比从 ViewManager 返回 View 更细致地控制您的原生组件。如果您想借助 生命周期方法(例如 onViewCreatedonPauseonResume)添加与视图绑定的自定义逻辑,您将需要这样做。以下步骤将向您展示如何操作

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. 从自定义 View 管理器开始

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

export const MyViewManager =
requireNativeComponent('MyViewManager');

II. 然后实现自定义 View,调用 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 示例