React NativeのNative Module化について(Android)
React NativeのNative Module化について(Android)
こんにちは! アルダグラムで、サーバーサイド寄りのエンジニアをしている内倉です。
KANNA のネイティブアプリは、React Native で作られているのですが
諸事情により、カメラの機能だけ Native Module で実装することになりました。
前回の記事では、@WatanabeKoki が iOS 版実装のさわり部分を紹介してくれたので
私の担当する Android 版のほうも、やっていきたいと思います💪
概要は↓こんな感じ。
1. Kotlin を使う準備
せっかくなら Kotlin で書きたいので、先に Kotlin を使うための設定をしていきます。
build.gradle
buildscript {
ext {
...
kotlinVersion = "1.6.10"
}
...
{
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
}
}
app/build.gradle
apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"
Java のファイルと Kotlin のファイルを分けて置きたい派の人は ↓ も追加しましょう!
android {
...
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
2. NativeModules を React Native から呼び出してみる
カメラの前に、とりあえず簡単なコンポーネントを作って React Native 側から呼び出してみたいと思います。
(1) Layout ファイル作成
src/main/res/layout に、適当にファイルを作ります。
後々、色々詰め込みたいので layout ファイルでやってしまいますが、動的にやってもOKです。
src/main/res/layout/layout_my_view.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/blue" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="40dp"
android:text="kokodayo" />
</FrameLayout>
(2) View 的なクラス作成
src/main/java/xxx の下に、適当なパッケージを切って、View となるクラスを作っていきます。
MyView.kt
class MyView : FrameLayout {
val TAG = "MyView"
private var context: ThemedReactContext? = null
constructor(context: ThemedReactContext) : super(context) {
this.context = context
var layout: FrameLayout = LayoutInflater.from(context).inflate(R.layout.layout_my_view, this, true) as FrameLayout
}
}
(3) ViewManager クラス作成
React Native からブリッジ経由で呼び出しがあった際に、対応するクラスを作ります。
MyViewManager.kt
class MyViewManager : SimpleViewManager<MyView>() {
override fun getName(): String {
// ReactNative から呼び出すときのコンポーネント名
return "MyView"
}
override fun createViewInstance(reactContext: ThemedReactContext): MyView {
// ↑で呼び出されたときに、ブリッジを通じて React Native へ送り返すインスタンスを返す
return MyView(reactContext)
}
}
(4) Package クラス作成
(2)(3) で作ったパッケージを管理するクラスを作成して
MainApplication.java の getPackages() メソッド内で登録します。
MyPackage.kt
class MyPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
return Collections.emptyList<NativeModule>()
}
override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> {
return mutableListOf(
MyViewManager() as ViewManager<View, ReactShadowNode<*>>
)
}
}
MainApplication.java
public class MainApplication extends Application implements ReactApplication {
...
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new MyPackage()); // ← ここを追加
return packages;
}
}
(5) React Native から呼び出し
最後に、React Native から呼び出します。
src/ui/organisms/NativeView.tsx
import { requireNativeComponent } from 'react-native'
// ViewManager.getName で設定したのと同じ値だよ
export default requireNativeComponent('MyView')
あとは、↑を好きなとこで import して使うだけ!
src/ui/screens/xxx.tsx
import { NativeView } from '@ui/organisms/NativeView'
...
const xxx: FC<xxxProps> = ({ navigation, route }) => {
...
return (
<View>
...
<NativeView style={{ width: '100%', height: '100%' }} />
...
</View>
)
}
React Native から呼び出すときに、style で表示範囲を指定するのがポイントです。
表示範囲の指定がないと、コンポーネントは呼ばれるけど何も表示されません。
ここまできたら、npm run android すると↓こんな画面が表示されるはずです。
3. カメラプレビューを実装する
では、先程作った MyView をカメラプレビューに改造していきましょう!
今回は CameraX を使います。
(1) 依存関係を追加
CameraX で、一部 Java8 のメソッドを使っているので、compileOptions も設定します。
app/build.gradle
androd {
...
buildTypes {
...
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
...
def camerax_version = "1.1.0-beta02"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
}
(2) Manifest ファイルに、カメラのパーミッションを追加
<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
...
<uses-permission android:name="android.permission.CAMERA" />
...
</manifest>
(3) layout ファイルの修正
TextView を消して、オレンジの部分を追加します。
src/main/res/layout/layout_my_view.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/blue" >
<androidx.camera.view.PreviewView
android:id="@+id/camera_preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
(4) MyView 修正
まずは公式のとおりにやっていきます。
a. ProcessCameraProvider を取得して初期化する。
startCamera() は、カメラ以外の準備が整ったら constructor から呼ぶ感じです。
bindPreview(cameraProvider) は、この次に実装するのでまだなくて大丈夫です。
private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
...
private fun startCamera() {
cameraProviderFuture = ProcessCameraProvider.getInstance(this.context as Context)
cameraProviderFuture.addListener(Runnable {
try {
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
bindPreview(cameraProvider) // これから作るね
} catch (e: Exception){
Log.e(TAG, e.stackTraceToString())
}
}, ContextCompat.getMainExecutor(this.context as Context))
}
b. bindPreview() 作成
実際に、カメラの準備をしていきます。
CameraX では、Preview、Image analysis、Image capture、Video capture の4つの機能( usecase )があり、必要なものだけを選んで登録します。
今回は、プレビューするだけなので↓こんなかんじ。
fun bindPreview(cameraProvider: ProcessCameraProvider) {
// 使いたい usecase 作成
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
// 使いたいカメラを指定
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// カメラと usecase をバインド
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
c. そして MyView 自体に LifecycleOwner インターフェイスを登録
class MyView : FrameLayout**, LifecycleOwner** {
...
private lateinit var lifecycleRegistry : LifecycleRegistry
...
constructor(context: ThemedReactContext) : super(context) {
...
lifecycleRegistry = LifecycleRegistry(this)
context.addLifecycleEventListener(object : LifecycleEventListener {
override fun onHostResume() {
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
}
override fun onHostPause() {
lifecycleRegistry.currentState = Lifecycle.State.CREATED
}
override fun onHostDestroy() {
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
}
})
}
override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
}
d. root の FrameLayout のサイズ設定
Native Module で実装する場合、ここまでだとまだ表示されません!
View が実際に追加されてからサイズを設定しないと表示できないようなので、指定を追加します。
...
private lateinit var previewView: PreviewView
constructor(context: ThemedReactContext) : super(context) {
...
var layout = LayoutInflater.from(context).inflate(R.layout.layout_my_view, this, true) as FrameLayout
previewView = layout.findViewById<PreviewView>(R.id.camera_preview_view)
previewView.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewRemoved(parent: View?, child: View?) = Unit
override fun onChildViewAdded(parent: View?, child: View?) {
parent?.measure(
View.MeasureSpec.makeMeasureSpec(measuredWidth, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(measuredHeight, View.MeasureSpec.EXACTLY)
)
parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
}
})
}
↓ a 〜 d まで実装した MyView がこちらです
class MyView : FrameLayout, LifecycleOwner {
val TAG = "MyView"
private lateinit var context: ThemedReactContext
private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
private lateinit var lifecycleRegistry : LifecycleRegistry
private lateinit var previewView: PreviewView
private var camera: Camera? = null
constructor(context: ThemedReactContext) : super(context) {
this.context = context
var layout = LayoutInflater.from(context).inflate(R.layout.layout_my_view, this, true) as FrameLayout
previewView = layout.findViewById<PreviewView>(R.id.camera_preview_view)
previewView.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewRemoved(parent: View?, child: View?) = Unit
override fun onChildViewAdded(parent: View?, child: View?) {
parent?.measure(
View.MeasureSpec.makeMeasureSpec(measuredWidth, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(measuredHeight, View.MeasureSpec.EXACTLY)
)
parent?.layout(0, 0, parent.measuredWidth, parent.measuredHeight)
}
})
startCamera()
lifecycleRegistry = LifecycleRegistry(this)
context.addLifecycleEventListener(object : LifecycleEventListener {
override fun onHostResume() {
lifecycleRegistry.currentState = Lifecycle.State.RESUMED
}
override fun onHostPause() {
lifecycleRegistry.currentState = Lifecycle.State.CREATED
}
override fun onHostDestroy() {
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
}
})
}
private fun startCamera() {
cameraProviderFuture = ProcessCameraProvider.getInstance(this.context as Context)
cameraProviderFuture.addListener(Runnable {
try {
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
bindPreview(cameraProvider)
} catch (e: Exception){
Log.e(TAG, e.stackTraceToString())
}
}, ContextCompat.getMainExecutor(this.context as Context))
}
fun bindPreview(cameraProvider: ProcessCameraProvider) {
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
}
無事、プレビューが表示されました!やったね🥳
株式会社アルダグラムのTech Blogです。 世界中のノンデスクワーク業界における現場の生産性アップを実現する現場DXサービス「KANNA」を開発しています。 採用情報はこちら: herp.careers/v1/aldagram0508/
Discussion