跳到主要内容

Android 原生 UI 组件

信息

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

最新的应用中有大量可直接使用的原生 UI widget——其中一些属于平台,另一些以第三方库的形式提供,还有更多可能正在你自己的项目组合中使用。React Native 已经封装了几个最关键的平台组件,例如 ScrollViewTextInput,但并不是全部,当然也不会包含你可能在之前的应用中自行编写的组件。幸运的是,我们可以封装这些现有组件,使其与你的 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,类型为 ReactImageViewReactImageView 是由该 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 中):booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap。Kotlin 中对应的类型为 BooleanIntFloatDoubleStringReadableArrayReadableMap

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

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

使用 @ReactPropGroup 注解的方法,其 setter 声明要求与 @ReactProp 不同;有关更多信息,请参阅 @ReactPropGroup 注解类文档。重要! 在 ReactJS 中更新属性值会导致调用 setter 方法。请注意,我们更新组件的方式之一,是移除之前设置的属性。在这种情况下,同样会调用 setter 方法,以通知 view manager 属性已发生变化。此时会提供“default”值(对于基本类型,可以使用 @ReactProp 注解的 defaultBooleandefaultFloat 等参数指定“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 更细粒度地控制原生组件。如果你希望借助生命周期方法添加与视图关联的自定义逻辑,例如 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 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 示例