Android 原生 UI 组件
原生模块和原生组件是我们传统架构中使用的稳定技术。当新架构稳定后,它们将在未来被弃用。新架构使用Turbo 原生模块 和 Fabric 原生组件 来实现类似的结果。
现在有大量的原生 UI 控件可供最新的应用使用 - 其中一些是平台自带的,另一些可以通过第三方库获得,还有一些可能在您自己的项目中使用。React Native 已经封装了一些最重要的平台组件,例如 ScrollView
和 TextInput
,但并非所有组件,当然也不包括您可能为以前的应用自己编写的组件。幸运的是,我们可以将这些现有的组件封装起来,以便与您的 React Native 应用无缝集成。
与原生模块指南类似,这同样是一个更高级的指南,假设您已经对 Android SDK 编程有所了解。本指南将向您展示如何构建原生 UI 组件,并引导您完成 React Native 核心库中现有 ImageView
组件子集的实现。
您还可以使用一条命令设置包含原生组件的本地库。阅读有关本地库设置的指南以了解更多详细信息。
ImageView 示例
在本示例中,我们将逐步介绍允许在 JavaScript 中使用 ImageView 的实现要求。
原生视图通过扩展 ViewManager
或更常见的 SimpleViewManager
来创建和操作。在本例中,SimpleViewManager
很方便,因为它应用了常见的属性,例如背景颜色、不透明度和 Flexbox 布局。
这些子类本质上是单例 - 桥接器只会创建每个子类的单个实例。它们将原生视图发送到 NativeViewHierarchyManager
,后者会委托回它们来根据需要设置和更新视图的属性。ViewManagers
通常也是视图的委托,通过桥接器将事件发送回 JavaScript。
要发送视图
- 创建 ViewManager 子类。
- 实现
createViewInstance
方法 - 使用
@ReactProp
(或@ReactPropGroup
)注解公开视图属性设置器 - 在应用程序包的
createViewManagers
中注册管理器。 - 实现 JavaScript 模块
1. 创建 ViewManager 子类
在本例中,我们创建了扩展 SimpleViewManager
的视图管理器类 ReactImageManager
,其类型为 ReactImageView
。ReactImageView
是管理器管理的对象类型,这将是自定义原生视图。getName
返回的名称用于从 JavaScript 中引用原生视图类型。
- Java
- Kotlin
class ReactImageManager(
private val callerContext: ReactApplicationContext
) : SimpleViewManager<ReactImageView>() {
override fun getName() = REACT_CLASS
companion object {
const val REACT_CLASS = "RCTImageView"
}
}
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
- Kotlin
override fun createViewInstance(context: ThemedReactContext) =
ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, callerContext)
@Override
public ReactImageView createViewInstance(ThemedReactContext context) {
return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);
}
3. 使用 @ReactProp
(或 @ReactPropGroup
)注解公开视图属性设置器
需要在 JavaScript 中反映的属性需要公开为使用 @ReactProp
(或 @ReactPropGroup
)注解的设置器方法。设置器方法应将要更新的视图(当前视图类型)作为第一个参数,并将属性值作为第二个参数。设置器应为公共的,并且不返回值(即 Java 中的返回类型应为 void
,Kotlin 中的返回类型应为 Unit
)。发送到 JS 的属性类型会根据设置器值参数的类型自动确定。目前支持以下类型的值(在 Java 中):boolean
、int
、float
、double
、String
、Boolean
、Integer
、ReadableArray
、ReadableMap
。Kotlin 中的对应类型为 Boolean
、Int
、Float
、Double
、String
、ReadableArray
、ReadableMap
。
注解 @ReactProp
具有一个强制参数 name
,类型为 String
。分配给与设置器方法链接的 @ReactProp
注解的名称用于在 JS 端引用该属性。
除了 name
之外,@ReactProp
注解还可以使用以下可选参数:defaultBoolean
、defaultInt
、defaultFloat
。这些参数应为对应类型(相应地,Java 中为 boolean
、int
、float
,Kotlin 中为 Boolean
、Int
、Float
),并且提供的 value 将在组件中删除了设置器引用的属性时传递给设置器方法。请注意,“默认”值仅提供给基本类型,如果设置器为某种复杂类型,则在相应的属性被删除时,将提供 null
作为默认值。
使用 @ReactPropGroup
注解的方法的设置器声明要求与 @ReactProp
不同,有关更多信息,请参阅 @ReactPropGroup
注解类的文档。**重要!**在 ReactJS 中,更新属性值将导致调用设置器方法。请注意,我们更新组件的方式之一是删除之前设置的属性。在这种情况下,也会调用设置器方法以通知视图管理器属性已更改。在这种情况下,将提供“默认”值(对于基本类型,“默认”值可以使用 @ReactProp
注解的 defaultBoolean
、defaultFloat
等参数指定,对于复杂类型,将使用设置为 null
的值调用设置器)。
- Java
- Kotlin
@ReactProp(name = "src")
fun setSrc(view: ReactImageView, sources: ReadableArray?) {
view.setSource(sources)
}
@ReactProp(name = "borderRadius", defaultFloat = 0f)
override fun setBorderRadius(view: ReactImageView, borderRadius: Float) {
view.setBorderRadius(borderRadius)
}
@ReactProp(name = ViewProps.RESIZE_MODE)
fun setResizeMode(view: ReactImageView, resizeMode: String?) {
view.setScaleType(ImageResizeMode.toScaleType(resizeMode))
}
@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
- Kotlin
override fun createViewManagers(
reactContext: ReactApplicationContext
) = listOf(ReactImageManager(reactContext))
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactImageManager(reactContext)
);
}
5. 实现 JavaScript 模块
最后一步是创建 JavaScript 模块,该模块定义了 Java/Kotlin 和 JavaScript 之间的接口层,供新视图的用户使用。建议您在此模块中记录组件接口(例如,使用 TypeScript、Flow 或普通注释)。
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
- Kotlin
class MyCustomView(context: Context) : View(context) {
...
fun onReceiveNativeEvent() {
val event = Arguments.createMap().apply {
putString("message", "MyMessage")
}
val reactContext = context as ReactContext
reactContext
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, "topChange", event)
}
}
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
- Kotlin
class ReactImageManager : SimpleViewManager<MyCustomView>() {
...
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return mapOf(
"topChange" to mapOf(
"phasedRegistrationNames" to mapOf(
"bubbled" to "onChange"
)
)
)
}
}
public class ReactImageManager extends SimpleViewManager<MyCustomView> {
...
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.builder().put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")
)
).build();
}
}
此回调将使用原始事件调用,我们通常在包装组件中处理此事件以创建更简单的 API
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
更精细的控制权。如果您想添加与视图相关的自定义逻辑,则需要这样做,借助于生命周期方法,例如 onViewCreated
、onPause
、onResume
。以下步骤将向您展示如何操作
1. 创建一个自定义视图示例
首先,让我们创建一个扩展 FrameLayout
的 CustomView
类(此视图的内容可以是您想要呈现的任何视图)
- Java
- Kotlin
// replace with your package
package com.mypackage
import android.content.Context
import android.graphics.Color
import android.widget.FrameLayout
import android.widget.TextView
class CustomView(context: Context) : FrameLayout(context) {
init {
// set padding and background color
setPadding(16,16,16,16)
setBackgroundColor(Color.parseColor("#5FD3F3"))
// add default text view
addView(TextView(context).apply {
text = "Welcome to Android Fragments with React Native."
})
}
}
// 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
- Java
- Kotlin
// 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
class MyFragment : Fragment() {
private lateinit var customView: CustomView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
customView = CustomView(requireNotNull(context))
return customView // this CustomView could be any view that you want to render
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// do any logic that should happen in an `onCreate` method, e.g:
// customView.onCreate(savedInstanceState);
}
override fun onPause() {
super.onPause()
// do any logic that should happen in an `onPause` method
// e.g.: customView.onPause();
}
override fun onResume() {
super.onResume()
// do any logic that should happen in an `onResume` method
// e.g.: customView.onResume();
}
override fun onDestroy() {
super.onDestroy()
// do any logic that should happen in an `onDestroy` method
// e.g.: customView.onDestroy();
}
}
// 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
子类
- Java
- Kotlin
// 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.fragment.app.FragmentActivity
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactPropGroup
class MyViewManager(
private val reactContext: ReactApplicationContext
) : ViewGroupManager<FrameLayout>() {
private var propWidth: Int? = null
private var propHeight: Int? = null
override fun getName() = REACT_CLASS
/**
* Return a FrameLayout which will later hold the Fragment
*/
override fun createViewInstance(reactContext: ThemedReactContext) =
FrameLayout(reactContext)
/**
* Map the "create" command to an integer
*/
override fun getCommandsMap() = mapOf("create" to COMMAND_CREATE)
/**
* Handle "create" command (called from JS) and call createFragment method
*/
override fun receiveCommand(
root: FrameLayout,
commandId: String,
args: ReadableArray?
) {
super.receiveCommand(root, commandId, args)
val reactNativeViewId = requireNotNull(args).getInt(0)
when (commandId.toInt()) {
COMMAND_CREATE -> createFragment(root, reactNativeViewId)
}
}
@ReactPropGroup(names = ["width", "height"], customType = "Style")
fun setStyle(view: FrameLayout, index: Int, value: Int) {
if (index == 0) propWidth = value
if (index == 1) propHeight = value
}
/**
* Replace your React Native view with a custom fragment
*/
fun createFragment(root: FrameLayout, reactNativeViewId: Int) {
val parentView = root.findViewById<ViewGroup>(reactNativeViewId)
setupLayout(parentView)
val myFragment = MyFragment()
val activity = reactContext.currentActivity as FragmentActivity
activity.supportFragmentManager
.beginTransaction()
.replace(reactNativeViewId, myFragment, reactNativeViewId.toString())
.commit()
}
fun setupLayout(view: View) {
Choreographer.getInstance().postFrameCallback(object: Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
manuallyLayoutChildren(view)
view.viewTreeObserver.dispatchOnGlobalLayout()
Choreographer.getInstance().postFrameCallback(this)
}
})
}
/**
* Layout all children properly
*/
private fun manuallyLayoutChildren(view: View) {
// propWidth and propHeight coming from react-native props
val width = requireNotNull(propWidth)
val height = requireNotNull(propHeight)
view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
view.layout(0, 0, width, height)
}
companion object {
private const val REACT_CLASS = "MyViewManager"
private const val COMMAND_CREATE = 1
}
}
// 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
- Java
- Kotlin
// replace with your package
package com.mypackage
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class MyPackage : ReactPackage {
...
override fun createViewManagers(
reactContext: ReactApplicationContext
) = listOf(MyViewManager(reactContext))
}
// 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
- Java
- Kotlin
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(MyAppPackage())
}
@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. 从自定义视图管理器开始
import {requireNativeComponent} from 'react-native';
export const MyViewManager =
requireNativeComponent('MyViewManager');
II. 然后实现调用 create
方法的自定义视图
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 示例。