メインコンテンツにスキップ

Android ネイティブ UI コンポーネント

情報

ネイティブモジュールとネイティブコンポーネントは、レガシーアーキテクチャで使用されている安定した技術です。新しいアーキテクチャが安定したら、将来的には廃止される予定です。新しいアーキテクチャでは、Turbo ネイティブモジュールFabric ネイティブコンポーネントを使用して同様の結果を実現します。

最新のアプリで利用できるネイティブUIウィジェットは数多く存在します。プラットフォームの一部であるもの、サードパーティライブラリとして利用できるもの、そして以前のアプリで自作したものもあるかもしれません。React Nativeは、ScrollViewTextInputなど、最も重要なプラットフォームコンポーネントのいくつかをすでにラップしていますが、すべてをラップしているわけではなく、以前のアプリで自作したコンポーネントはラップしていない可能性があります。幸いなことに、React Nativeアプリケーションとシームレスに統合するために、これらの既存のコンポーネントをラップすることができます。

ネイティブモジュールのガイドと同様に、これもAndroid SDKプログラミングにある程度精通していることを前提とした、より高度なガイドです。このガイドでは、ネイティブUIコンポーネントの構築方法を、コアReact Nativeライブラリで利用可能な既存のImageViewコンポーネントのサブセットの実装を通して説明します。

情報

また、1つのコマンドでネイティブコンポーネントを含むローカルライブラリをセットアップすることもできます。詳細はローカルライブラリのセットアップガイドをご覧ください。

ImageView の例

この例では、JavaScriptでImageViewを使用できるようにするための実装要件について説明します。

ネイティブビューは、ViewManagerを拡張するか、より一般的にはSimpleViewManagerを拡張することによって作成および操作されます。SimpleViewManagerは、背景色、不透明度、Flexboxレイアウトなどの一般的なプロパティを適用するため、この場合に便利です。

これらのサブクラスは本質的にシングルトンです。ブリッジによってそれぞれ1つのインスタンスのみが作成されます。ネイティブビューをNativeViewHierarchyManagerに送信し、必要に応じてビューのプロパティを設定および更新するために、ビューマネージャーに委任します。ViewManagerは通常、ビューのデリゲートでもあり、ブリッジを介してJavaScriptにイベントを送り返します。

ビューを送信するには

  1. ViewManager サブクラスを作成します。
  2. createViewInstance メソッドを実装します
  3. @ReactProp (または @ReactPropGroup) アノテーションを使用してビュープロパティセッターを公開します
  4. アプリケーションパッケージの createViewManagers にマネージャーを登録します。
  5. JavaScript モジュールを実装します

1. ViewManager サブクラスを作成する

この例では、ReactImageView型のSimpleViewManagerを拡張するビューマネージャークラスReactImageManagerを作成します。ReactImageViewはマネージャーによって管理されるオブジェクトの型であり、これはカスタムネイティブビューになります。getNameによって返される名前は、JavaScriptからネイティブビュートypを参照するために使用されます。

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) でアノテーションが付けられたセッターメソッドとして公開する必要があります。セッターメソッドは、更新するビュー (現在のビュートyp) を最初の引数として、プロパティ値を2番目の引数として取る必要があります。セッターはパブリックであり、値を返さないようにする必要があります (つまり、Javaでは戻り値の型はvoid、KotlinではUnitにする必要があります)。JSに送信されるプロパティの型は、セッターの値引数の型に基づいて自動的に決定されます。現在、次の型の値がサポートされています (Javaの場合): booleanintfloatdoubleStringBooleanIntegerReadableArrayReadableMap。Kotlinの対応する型は、BooleanIntFloatDoubleStringReadableArrayReadableMapです。

アノテーション@ReactPropには、String型の必須引数nameが1つあります。セッターメソッドにリンクされている@ReactPropアノテーションに割り当てられた名前は、JS側でプロパティを参照するために使用されます。

nameを除いて、@ReactPropアノテーションは次のオプションの引数を取ることができます: defaultBooleandefaultIntdefaultFloat。これらの引数は対応する型 (Javaではそれぞれbooleanintfloat、KotlinではBooleanIntFloat) である必要があり、セッターが参照しているプロパティがコンポーネントから削除された場合、提供された値がセッターメソッドに渡されます。「デフォルト」値はプリミティブ型にのみ提供されます。セッターが何らかの複雑な型の場合、対応するプロパティが削除されると、デフォルト値としてnullが提供されます。

@ReactPropGroupでアノテーションが付けられたメソッドのセッター宣言の要件は、@ReactPropとは異なります。詳細については、@ReactPropGroupアノテーションクラスのドキュメントを参照してください。**重要!** ReactJSでは、プロパティ値を更新するとセッターメソッドが呼び出されます。コンポーネントを更新する方法の1つは、以前に設定したプロパティを削除することです。その場合、プロパティが変更されたことをビューマネージャーに通知するために、セッターメソッドも呼び出されます。その場合、「デフォルト」値が提供されます (プリミティブ型の場合、「デフォルト」値は@ReactPropアノテーションのdefaultBooleandefaultFloatなどの引数を使用して指定できます。複雑な型の場合、セッターは値が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 モジュールを実装する

最後の手順は、新しいビューのユーザーのために、Java/KotlinとJavaScriptの間のインターフェースレイヤーを定義するJavaScriptモジュールを作成することです。このモジュールにコンポーネントインターフェースを dokumentieren することをお勧めします (例: 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表現にイベントを発行する必要があり、2つのビューは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);
}
}

JavaScriptでtopChangeイベント名をonChangeコールバックプロップにマップするには、ViewManagergetExportedCustomBubblingEventTypeConstantsメソッドをオーバーライドして登録します

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アプリに統合するには、ViewManagerからViewを返すよりも、Android Fragmentを使用してネイティブコンポーネントをより細かく制御する必要がある場合があります。onViewCreatedonPauseonResumeなどのライフサイクルメソッドを使用して、ビューに関連付けられたカスタムロジックを追加する場合に、これが必要になります。次の手順は、その方法を示しています

1. カスタムビューの例を作成する

まず、FrameLayoutを拡張するCustomViewクラスを作成します(このビューの内容は、レンダリングしたい任意のビューにすることができます)。

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. カスタムViewManagerから始める

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)アノテーションを使用してプロパティセッターを公開する場合は、上記のImageViewの例を参照してください。