Skip to main content

Android 原生 UI 组件

信息

Native Module 和 Native Components 是我们旧版架构使用的稳定技术。当新架构稳定后,它们将被弃用。新架构使用 Turbo Native 模块Fabric 原生组件 来实现类似的结果。

¥Native Module and Native Components are our stable technologies used by the legacy architecture. They will be deprecated in the future when the New Architecture will be stable. The New Architecture uses Turbo Native Module and Fabric Native Components to achieve similar results.

有大量原生 UI 小部件可供在最新应用中使用 - 其中一些是平台的一部分,其他一些可作为第三方库使用,还有更多可能会在你自己的产品组合中使用。React Native 已经封装了几个最关键的平台组件,例如 ScrollViewTextInput,但不是全部,当然也不是你自己为以前的应用编写的组件。幸运的是,我们可以封装这些现有组件,以便与你的 React Native 应用无缝集成。

¥There are tons of native UI widgets out there ready to be used in the latest apps - some of them are part of the platform, others are available as third-party libraries, and still more might be in use in your very own portfolio. React Native has several of the most critical platform components already wrapped, like ScrollView and TextInput, but not all of them, and certainly not ones you might have written yourself for a previous app. Fortunately, we can wrap up these existing components for seamless integration with your React Native application.

与原生模块指南一样,这也是一个更高级的指南,假设你对 Android SDK 编程有些熟悉。本指南将向你展示如何构建原生 UI 组件,引导你完成核心 React Native 库中可用的现有 ImageView 组件子集的实现。

¥Like the native module guide, this too is a more advanced guide that assumes you are somewhat familiar with Android SDK programming. This guide will show you how to build a native UI component, walking you through the implementation of a subset of the existing ImageView component available in the core React Native library.

信息

你还可以使用一个命令设置包含原生组件的本地库。阅读 本地库设置 指南了解更多详情。

¥You can also setup local library containing native component with one command. Read the guide to Local libraries setup for more details.

ImageView 示例

¥ImageView example

对于此示例,我们将逐步介绍允许在 JavaScript 中使用 ImageView 的实现要求。

¥For this example we are going to walk through the implementation requirements to allow the use of ImageViews in JavaScript.

原生视图是通过扩展 ViewManager 或更常见的 SimpleViewManager 创建和操作的。 在这种情况下,SimpleViewManager 很方便,因为它应用了常见属性,例如背景颜色、不透明度和 Flexbox 布局。

¥Native views are created and manipulated by extending ViewManager or more commonly SimpleViewManager . A SimpleViewManager is convenient in this case because it applies common properties such as background color, opacity, and Flexbox layout.

这些子类本质上是单例 - 桥只创建每个实例的一个实例。他们将原生视图发送到 NativeViewHierarchyManagerNativeViewHierarchyManager 将委托返回给他们以根据需要设置和更新视图的属性。ViewManagers 通常也是视图的代表,通过桥将事件发送回 JavaScript。

¥These subclasses are essentially singletons - only one instance of each is created by the bridge. They send native views to the NativeViewHierarchyManager, which delegates back to them to set and update the properties of the views as necessary. The ViewManagers are also typically the delegates for the views, sending events back to JavaScript via the bridge.

发送视图:

¥To send a view:

  1. 创建 ViewManager 子类。

    ¥Create the ViewManager subclass.

  2. 实现 createViewInstance 方法

    ¥Implement the createViewInstance method

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

    ¥Expose view property setters using @ReactProp (or @ReactPropGroup) annotation

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

    ¥Register the manager in createViewManagers of the applications package.

  5. 实现 JavaScript 模块

    ¥Implement the JavaScript module

1. 创建 ViewManager 子类

¥ Create the ViewManager subclass

在此示例中,我们创建视图管理器类 ReactImageManager,它扩展了类型 ReactImageViewSimpleViewManagerReactImageView 是管理器管理的对象类型,这将是自定义原生视图。getName 返回的名称用于引用 JavaScript 中的原生视图类型。

¥In this example we create view manager class ReactImageManager that extends SimpleViewManager of type ReactImageView. ReactImageView is the type of object managed by the manager, this will be the custom native view. Name returned by getName is used to reference the native view type from JavaScript.

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

¥ Implement method createViewInstance

