Android ネイティブUIコンポーネント
ネイティブモジュールとネイティブコンポーネントは、レガシーアーキテクチャで使用されている安定した技術です。これらは、新しいアーキテクチャが安定した将来、非推奨となる予定です。新しいアーキテクチャでは、Turbo Native Module と Fabric Native Components を使用して同様の結果を実現します。
最新のアプリで利用できるネイティブUIウィジェットは世の中にたくさんあります。プラットフォームの一部であるもの、サードパーティのライブラリとして利用できるもの、そしてあなた自身のポートフォリオですでに使用しているものもあるかもしれません。React Nativeには、ScrollView
やTextInput
のような最も重要なプラットフォームコンポーネントがすでにいくつかラップされていますが、すべてではありませんし、あなたが以前のアプリで自分で書いたものはもちろん含まれていません。幸いなことに、これらの既存のコンポーネントをラップして、React Nativeアプリケーションとシームレスに統合することができます。
ネイティブモジュールのガイドと同様に、これもAndroid SDKプログラミングにある程度精通していることを前提とした、より高度なガイドです。このガイドでは、ネイティブUIコンポーネントを構築する方法を示し、コアのReact Nativeライブラリで利用可能な既存のImageView
コンポーネントのサブセットの実装を通して解説します。
また、ネイティブコンポーネントを含むローカルライブラリを1つのコマンドでセットアップすることもできます。詳細については、ローカルライブラリのセットアップガイドをお読みください。
ImageViewの例
この例では、JavaScriptでImageViewを使用できるようにするための実装要件を順を追って説明します。
ネイティブビューは、ViewManager
、またはより一般的にはSimpleViewManager
を拡張して作成および操作されます。この場合、SimpleViewManager
は背景色、不透明度、Flexboxレイアウトなどの共通プロパティを適用するため便利です。
これらのサブクラスは本質的にシングルトンであり、ブリッジによって各インスタンスが1つだけ作成されます。それらはネイティブビューをNativeViewHierarchyManager
に送り、NativeViewHierarchyManager
は必要に応じてビューのプロパティを設定および更新するためにそれらにデリゲートバックします。ViewManagers
は通常、ビューのデリゲートでもあり、ブリッジを介してJavaScriptにイベントを送り返します。
ビューを公開するには
- ViewManagerサブクラスを作成します。
createViewInstance
メソッドを実装します。@ReactProp
(または@ReactPropGroup
)アノテーションを使用してビューのプロパティセッターを公開します。- アプリケーションパッケージの
createViewManagers
でマネージャーを登録します。 - JavaScriptモジュールを実装します。
1. ViewManager
サブクラスの作成
この例では、ReactImageView
型のSimpleViewManager
を拡張するビューマネージャクラスReactImageManager
を作成します。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
)でアノテーションされたセッターメソッドとして公開する必要があります。セッターメソッドは、最初の引数として更新されるビュー(現在のビュー型)を、2番目の引数としてプロパティ値を取る必要があります。セッターはpublicでなければならず、値を返してはなりません(つまり、Javaでは戻り値の型がvoid
、KotlinではUnit
であるべきです)。JSに送信されるプロパティの型は、セッターの値引数の型に基づいて自動的に決定されます。現在サポートされている値の型は次のとおりです(Javaの場合):boolean
、int
、float
、double
、String
、Boolean
、Integer
、ReadableArray
、ReadableMap
。Kotlinでの対応する型はBoolean
、Int
、Float
、Double
、String
、ReadableArray
、ReadableMap
です。
@ReactProp
アノテーションには、String
型の必須引数name
が1つあります。セッターメソッドにリンクされた@ReactProp
アノテーションに割り当てられた名前は、JS側でプロパティを参照するために使用されます。
name
以外に、@ReactProp
アノテーションは以下のオプション引数を取ることができます:defaultBoolean
、defaultInt
、defaultFloat
。これらの引数は対応する型(Javaではそれぞれboolean
、int
、float
、KotlinではBoolean
、Int
、Float
)でなければならず、提供された値は、セッターが参照しているプロパティがコンポーネントから削除された場合にセッターメソッドに渡されます。「デフォルト」値はプリミティブ型に対してのみ提供されることに注意してください。セッターが何らかの複合型である場合、対応するプロパティが削除されるとデフォルト値としてnull
が提供されます。
@ReactPropGroup
でアノテーションされたメソッドのセッター宣言要件は、@ReactProp
とは異なります。詳細については、@ReactPropGroup
アノテーションクラスのドキュメントを参照してください。重要! ReactJSでは、プロパティ値を更新するとセッターメソッドが呼び出されます。コンポーネントを更新する方法の1つとして、以前に設定されたプロパティを削除することがあることに注意してください。その場合も、プロパティが変更されたことをビューマネージャに通知するためにセッターメソッドが呼び出されます。その場合、「デフォルト」値が提供されます(プリミティブ型の場合、「デフォルト」値は@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モジュールの実装
最後の最後のステップは、新しいビューのユーザーのためにJava/KotlinとJavaScriptの間のインターフェース層を定義する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表現に対してイベントを発行し、2つのビューは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
コールバックプロップにマッピングするには、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アプリに統合するには、ViewManager
からView
を返すよりも、ネイティブコンポーネントをよりきめ細かく制御するためにAndroid Fragmentを使用する必要があるかもしれません。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. まずカスタムView Managerから始めます
import {requireNativeComponent} from 'react-native';
export const MyViewManager =
requireNativeComponent('MyViewManager');
II. 次に、create
メソッドを呼び出すカスタムViewを実装します
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の例を参照してください。