跳到主要内容
新架构实战课 实操 + 基建 + 原理全维度包揽,抢先掌握 React Native 新架构精髓 立即查看 >Version: Next

Android 原生UI组件

提示

原生模块和原生组件是我们传统架构中使用的稳定技术。 当新架构稳定后,它们将被弃用。新架构使用TurboModuleFabric 组件来实现类似的功能。

在如今的 App 中,已经有成千上万的原生 UI 部件了——其中的一些是平台的一部分,另一些可能来自于一些第三方库,而且可能你自己还收藏了很多。React Native 已经封装了大部分最常见的组件,譬如ScrollViewTextInput,但不可能封装全部组件。而且,说不定你曾经为自己以前的 App 还封装过一些组件,React Native 肯定没法包含它们。幸运的是,在 React Naitve 应用程序中封装和植入已有的组件非常简单。

和原生模块向导一样,本向导也是一个相对高级的向导,我们假设你已经对 Android 编程颇有经验。本向导会引导你如何构建一个原生 UI 组件,带领你了解 React Native 核心库中ImageView组件的具体实现。

info

您还可以通过一个命令来配置生成包含原生组件的本地库模板。阅读本地库设置指南以获取更多详细信息。

ImageView 示例

在这个例子里,我们来看看为了让 JavaScript 中可以使用 ImageView,需要做哪些准备工作。

原生视图需要被一个ViewManager的派生类(或者更常见的,SimpleViewManager的派生类)创建和管理。一个SimpleViewManager可以用于这个场景,是因为它能够包含更多公共的属性,譬如背景颜色、透明度、Flexbox 布局等等。

这些子类本质上都是单例——React Native 只会为每个管理器创建一个实例。它们创建原生的视图并提供给NativeViewHierarchyManager,NativeViewHierarchyManager 则会反过来委托它们在需要的时候去设置和更新视图的属性。ViewManager还会代理视图的所有委托,并给 JavaScript 发回对应的事件。

提供原生视图很简单:

  1. 创建一个 ViewManager 的子类。
  2. 实现createViewInstance方法。
  3. 导出视图的属性设置器:使用@ReactProp(或@ReactPropGroup)注解。
  4. 把这个视图管理类注册到应用程序包的createViewManagers里。
  5. 实现 JavaScript 模块。

1. 创建ViewManager的子类

在这个例子里我们创建一个视图管理类ReactImageManager,它继承自SimpleViewManager<ReactImageView>ReactImageView是这个视图管理类所管理的对象类型,也就是我们自定义的原生视图。getName方法返回的名字会用于在 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

视图在createViewInstance中创建,且应当把自己初始化为默认的状态。所有属性的设置都通过后续的updateView来进行。

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

3. 通过@ReactProp(或@ReactPropGroup)注解来导出属性的设置方法。

要导出给 JavaScript 使用的属性,需要申明带有@ReactProp(或@ReactPropGroup)注解的设置方法。方法的第一个参数是要修改属性的视图实例,第二个参数是要设置的属性值。方法的返回值类型必须为void,在 Kotlin 中是 Unit,而且访问控制必须被声明为public。JavaScript 所得知的属性类型会由该方法第二个参数的类型来自动决定。支持的类型有:boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap。Kotlin 中对应的则是 Boolean, Int, Float, Double, String, ReadableArray, ReadableMap.

@ReactProp注解必须包含一个字符串类型的参数name。这个参数指定了对应属性在 JavaScript 端的名字。

除了name@ReactProp注解还接受这些可选的参数:defaultBoolean, defaultInt, defaultFloat。这些参数必须是对应的基础类型的值(也就是对应 Java 中的 boolean, int, float, 或是 Kotlin 中的 Boolean, Int, Float),这些值会被传递给 setter 方法,以免 JavaScript 端某些情况下在组件中移除了对应的属性。注意这个"默认"值只对基本类型生效,对于其他的类型而言,当对应的属性删除时,null会作为默认值提供给方法。

使用@ReactPropGroup来注解的设置方法和@ReactProp不同。请参见@ReactPropGroup注解类源代码中的文档来获取更多详情。

重要! 在 ReactJS 里,修改一个属性会引发一次对设置方法的调用。有一种修改情况是,移除掉之前设置的属性。在这种情况下设置方法也一样会被调用,并且“默认”值会被作为参数提供(对于基础类型来说可以通过defaultBooleandefaultFloat@ReactProp的属性提供,而对于复杂类型来说参数则会设置为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

最后一步就是把视图控制器注册到应用中。这和原生模块的注册方法类似,唯一的区别是我们把它放到createViewManagers方法的返回值里。

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

完成上面这些代码后,请一定记得要重新编译!(运行yarn android命令)

5. 实现对应的 JavaScript 模块

整个过程的最后一步就是创建 JavaScript 模块并且定义 Java 和 JavaScript 之间的接口层。我们建议你使用 TypeScript 来规范定义接口的具体结构,或者至少用注释说明清楚(老版本的 RN 使用propTypes来规范接口定义,这一做法已不再支持)。

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()而关联在一起。

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方法,并在其中进行注册:

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

这个回调会传递一个原生事件对象,一般来说我们会在封装组件里进行处理以便外部使用:

MyCustomView.tsx
import React, { useCallback } from 'react';

const MyCustomView = ({ onChangeMessage, ...props }) => {
const onChange = useCallback((event) => {
if (!onChangeMessage) {
return;
}
onChangeMessage(event.nativeEvent.message);
}, [onChangeMessage]);

return (
<RCTMyCustomView
{...props}
onChange={onChange}
/>
);
};


const RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`);

与 Android Fragment 的整合实例

为了将现有的原生 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. MyViewManager.tsx

MyViewManager.tsx
import { requireNativeComponent } from 'react-native';
export const MyViewManager =
requireNativeComponent('MyViewManager');

II. MyView.tsx 调用 create 方法

MyView.tsx
import React, { useEffect, useRef } from 'react';
import { 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 = ({ style }) => {
const ref = useRef(null);

useEffect(() => {
const viewId = findNodeHandle(ref.current);
createFragment(viewId!);
}, []);

return (
<MyViewManager
style={{
...(style || {}),
height: style && style.height !== undefined ? style.height || '100%',
width: style && style.width !== undefined ? style.width || '100%'
}}
ref={ref}
/>
);
};

如果您想使用公开属性设置器 @ReactProp (or @ReactPropGroup) 详见上面的 ImageView 示例