跳至主要內容

Android 原生 UI 組件

資訊

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

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

與原生模組指南一樣,這也是一個較進階的指南,假設您對 Android SDK 程式設計有一定程度的熟悉。本指南將向您展示如何建置原生 UI 元件,引導您完成核心 React Native 程式庫中現有 ImageView 元件子集的實作。

ImageView 範例

對於此範例,我們將逐步說明實作需求,以允許在 JavaScript 中使用 ImageView。

原生檢視是透過延伸 ViewManager 或更常見的 SimpleViewManager 來建立和操作的。SimpleViewManager 在這種情況下很方便,因為它套用常見的屬性,例如背景顏色、不透明度和 Flexbox 版面。

這些子類別基本上是單例 - 橋接器僅建立每個類別的一個實例。它們會將原生檢視傳送給 NativeViewHierarchyManager,而 NativeViewHierarchyManager 會委派它們設定和更新檢視的屬性(視需要而定)。ViewManagers 通常也是檢視的委派,會透過橋接器將事件傳送回 JavaScript。

傳送檢視

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

1. 建立 ViewManager 子類別

在此範例中,我們建立檢視管理員類別 ReactImageManager,它延伸 SimpleViewManager 型別的 ReactImageViewReactImageView 是由管理員管理的物件型別,這將會是自訂的原生檢視。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)註解的 setter 方法。setter 方法應將要更新的檢視(當前檢視類型)作為第一個參數,將屬性值作為第二個參數。setter 應為公用且不傳回值(即傳回類型在 Java 中應為 void,在 Kotlin 中應為 Unit)。傳送至 JS 的屬性類型會根據 setter 的值參數類型自動決定。目前支援下列值類型(在 Java 中):booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap。在 Kotlin 中對應的類型為 BooleanIntFloatDoubleStringReadableArrayReadableMap

註解 @ReactProp 有個必填參數 name,類型為 String。連結至 setter 方法的 @ReactProp 註解所指定的 name 用於在 JS 端參照屬性。

除了 name@ReactProp 註解可以接受下列選用參數:defaultBooleandefaultIntdefaultFloat。這些參數應為對應的類型(在 Java 中分別為 booleanintfloat,在 Kotlin 中分別為 BooleanIntFloat),且在 setter 所參照的屬性已從元件中移除時,會將提供的值傳遞給 setter 方法。請注意,「預設」值僅提供給基本類型,如果 setter 為某個複雜類型,則在對應的屬性被移除時,會提供 null 作為預設值。

使用 @ReactPropGroup 標記的方法的 setter 宣告需求與 @ReactProp 不同,請參閱 @ReactPropGroup 標記類別文件,以取得更多相關資訊。重要!在 ReactJS 中,更新屬性值會導致呼叫 setter 方法。請注意,我們可以更新元件的方法之一是移除之前已設定的屬性。在這種情況下,也會呼叫 setter 方法,以通知檢視管理員屬性已變更。在這種情況下,將提供「預設」值(對於基本類型,可以使用 @ReactProp 標記的 defaultBooleandefaultFloat 等引數指定「預設」值,對於複雜類型,setter 會呼叫將值設定為 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

最後一步驟是將 ViewManager 註冊到應用程式,這會以類似於 原生模組 的方式進行,透過應用程式套件成員函式 createViewManagers

  @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: string
* - borderRadius: number
* - resizeMode: 'cover' | 'contain' | 'stretch'
*/
module.exports = 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();
}
}

這個回呼會使用原始事件呼叫,我們通常會在包裝元件中處理它以建立更簡潔的 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 整合的範例

若要將現有的原生 UI 元素整合到 React Native 應用程式,你可能需要使用 Android Fragments,以提供比從 ViewManager 傳回 View 更精細的原生元件控制權。如果你想要使用生命週期方法(例如 onViewCreatedonPauseonResume)來新增與你的檢視相關聯的客製化邏輯,你將需要這樣做。下列步驟將說明如何執行此操作

1. 建立範例客製化檢視

首先,我們建立一個 CustomView 類別,它會延伸 FrameLayout(這個檢視的內容可以是你想要呈現的任何檢視)

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.add(new MyPackage());
return packages;
}

6. 實作 JavaScript 模組

I. 從自訂 View 管理員開始

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

export const MyViewManager =
requireNativeComponent('MyViewManager');

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

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