视图是在 createViewInstance 方法中创建的,视图应该在默认状态下初始化自身,任何属性都将通过对 updateView. 的后续调用来设置

¥Views are created in the createViewInstance method, the view should initialize itself in its default state, any properties will be set via a follow up call to updateView.

  @Override
public ReactImageView createViewInstance(ThemedReactContext context) {
return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);
}

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

¥ Expose view property setters using @ReactProp (or @ReactPropGroup) annotation

要在 JavaScript 中反映的属性需要公开为用 @ReactProp(或 @ReactPropGroup)注释的 setter 方法。Setter 方法应该将要更新的视图(当前视图类型)作为第一个参数,将属性值作为第二个参数。Setter 应该是公共的并且不返回值(即返回类型应该是 Java 中的 void 或 Kotlin 中的 Unit)。发送到 JS 的属性类型是根据 setter 的值参数的类型自动确定的。当前支持以下类型的值(在 Java 中):booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap。Kotlin 中对应的类型为 BooleanIntFloatDoubleStringReadableArrayReadableMap

¥Properties that are to be reflected in JavaScript needs to be exposed as setter method annotated with @ReactProp (or @ReactPropGroup). Setter method should take view to be updated (of the current view type) as a first argument and property value as a second argument. Setter should be public and not return a value (i.e. return type should be void in Java or Unit in Kotlin). Property type sent to JS is determined automatically based on the type of value argument of the setter. The following type of values are currently supported (in Java): boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap. The corresponding types in Kotlin are Boolean, Int, Float, Double, String, ReadableArray, ReadableMap.

注释 @ReactProp 有一个类型为 String 的强制参数 name。分配给链接到 setter 方法的 @ReactProp 注释的名称用于引用 JS 端的属性。

¥Annotation @ReactProp has one obligatory argument name of type String. Name assigned to the @ReactProp annotation linked to the setter method is used to reference the property on JS side.

除了 name 之外,@ReactProp 注释可以采用以下可选参数:defaultBooleandefaultIntdefaultFloat。这些参数应具有相应的类型(相应地,Java 中为 booleanintfloat,Kotlin 中为 BooleanIntFloat),并且提供的值将传递给 setter 方法,以防 setter 引用的属性已被删除 来自组件。请注意,"default" 值仅针对原始类型提供,如果 setter 属于某种复杂类型,则 null 将作为默认值提供,以防相应的属性被删除。

¥Except from name, @ReactProp annotation may take following optional arguments: defaultBoolean, defaultInt, defaultFloat. Those arguments should be of the corresponding type (accordingly boolean, int, float in Java and Boolean, Int, Float in Kotlin) and the value provided will be passed to the setter method in case when the property that the setter is referencing has been removed from the component. Note that "default" values are only provided for primitive types, in case when setter is of some complex type, null will be provided as a default value in case when corresponding property gets removed.

使用 @ReactPropGroup 注解的方法的 Setter 声明要求与 @ReactProp 不同,请参阅 @ReactPropGroup 注解类文档以获取更多信息。重要的!在 ReactJS 中更新属性值将导致 setter 方法调用。请注意,我们更新组件的方法之一是删除之前设置的属性。在这种情况下,setter 方法也会被调用,以通知视图管理器属性已更改。在这种情况下,将提供 "default" 值(对于基本类型,可以使用 defaultBooleandefaultFloat@ReactProp 注释的参数来指定 "default" 值,对于复杂类型,将调用设置器并将值设置为 null)。

¥Setter declaration requirements for methods annotated with @ReactPropGroup are different than for @ReactProp, please refer to the @ReactPropGroup annotation class docs for more information about it. IMPORTANT! in ReactJS updating the property value will result in setter method call. Note that one of the ways we can update component is by removing properties that have been set before. In that case setter method will be called as well to notify view manager that property has changed. In that case "default" value will be provided (for primitive types "default" value can be specified using defaultBoolean, defaultFloat, etc. arguments of @ReactProp annotation, for complex types setter will be called with value set to null).

  @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

¥ Register the ViewManager

最后一步是将 ViewManager 注册到应用,这与 原生模块 类似,通过应用包成员函数 createViewManagers 进行。

¥The final step is to register the ViewManager to the application, this happens in a similar way to Native Modules, via the applications package member function createViewManagers.

  @Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactImageManager(reactContext)
);
}

5. 实现 JavaScript 模块

