Android 原生UI组件
在现代App开发中,存在大量原生UI部件,包括平台内置组件、第三方库或自定义收藏。React Native 已经封装了常见组件如 ScrollView 和 TextInput,但无法覆盖所有情况。幸运的是,在React Native应用中集成现有原生组件非常简单。
本指南将介绍如何构建原生UI组件,以React Native核心库中的ImageView为例,演示完整流程。假设您已有Android编程经验。
ImageView 示例
为了让JavaScript端使用ImageView,需要完成以下准备工作。原生视图由ViewManager的派生类(通常使用SimpleViewManager)创建和管理。SimpleViewManager适用于此场景,因为它支持公共属性如背景颜色、透明度和Flexbox布局。
这些子类是单例,React Native为每个管理器创建一个实例。它们创建原生视图并交给NativeViewHierarchyManager处理属性更新,同时代理视图事件并发送回JavaScript。
提供原生视图的基本步骤包括:创建ViewManager子类、实现createViewInstance方法、使用@ReactProp或@ReactPropGroup注解导出属性设置器、在应用程序包中注册视图管理类,以及实现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 注解导出属性设置器
导出属性时,使用@ReactProp或@ReactPropGroup注解的公共方法。第一个参数是视图实例,第二个是属性值,支持类型包括boolean、int、float、double、String、Boolean、Integer、ReadableArray、ReadableMap(Kotlin中对应类型)。
@ReactProp注解必须包含name参数指定JavaScript端属性名,可选参数如defaultBoolean、defaultInt、defaultFloat提供默认值。
@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模块定义接口,建议使用TypeScript或注释说明结构。
import { requireNativeComponent } from 'react-native';
/**
* Composes `View`.
*
* - src: Array<{url: string}>
* - borderRadius: number
* - resizeMode: 'cover' | 'contain' | 'stretch'
*/
export default requireNativeComponent('RCTImageView');
requireNativeComponent接受原生视图名。对于复杂逻辑如事件处理,可以用React组件封装。
事件处理
处理用户事件如缩放或拖动时,原生事件应触发JavaScript端视图事件,通过getId()关联视图。
在原生视图中发送事件:
public void onReceiveNativeEvent() {
WritableMap event = Arguments.createMap();
event.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext)getContext();
reactContext.getJSModule(RCTEventEmitter.class)
.receiveEvent(getId(), "topChange", event);
}
在ViewManager中注册事件映射到JavaScript回调:
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.builder().put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")
)
).build();
}
JavaScript端封装组件处理事件:
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 的整合实例
对于更精细的控制,可以使用Android Fragments替代直接返回View,以便利用生命周期方法如onViewCreated、onPause、onResume。
1. 创建一个自定义视图
继承FrameLayout创建CustomView。
package com.mypackage;
import android.content.Context;
import android.graphics.Color;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
public class CustomView extends FrameLayout {
public CustomView(@NonNull Context context) {
super(context);
this.setPadding(16,16,16,16);
this.setBackgroundColor(Color.parseColor("#5FD3F3"));
TextView text = new TextView(context);
text.setText("Welcome to Android Fragments with React Native.");
this.addView(text);
}
}
2. 创建一个 Fragment
定义MyFragment管理视图生命周期。
package com.mypackage;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
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;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 添加自定义逻辑
}
@Override
public void onPause() {
super.onPause();
// 添加自定义逻辑
}
@Override
public void onResume() {
super.onResume();
// 添加自定义逻辑
}
@Override
public void onDestroy() {
super.onDestroy();
// 添加自定义逻辑
}
}
3. 创建 ViewManager 子类
MyViewManager继承ViewGroupManager<FrameLayout>,处理Fragment创建和布局。
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.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;
}
@Override
public FrameLayout createViewInstance(ThemedReactContext reactContext) {
return new FrameLayout(reactContext);
}
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of("create", COMMAND_CREATE);
}
@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;
}
}
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);
}
});
}
public void manuallyLayoutChildren(View view) {
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
在自定义包中注册视图管理器。
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
在应用主类中添加自定义包。
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new MyPackage()); // 添加自定义包
return packages;
}
6. 执行 JavaScript 模块
首先,导出原生组件。
import { requireNativeComponent } from 'react-native';
export const MyViewManager = requireNativeComponent('MyViewManager');
然后,在React组件中调用create命令初始化Fragment。
import React, { useEffect, useRef } from 'react';
import { UIManager, findNodeHandle } from 'react-native';
import { MyViewManager } from './my-view-manager';
const createFragment = (viewId: number) =>
UIManager.dispatchViewManagerCommand(
viewId,
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?.height !== undefined ? style.height : '100%',
width: style?.width !== undefined ? style.width : '100%',
}}
ref={ref}
/>
);
};
属性设置器可使用@ReactProp注解,参考前面ImageView示例。