跳到主要內容

Android 原生 UI 組件

資訊

原生模組和原生組件是舊版架構使用的穩定技術。當新架構穩定後,它們將在未來被棄用。新架構使用 Turbo 原生模組Fabric 原生組件 來達到類似的效果。

市面上有很多原生 UI 小部件,可以立即在最新的應用程式中使用 - 其中一些是平台的一部分,另一些則以第三方函式庫的形式提供,還有更多可能在你自己的作品集中使用。React Native 已經封裝了幾個最關鍵的平台組件,例如 ScrollViewTextInput,但並非全部,當然也不包括你可能為之前的應用程式編寫的組件。幸運的是,我們可以封裝這些現有的組件,以便與你的 React Native 應用程式無縫整合。

與原生模組指南類似,這也是一份更進階的指南,假設你對 Android SDK 程式設計有一定程度的熟悉。本指南將向你展示如何建構原生 UI 組件,並逐步引導你完成核心 React Native 函式庫中現有 ImageView 組件子集的實作。

資訊

你也可以使用一個命令設定包含原生組件的本地函式庫。請閱讀本地函式庫設定指南以瞭解更多詳細資訊。

ImageView 範例

在本範例中,我們將逐步介紹實作需求,以允許在 JavaScript 中使用 ImageViews。

原生視圖透過擴展 ViewManager 或更常見的 SimpleViewManager 來建立和操作。在這種情況下,SimpleViewManager 很方便,因為它應用了常見的屬性,例如背景顏色、不透明度和 Flexbox 佈局。

這些子類別本質上是單例模式 - 每種類型只會由橋接器建立一個實例。它們將原生視圖發送到 NativeViewHierarchyManager,後者委派回它們以根據需要設定和更新視圖的屬性。ViewManagers 通常也是視圖的委派,透過橋接器將事件發送回 JavaScript。

傳送視圖

  1. 建立 ViewManager 子類別。
  2. 實作 createViewInstance 方法
  3. 使用 @ReactProp(或 @ReactPropGroup)註解公開視圖屬性 setter
  4. 在應用程式套件的 createViewManagers 中註冊管理器。
  5. 實作 JavaScript 模組

1. 建立 ViewManager 子類別

在本範例中,我們建立視圖管理器類別 ReactImageManager,它擴展了類型為 ReactImageViewSimpleViewManagerReactImageView 是管理器管理的物件類型,這將是自訂原生視圖。由 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 方法。請注意,「預設」值僅針對原始類型提供,如果 setter 是某種複雜類型,則當相應的屬性被移除時,將提供 null 作為預設值。

使用 @ReactPropGroup 註解的方法的 setter 宣告要求與 @ReactProp 不同,有關更多資訊,請參閱 @ReactPropGroup 註解類別文件。**重要!** 在 ReactJS 中,更新屬性值將導致 setter 方法呼叫。請注意,我們更新組件的方法之一是移除之前設定的屬性。在這種情況下,也會呼叫 setter 方法以通知視圖管理器屬性已變更。在這種情況下,將提供「預設」值(對於原始類型,「預設」值可以使用 @ReactProp 註解的 defaultBooleandefaultFloat 等引數指定,對於複雜類型,將呼叫 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,這與原生模組類似,透過應用程式套件成員函數 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 自由控制的原生視圖組件,但是我們如何處理來自使用者的事件,例如捏合縮放或平移?當原生事件發生時,原生程式碼應向視圖的 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 回呼屬性,請透過覆寫 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={props.onChange} />;
}

與 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. 從自訂 View manager 開始

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

export const MyViewManager =
requireNativeComponent('MyViewManager');

II. 然後實作呼叫 create 方法的自訂 View

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)註解公開屬性 setter,請參閱上方的 ImageView 範例