¥ Implement the JavaScript module

最后一步是创建 JavaScript 模块,为新视图的用户定义 Java/Kotlin 和 JavaScript 之间的接口层。建议你在此模块中记录组件接口(例如使用 TypeScript、Flow 或普通的旧注释)。

¥The very final step is to create the JavaScript module that defines the interface layer between Java/Kotlin and JavaScript for the users of your new view. It is recommended for you to document the component interface in this module (e.g. using TypeScript, Flow, or plain old comments).

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

/**

* Composes `View`.

* * - src: Array<{url: string}>

* - borderRadius: number

* - resizeMode: 'cover' | 'contain' | 'stretch'
*/
module.exports = requireNativeComponent('RCTImageView');

requireNativeComponent 函数采用原生视图的名称。请注意,如果你的组件需要执行更复杂的操作(例如自定义事件处理),你应该将原生组件封装在另一个 React 组件中。下面的 MyCustomView 示例对此进行了说明。

¥The requireNativeComponent function takes the name of the native view. Note that if your component needs to do anything more sophisticated (e.g. custom event handling), you should wrap the native component in another React component. This is illustrated in the MyCustomView example below.

事件

¥Events

现在我们知道如何公开可以从 JS 自由控制的原生视图组件,但是我们如何处理来自用户的事件,例如捏缩放或平移?当原生事件发生时,原生代码应向视图的 JavaScript 表示形式发出一个事件,并且两个视图与从 getId() 方法返回的值链接。

¥So now we know how to expose native view components that we can control freely from JS, but how do we deal with events from the user, like pinch-zooms or panning? When a native event occurs the native code should issue an event to the JavaScript representation of the View, and the two views are linked with the value returned from the getId() method.

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 方法来注册它:

¥To map the topChange event name to the onChange callback prop in JavaScript, register it by overriding the getExportedCustomBubblingEventTypeConstants method in your ViewManager:

public class ReactImageManager extends SimpleViewManager<MyCustomView> {
...
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.builder().put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")
)
).build();
}
}

此回调是通过原始事件调用的,我们通常在封装器组件中处理该事件以创建更简单的 API:

¥This callback is invoked with the raw event, which we typically process in the wrapper component to make a simpler API:

MyCustomView.tsx
class MyCustomView extends React.Component {
constructor(props) {
super(props);
this._onChange = this._onChange.bind(this);
}
_onChange(event) {
if (!this.props.onChangeMessage) {
return;
}
this.props.onChangeMessage(event.nativeEvent.message);
}
render() {
return <RCTMyCustomView {...this.props} onChange={this._onChange} />;
}
}
MyCustomView.propTypes = {
/**

* Callback that is called continuously when the user is dragging the map.
*/
onChangeMessage: PropTypes.func,
...
};

const RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`);

与 Android Fragment 示例集成

¥Integration with an Android Fragment example

为了将现有的原生 UI 元素集成到你的 React Native 应用中,你可能需要使用 Android Fragments 来对原生组件进行更精细的控制,而不是从 ViewManager 返回 View。如果你想在 生命周期方法 的帮助下添加与视图绑定的自定义逻辑(例如 onViewCreatedonPauseonResume),则需要此选项。以下步骤将向你展示如何操作:

¥In order to integrate existing Native UI elements to your React Native app, you might need to use Android Fragments to give you a more granular control over your native component than returning a View from your ViewManager. You will need this if you want to add custom logic that is tied to your view with the help of lifecycle methods, such as onViewCreated, onPause, onResume. The following steps will show you how to do it:

1. 创建示例自定义视图

¥ Create an example custom view

首先,让我们创建一个扩展 FrameLayoutCustomView 类(该视图的内容可以是你想要渲染的任何视图)

¥First, let's create a CustomView class which extends FrameLayout (the content of this view can be any view that you'd like to render)

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

¥ Create a 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 子类

¥ Create the ViewManager subclass

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

¥ Register the 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

¥ Register the 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 模块

¥ Implement the JavaScript module

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

¥I. Start with custom View manager:

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

export const MyViewManager =
requireNativeComponent('MyViewManager');

II.然后调用 create 方法实现自定义 View:

¥II. Then implement custom View calling the create method:

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

¥If you want to expose property setters using @ReactProp (or @ReactPropGroup) annotation see the ImageView example above.