- 投稿日:2020-07-31T17:03:40+09:00
ReactNativeでAndroidのネイティブモジュールを実装する
作りたいアプリがAndroidのネイティブモジュールが必要なものだったので、作ることにしました。
2年前にもちょっとさわりましたが、前回はだいぶ適当だった+忘れていることもあり、自分用のメモも兼ねて書きます。はじめに
ReactNativeにはデフォルトである程度のネイティブモジュールが用意されています。例えば、カメラや録音など
通常その用意されたモジュールを使うので、Androidのネイティブモジュールを別段実装する必要はありません。
用意されたモジュールでは対応できない場合に実装する必要があります。
実装はAndroidJavaで行います。
Androidのネイティブモジュール
ドキュメントはここを読みました。
今回はドキュメントに書かれているToastModule(一瞬表示されるアラートのようなやつ)を参考に勘所が書けたらなと思います。
モジュールを作る(例.ToastModule.java)
①作りたいモジュールのJavaファイルを作る
作る場所はandroid/app/src/main/hjava/com/{{アプリ名}}/
{{作りたいモジュール名}}Module.java 例:ToastModule.javaというファイルを用意します。
ここで作ったモジュールが、ReactNativeから呼び出すモジュールになります。②お作法にのっとってモジュールを作る
作ったjavaファイルに、モジュールを書きます。
ToastModule.javapackage com.your-app-name; // AndroidのToastパッケージ import android.widget.Toast; // ReactNativeのパッケージ使用 import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; // Javaのパッケージ import java.util.Map; import java.util.HashMap; // ToastModule本体 public class ToastModule extends ReactContextBaseJavaModule { private static ReactApplicationContext reactContext; // Toastの表示時間を決める変数 private static final String DURATION_SHORT_KEY = "SHORT"; private static final String DURATION_LONG_KEY = "LONG"; // ReactNativeのパッケージのクラスを継承。必須 ToastModule(ReactApplicationContext context) { super(context); reactContext = context; } // モジュール名。JSから呼び出す際の名前。必須 @Override public String getName() { return "ToastExample"; } // 定数定義。ReactNative側の定数を返します @Override public Map<String, Object> getConstants() { final Map<String, Object> constants = new HashMap<>(); constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT); constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG); return constants; } // ReactNative側が呼び出すメソッド。実現したい機能を実装します。 // ToastModuleでは、AndroidのToastモジュールを呼び出し、 // メッセージを指定された秒数だけ表示するメソッドを作っています。 @ReactMethod public void show(String message, int duration) { Toast.makeText(getReactApplicationContext(), message, duration).show(); } }ReactNative側からReactMethodに変数を渡すと下記のようにマッピングされます。
JavaScript -> Java Boolean -> Bool Integer -> Number Double -> Number Float -> Number String -> String Callback -> function ReadableMap -> Object ReadableArray -> Arraycontextの継承部分とget_name(モジュールの名前を定義)の部分以外は好きに書いて大丈夫のようです。
モジュールのパッケージ化(例.CustomToastPackage.java)
作ったモジュールをパッケージ化します。
①パッケージのJavaファイルを作る
作る場所はandroid/app/src/main/hjava/com/{{アプリ名}}/
{{作りたいパッケージ名}}Package.java 例:CustomToastPackage.javaというファイルを用意します。
このパッケージファイルにモジュール(複数可)を登録し、MainApplication.javaから読みこんで使います。②お作法にのっとって書く
CustomToastPackage.javapackage com.your-app-name; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class CustomToastPackage implements ReactPackage { @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules( ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new ToastModule(reactContext)); return modules; } }一瞬難しく感じますが、このファイルでやっているのは、モジュールをパッケージ化しているだけです。
modules.add(new ToastModule(reactContext));この部分で作ったモジュールを追加しています。
そのため、別のモジュールを追加する場合は、modules.add(new ToastModule(reactContext)); modules.add(new ToastModule2(reactContext));のようにするだけです。
パッケージをMainApplicationに登録
①パッケージを登録し、ReactNativeのNativeModuleから呼び出せるようにする
ReactNativeではNativeModuleというパッケージが用意されており、このパッケージからNativeモジュールを呼び出します。
NativeModuleから呼び出すために、MainApplication.javaにパッケージを登録します。ファイルの場所はandroid/app/src/main/hjava/com/{{アプリ名}}/MainApplication.java
このファイルはデフォルトで用意されていると思います。ここにMainApplication.java... // 作ったパッケージ import com.your-app-name.CustomToastPackage; // <-- Add this line with your package name. ... protected List<ReactPackage> getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") 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 CustomToastPackage()); // <-- Add this line with your package name. return packages; }のように書くことで、パッケージを呼び出せるようになります。
JSから呼び出す
最後に、JSからモジュールを呼び出す専用のファイルを呼び出すコードを書きます。
ToastExample.jsimport { NativeModules } from 'react-native'; // get_nameで指定した名前 module.exports = NativeModules.ToastExample;App.jsimport ToastExample from './ToastExample'; // Module.javaでReactMethodで定義したメソッド ToastExample.show('Awesome', ToastExample.SHORT);以上、ネイティブモジュールの作り方です。
ここからちょっとおまけになります。
ネイティブモジュールのメソッド(ReactMethodの指定がされているメソッド)で値を返したい場合、どういう返しをするのかという話があります。ネイティブモジュールの戻り値
ネイティブモジュールからは指定の値をコールバックとPromiseで返せます。
ブリッジ通信は非同期であり、呼び出しと値の受け取りはちょっと考える必要があります。例えば、コールバックの場合
import com.facebook.react.bridge.Callback; public class UIManagerModule extends ReactContextBaseJavaModule { ... @ReactMethod public void measureLayout( int tag, int ancestorTag, Callback errorCallback, Callback successCallback) { try { measureLayout(tag, ancestorTag, mMeasureBuffer); float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]); float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]); float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]); float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]); successCallback.invoke(relativeX, relativeY, width, height); } catch (IllegalViewOperationException e) { errorCallback.invoke(e.getMessage()); } } ...上のコードの場合、
成功したら xy座標と横幅と縦幅を返す
失敗したら エラーメッセージを返す
という感じです。JS側では、UIManager.measureLayout( 100, 100, (msg) => { console.log(msg); }, (x, y, width, height) => { console.log(x + ':' + y + ':' + width + ':' + height); } );のように書くことで、成功と失敗両方に対応できます。
また、Promiseの場合は、
import com.facebook.react.bridge.Promise; public class UIManagerModule extends ReactContextBaseJavaModule { ... private static final String E_LAYOUT_ERROR = "E_LAYOUT_ERROR"; @ReactMethod public void measureLayout( int tag, int ancestorTag, Promise promise) { try { measureLayout(tag, ancestorTag, mMeasureBuffer); WritableMap map = Arguments.createMap(); map.putDouble("relativeX", PixelUtil.toDIPFromPixel(mMeasureBuffer[0])); map.putDouble("relativeY", PixelUtil.toDIPFromPixel(mMeasureBuffer[1])); map.putDouble("width", PixelUtil.toDIPFromPixel(mMeasureBuffer[2])); map.putDouble("height", PixelUtil.toDIPFromPixel(mMeasureBuffer[3])); promise.resolve(map); } catch (IllegalViewOperationException e) { promise.reject(E_LAYOUT_ERROR, e); } } ...上のコードの場合、
成功したら xy座標と横幅と縦幅を持つJSオブジェクトを返す
失敗したら エラーオブジェクトを返す
という感じです。JS側では、const measureLayout = async () => { try { var { relativeX, relativeY, width, height } = await UIManager.measureLayout(100, 100); console.log( relativeX + ':' + relativeY + ':' + width + ':' + height ); } catch (e) { console.error(e); } }; measureLayout();awaitを使って、値を受け取ります。このあたりは非同期通信とまんまいっしょですね。
JSへイベントの送信
私はまったくやったことないですが、ドキュメントに書いてあったので、一応ちょっと調べました。
JS側から呼び出されなくても、ネイティブモジュール側からイベント送信ができるというものです。
タイマーとか?で使えるのかと思います。ネイティブモジュール側
... import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.Arguments; ... private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); } ... WritableMap params = Arguments.createMap(); params.putString("eventProperty", "someValue"); ... sendEvent(reactContext, "EventReminder", params);ReactContextで、
reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params);getJSModuleというメソッドを呼び出すことで、JS側のイベントをトリガーに設定できるようです。
JS側はimport { NativeEventEmitter, NativeModules } from 'react-native'; ... componentDidMount() { ... const eventEmitter = new NativeEventEmitter(NativeModules.ToastExample); this.eventListener = eventEmitter.addListener('EventReminder', (event) => { console.log(event.eventProperty) // "someValue" }); ... } componentWillUnmount() { this.eventListener.remove(); //Removes the listener }addListenerで上で指定したネイティブモジュールでイベントを登録できるようです。
ネイティブモジュールを作るだけでしたら、以外と簡単でした。
2年前はあんなに苦労したのに。。。
- 投稿日:2020-07-31T16:05:05+09:00
スクリーンリーダーでのAndroidアプリ開発_使用を見送ったNativeScript編
Flutterをがっつり使ってみようと思う前に少し興味がありNativeScriptを触っていました。
XMLとJavaScriptでネイティブアプリが作れるということでサンプルコードをいじっていました。
また調べていく中でプラグインでスクリーンリーダー(AndroidではTalkBack、iOsではVoiceOver)を制御できる物があることを知りました。
アプリを動かす上で画面表示されている内容を読み上げるだけでなく、強制的にアナウンスをさせたい場面があります。
たとえば画面の遷移中や読み込み中て時間がかかっているときなどはその旨のアナウンスがあるとユーザは状況がわかり安心します。
なのでこのプラグインにも魅力を感じていました。
(Flutterにもいくつか同様のライブラリはあるようです)
ですが、以下の問題がありNativeScriptの使用を止めることにしました。
私の学習が足りなかったところもあると思いますが、記録として残しておきます。
ボタンに対するテキストラベルの問題
ただのアイコンや画像のボタンではそれがとのような働きをするのかスクリーンリーダーではわかりません。
そのためHTMLのalt属性のようにテキストラベルを付加します。
しかしこれが反映しないボタンやラベルとボタンが分離してしまう物がありました。タップ動作の付確実さ
サンプルコードで確認しただけなので確実ではありませんが、
TalkBackを有効にした上で
リストのアイテムをタップして項目を表示することができませんでした。
NativeScriptは問題ありませんでしたが、AngularとTypeScriptでこの現象を確認しました。スクリーンリーダープラグインが動作しなかった
表題の通りです。
開発は続いているようでしたのでフィードバックはしてあります。
Accessibility | NativeScript Marketplace
- 投稿日:2020-07-31T16:00:01+09:00
FlutterでSemanticsクラスを使ってコンテキストメニューを作成
表題の機能を組み込むときに参考情報が少なかったので、メモとして記します。
実現したいこと
アプリにはタップや長押しで呼び出すメニューの他に、スクリーンリーダー(Android→TalkBack、iOS→VoiceOver)向けにコンテキストメニューを提供することができます。
Androidでは
[[ローカルコンテキストメニュー]] → [操作]
iOSでは縦のスワイプ
でそれぞれ呼び出すことができます。
これにより素早く機能を呼び出すことが可能です。
作成は対象のウィジェットを[[Semantics()]]で囲い[[customSemanticsActions]]プロパティでメニューを定義します。また[[onTap]]と[[onLongPress]]プロパティでタップと長押しの動作も定義できます。
これは[[GestureDetector()]]でウィジェットを囲い定義することと同様です。
Semantics()ではそれぞれ[[onTaphint]]と[[onLongPressHint]]プロパティを使うことで、スクリーンリーダーがアナウンスする内容を指定することができます。
それぞれのジェスチャーで何ができるのか、事前に把握できるのはユーザにとって有益です。下記のサンプルコードは新規プロジェクトで作成されるコードに上記の内容を書き加えた物です。
スクリーンリーダーの挙動はこちらで確認できます。サンプルコード
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';void main() {
runApp(MyApp());
}class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
// This makes the visual density adapt to the platform that you run
// the app on. For desktop platforms, the controls will be smaller and
// closer together (more dense) than on mobile platforms.
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}class _MyHomePageState extends State {
int _counter = 0;_incrementCounter() {
setState((){
_counter ++;
});
}@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'You have pushed the button this many times:',
),
Semantics(
child: Text(
'$counter',
style: Theme.of(context).textTheme.headline4,
),
onTap: _incrementCounter,
onTapHint: '値を増加',
onLongPress: () => setState((){ _counter = 0;}),
onLongPressHint: 'リセット',
customSemanticsActions: {
CustomSemanticsAction(label: 'Increment'): _incrementCounter,
CustomSemanticsAction(label: 'reset'): () => setState(() => _counter = 0),
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () { _incrementCounter (); },
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
補足
ボタンにコンテキストメニューを不可する場合は更に
excludeSemantics: true,
value: 'increment',
button: true,
などのプロパティが必要です。
プロパティがなくても動作しますが、スクリーンリーダーがアナウンスしないため、ユーザがメニューに気が付けません。
- 投稿日:2020-07-31T14:21:58+09:00
Gradle で依存ライブラリをまるっとダウンロードする方法
以下のサイトの方法で行けました。
https://www.torutk.com/issues/79
自分のケース
com.microsoft.identity.client:msal:1.6.0
の依存ライブラリをまるっとダウンロードしたかったです。
com.microsoft.device.display
は exclude する必要があったので、最終的には以下の gradle ファイルで行けました。build.gradleapply plugin: 'java' repositories { google() mavenCentral() } dependencies { compile ("com.microsoft.identity.client:msal:1.6.0") { exclude group: 'com.microsoft.device.display' } } task copyDeps(type: Copy) { from (configurations.compile + configurations.testCompile) into "lib" }実行は以下のコマンドで
$ gradle copyDeps実行すると lib フォルダーに欲しいものがまとめて入ってます。
Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2020/07/31 14:16 30035 accessors-smart-1.2.jar -a--- 2020/07/31 14:16 25378 annotation-1.0.0.jar -a--- 2020/07/31 14:16 1077153 appcompat-1.0.2.aar -a--- 2020/07/31 14:16 53297 asm-5.0.4.jar -a--- 2020/07/31 14:16 7826 asynclayoutinflater-1.0.0.aar -a--- 2020/07/31 14:16 62313 browser-1.0.0.aar -a--- 2020/07/31 14:16 41692 collection-1.0.0.jar -a--- 2020/07/31 14:16 728460 common-2.0.15.aar -a--- 2020/07/31 14:16 64301 constraintlayout-1.1.3.aar -a--- 2020/07/31 14:16 121203 constraintlayout-solver-1.1.3.jar -a--- 2020/07/31 14:16 44222 coordinatorlayout-1.0.0.aar -a--- 2020/07/31 14:16 633019 core-1.0.1.aar -a--- 2020/07/31 14:16 11249 core-common-2.0.0.jar -a--- 2020/07/31 14:16 5475 core-runtime-2.0.0.aar -a--- 2020/07/31 14:16 10591 cursoradapter-1.0.0.aar -a--- 2020/07/31 14:16 33238 customview-1.0.0.aar -a--- 2020/07/31 14:16 11221 documentfile-1.0.0.aar -a--- 2020/07/31 14:16 32470 drawerlayout-1.0.0.aar -a--- 2020/07/31 14:16 156450 fragment-1.0.0.aar -a--- 2020/07/31 14:16 241622 gson-2.8.5.jar -a--- 2020/07/31 14:16 7669 interpolator-1.0.0.aar -a--- 2020/07/31 14:16 4722 jcip-annotations-1.0-1.jar -a--- 2020/07/31 14:16 120316 json-smart-2.3.jar -a--- 2020/07/31 14:16 11309 legacy-support-core-ui-1.0.0.aar -a--- 2020/07/31 14:16 4104 legacy-support-core-utils-1.0.0.aar -a--- 2020/07/31 14:16 20306 lifecycle-common-2.0.0.jar -a--- 2020/07/31 14:16 9431 lifecycle-livedata-2.0.0.aar -a--- 2020/07/31 14:16 8372 lifecycle-livedata-core-2.0.0.aar -a--- 2020/07/31 14:16 9164 lifecycle-runtime-2.0.0.aar -a--- 2020/07/31 14:16 6759 lifecycle-viewmodel-2.0.0.aar -a--- 2020/07/31 14:16 33445 loader-1.0.0.aar -a--- 2020/07/31 14:16 6808 localbroadcastmanager-1.0.0.aar -a--- 2020/07/31 14:16 244892 msal-1.6.0.aar -a--- 2020/07/31 14:16 342465 nimbus-jose-jwt-8.2.jar -a--- 2020/07/31 14:16 15782 print-1.0.0.aar -a--- 2020/07/31 14:16 23503 slidingpanelayout-1.0.0.aar -a--- 2020/07/31 14:16 32883 swiperefreshlayout-1.0.0.aar -a--- 2020/07/31 14:16 32789 vectordrawable-1.0.1.aar -a--- 2020/07/31 14:16 34398 vectordrawable-animated-1.0.0.aar -a--- 2020/07/31 14:16 27250 versionedparcelable-1.0.0.aar -a--- 2020/07/31 14:16 53513 viewpager-1.0.0.aar
- 投稿日:2020-07-31T05:45:04+09:00
FlutterFragmentのためにViewPager2を改造する
前回の記事
FlutterFragmentとViewPagerの組み合わせはonResumeのタイミングで難しかった
では、ViewPager2はページが完全遷移するまではonResumeが呼ばれないので、初回のonResumeで描画するFlutterFragmentとは相性が悪いという説明をしました。よってすぐにonResumeが呼ばれるようにViewPager2を書き換えてみようと思います。setMaxLifecycleメソッドの呼び出しが問題なので該当部分をコメントアウトします。
Android Jetpack ソースコードのダウンロード
ViewPager2はAndroid Jetpackの一部分なのでAndroid Jetpackのソースコードをダウンロードします。
公式のダウンロード方法解説(英語)
Qiitaに日本語の解説もあります。
AndroidX をソースからビルドするダウンロードすると15GBになります。
注意点
多くの人はライブラリについて安定版の最新版(執筆時点ではバージョン1.0.0)を使うと思いますが、こうしてダウンロードしたバージョンはアルファ版(執筆時点ではバージョン1.1.0-alpha01)になっていると思います。バージョンを指定してダウンロードする方法は見つけられませんでした。
ライブラリモジュールを作成する
改造したViewPager2を取り込みたいプロジェクトにライブラリモジュールを追加します。
プロジェクトのルート要素で右クリックします。
Android Libraryを選択します。
モジュール名とパッケージ名を指定します。
ViewPager2のソースコードをライブラリモジュールに追加する
このようなディレクトリ構成を想定します。
~/work/androidx-master-dev
ダウンロードしたAndroid Jetpackのソースコード~/work/quickecho
改造したViewPager2を含めたいアプリプロジェクトcd work/androidx-master-dev/frameworks/support/viewpager2 # Javaソースコードをコピーする cp -r viewpager2/src/main/java/androidx ~/work/quickecho/myviewpager2/src/main/java/ # リソースファイルをコピーする cp -r viewpager2/src/main/res ~/work/quickecho/myviewpager2/src/main/ViewPager2を書き換える
まず、リソースファイルのパッケージ名が違くてビルドできないので修正します。
ViewPager2.java// import androidx.viewpager2.R; import com.tfandkusu.myviewpager2.R;問題のsetMaxLifecycleメソッド呼び出し部分をコメントアウトします。3カ所あります。
FragmentStateAdapter.javamFragmentManager.beginTransaction() .add(fragment, "f" + holder.getItemId()) // .setMaxLifecycle(fragment, STARTED) .commitNow();ViewPager2を使用するモジュールからのモジュール取り込みを設定します。
build.gradle// 略 dependencies { // 今回作ったライブラリモジュール implementation project(':myviewpager2') // クラス名が一緒なので本家の取り込みはコメントアウトする // implementation "androidx.viewpager2:viewpager2:1.0.0" }あとは通常のViewPager2と同じように使えば良いです。
できあがり