跳到主要内容

Android 原生 UI 组件

信息

Native Module 和 Native Components 是 legacy architecture 中使用的稳定技术。 当 New Architecture 稳定后,它们将在未来被弃用。New Architecture 使用 Turbo Native Module 和 Fabric Native Components 来实现类似的结果。

最新的应用中有大量可直接使用的原生 UI widget——其中一些属于平台,另一些以第三方库的形式提供,还有更多可能正在你自己的项目组合中使用。React Native 已经封装了几个最关键的平台组件,例如 ScrollView 和 TextInput,但并不是全部,当然也不会包含你可能在之前的应用中自行编写的组件。幸运的是,我们可以封装这些现有组件,使其与你的 React Native 应用无缝集成。

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

信息

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

ImageView 示例​

在本示例中,我们将逐步讲解如何实现相关要求,以便在 JavaScript 中使用 ImageView。

原生视图通过扩展 ViewManager,或者更常见地扩展 SimpleViewManager 来创建和操作。SimpleViewManager 在此处很方便,因为它会应用背景颜色、不透明度和 Flexbox 布局等常见属性。

这些子类本质上是单例——bridge 只会创建每个类的一个实例。它们将原生视图发送给 NativeViewHierarchyManager,后者会在必要时回调它们,以设置和更新视图的属性。ViewManagers 通常也是视图的代理,通过 bridge 将事件发送回 JavaScript。

要发送一个视图:

  1. 创建 ViewManager 子类
  2. 实现 createViewInstance 方法
  3. 使用 @ReactProp(或 @ReactPropGroup)注解暴露视图属性 setter
  4. 在应用的 package 的 createViewManagers 中注册 manager
  5. 实现 JavaScript 模块

1. 创建 ViewManager 子类​

在本示例中,我们创建了一个名为 ReactImageManager 的视图 manager 类,它扩展了 SimpleViewManager,类型为 ReactImageView。ReactImageView 是由该 manager 管理的对象类型,也就是自定义原生视图。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 中):boolean、int、float、double、String、Boolean、Integer、ReadableArray、ReadableMap。Kotlin 中对应的类型为 Boolean、Int、Float、Double、String、ReadableArray、ReadableMap。

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

除 name 外,@ReactProp 注解还可以接受以下可选参数:defaultBoolean、defaultInt、defaultFloat。这些参数应为对应的类型(在 Java 中分别为 boolean、int、float,在 Kotlin 中分别为 Boolean、Int、Float),当 setter 所引用的属性从组件中移除时,所提供的值将传递给 setter 方法。请注意,“default”值仅适用于基本类型;当 setter 使用某种复杂类型时,如果对应属性被移除,将提供 null 作为默认值。

使用 @ReactPropGroup 注解的方法,其 setter 声明要求与 @ReactProp 不同;有关更多信息,请参阅 @ReactPropGroup 注解类文档。重要! 在 ReactJS 中更新属性值会导致调用 setter 方法。请注意,我们更新组件的方式之一,是移除之前设置的属性。在这种情况下,同样会调用 setter 方法,以通知 view manager 属性已发生变化。此时会提供“default”值(对于基本类型,可以使用 @ReactProp 注解的 defaultBoolean、defaultFloat 等参数指定“default”值;对于复杂类型,调用 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,这与原生模块类似,通过应用 package 的成员函数 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 自由控制的原生视图组件,但应该如何处理用户事件,例如捏合缩放或平移?当原生事件发生时,原生代码应向 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 回调 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={onChange} />;
}

与 Android Fragment 集成示例​

为了将现有的 Native UI 元素集成到 React Native 应用中,你可能需要使用 Android Fragments,以便比从 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. 从自定义 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,
// 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)注解暴露属性 setter,请参阅上面的 ImageView 示例。