- 投稿日:2020-02-13T21:02:55+09:00
51歳から(現52)のプログラミング 備忘 Fragment 静的動的
Fragment 静的生成
Fragmentを静的に描写する流れ
1.MainActivity.classを実行すると、onCreate()でレイアウト(activity_main.xml)を生成
2.activity_main.xmlに<fragmen>タグを記載しておくと、
レイアウト(activity_main.xml)を生成するときに、
<fragment>を記載した表示領域でFragment.classを呼び出す
3.呼び出されたFragment.classのonCreate()でレイアウト(fragment.xml)が生成され、
activity_main.xmlで記載した表示領域にfragment.xmlが描写されるMainActivity.class -> activity_main.xml -> Fragment.class -> fragment.xml
サンプルコード Fragmentの静的生成
MainActivity.java@Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentCView(R.layout.activity_main); // <- ここでレイアウト生成 }}activity_main.xml//... <fragment android:id="@+id/fragmentArea" android:layout_width="wrap_content" android:layout_height="wrap_content" class="(パッケージ名).Fragment" //<- ここでFragment.javaを呼び出す />Fragment.javapublic class Fragment extends Fragment{ @Override public View onCreateView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState){ return inflater.inflate(R.layout.fragment,container,false);// <- fragmen.xmlをインフレート! } @Override public void onActivityCreated(Bundle savedInstanceState){ // Fragment生成後の処理 super.onActivityCreated(savedIndtanceState); TextView text = getActivity().findViewById(R.id.text); text.setText("From Fragment !"); } }fragment.xml//... <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="match_parent" /> }Fragment 動的生成
Fragmentを動的に生成する流れ
1.MainActivity.classを実行すると、onCreate()でレイアウト(activity_main.xml)を生成
2.activity_main.xmlで<frameLayout>を記載し、Fragmentの表示領域を確保
3.MainActivity.classで、Fragmentオブジェクトを生成して、
fragmentTransactionで、activity_main.xmlの<frameLayout>にFragmentオブジェクトを配置MainActivity.class -> activity_main.xml -> fragmentTransaction
サンプルコード Fragmentの動的生成
activity_main.xml<frameLayout android:id="@+id/fragmentArea" android:layout_width="match_parent" android:layout_height="match_parent" />MainActivity.javaprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FragmentManager manager = getSupportFragmentManager(); FragmentTransaction transaction = manager.beginTransaction();//managerでTransactionを開始 Fragment fragment = new Fragment(); //Fragmentオブジェクト生成 transaction.add(R.id.fragmentArea,fragment); //transactionでactivity_main.xmlの\<framLayout>の描写領域にfragmentオブジェクトをセット transaction.commit();Fragment.java@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment,container,false); // <-レイアウト(fragment.xml)をインフレート! } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { // fragmentオブジェクト生成後の処理 super.onActivityCreated(savedInstanceState); TextView text = (TextView) getActivity().findViewById(R.id.text); }fragment.xml<TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="match_parent" />サンプルコード Fragmentで画面遷移風
ファイル構成
activity_main.xml<fragment android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/fragmentArea" class="cat.baby.sample.Fragment_Toolbar" /> <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/frameArea" />MainActicity.java@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FragmentManager manager = getSupportFragmentManager(); FragmentTransaction transaction = manager.beginTransaction(); Fragment01 fragment_main = new Fragment01("1"); transaction.add(R.id.frameArea,fragment_main); transaction.commit(); }Fragment_Toolbar.javaAppCompatActivity activity; FrameLayout frameLayout; FragmentManager manager; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { activity = (AppCompatActivity) getActivity(); frameLayout = getActivity().findViewById(R.id.frameArea); manager = activity.getSupportFragmentManager(); View view = inflater.inflate(R.layout.fragment_toolbar,container,false); Toolbar toolbar = view.findViewById(R.id.toolbar); activity.setSupportActionBar(toolbar); setHasOptionsMenu(true); return view; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.overflow_menu01,menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()){ case R.id.menu01: FragmentTransaction transaction_menu01 = manager.beginTransaction(); Fragment01 fragment_main = new Fragment01("1"); transaction_menu01.replace(R.id.frameArea,fragment_main); transaction_menu01.commit(); return true; case R.id.menu02: FragmentTransaction transaction_menu02 = manager.beginTransaction(); Fragment01 fragment_second = new Fragment01("2"); transaction_menu02.replace(R.id.frameArea,fragment_second); transaction_menu02.commit(); return true; case R.id.menu03: FragmentTransaction transaction_menu03 = manager.beginTransaction(); Fragment01 fragment_third = new Fragment01("3"); transaction_menu03.replace(R.id.frameArea,fragment_third); transaction_menu03.commit(); } return super.onOptionsItemSelected(item); }Fragment01.javaint fragmentRes; public Fragment01(String no) { switch(no){ case "1": fragmentRes = R.layout.fragment_main; break; case "2": fragmentRes = R.layout.fragment_second; break; case "3": fragmentRes = R.layout.fragment_third; break; default: fragmentRes = R.layout.fragment_main; } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { View view = inflater.inflate(fragmentRes,container,false); return view; }fragment_toolbar.xml<androidx.appcompat.widget.Toolbar android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="#abb" android:id="@+id/toolbar" />fragment_main.xml<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="main" />fragment_second.xml<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="second" />fragment_third.xml<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="third" />でもこれだけじゃ、戻れない!
- 投稿日:2020-02-13T20:48:01+09:00
AAC Navigationでnavigate同時実行時のクラッシュへの対処法
問題
例えば、AAC Navigationのnavigate()するボタンを2つ同時に押すなどした時に以下のように怒られクラッシュする。
2020-02-13 15:24:08.709 21427-21427/jp.studysapuri.for_school.dev.debug E/AndroidRuntime: FATAL EXCEPTION: main Process: jp.studysapuri.for_school.dev.debug, PID: 21427 java.lang.IllegalArgumentException: navigation destination jp.studysapuri.for_school.dev.debug:id/action_mypageFragment_to_searchSchoolByConditionFragment is unknown to this NavController原因
navigate()が2つ同時に実行された場合、2つ目の実行時には現在の画面(ナビゲーションgraphのnode)が1つ目の遷移先になっているため、画面に紐つくactionが無いことになってしまう。
回避策
今いるnode(画面)に紐つくactionが有るかを確認しあればnavigate()を実行、なければ何もしないようにするような拡張関数(以下の例ではnavigateSafe())を作り、navigate()の代わりに使う。
import android.os.Bundle import androidx.annotation.IdRes import androidx.navigation.* fun NavController.navigateSafe( @IdRes resId: Int, args: Bundle? = null, navOptions: NavOptions? = null, navExtras: Navigator.Extras? = null ) { val action = currentDestination?.getAction(resId) ?: graph.getAction(resId) if (action != null && currentDestination?.id != action.destinationId) { navigate(resId, args, navOptions, navExtras) } } fun NavController.navigateSafe( node: NavDirections, navOptions: NavOptions? = null ) { val action = currentDestination?.getAction(node.actionId) ?: graph.getAction(node.actionId) if (action != null && currentDestination?.id != action.destinationId) { navigate(node, navOptions) } }
- 投稿日:2020-02-13T20:28:53+09:00
TextView内の特定の文字列に対して手軽にタップイベントを追加する
Androidアプリ内のTextViewに対してWebのハイパーリンクのようなタップイベントをつけたい事がたまによくあるので、通常のonClickイベントと同じ使い勝手でなるべく簡単に扱えるようKotlinの拡張関数を書いてみました
使用感イメージ
下記のスクリーンショットのように使うイメージです。
よくある利用シーンとしては「利用規約」や「プライバシーポリシー」の文言にタップイベントをつけたりするときとかですかね利用側のコードもここまで簡潔に記述するのを目的にしてみます
MainActivity.kthyperLinkTextView.text = "Hello World!" hyperLinkTextView.addHyperLink("Hello") { Toast.makeText(this, "'Hello' clicked!", Toast.LENGTH_SHORT).show() }ソースコード
実際のタップイベントや文字色の変更、下線を引いたりするのは
SpannableString
を使用しますが、ベタ書きすると煩雑なコードになりやすいので今回の利用用途に特化した拡張関数を書いてみます。HyperLinkExtension.ktfun TextView.addHyperLink(linkText: String, callback: ((view: View) -> Unit)) { val spannableMessage = SpannableString(text) val pattern = Pattern.compile(linkText) val matcher = pattern.matcher(text) while (matcher.find()) { spannableMessage.setSpan(object : ClickableSpan() { override fun onClick(textView: View) { callback.invoke(textView) } }, matcher.start(), matcher.end(), Spanned.SPAN_INCLUSIVE_INCLUSIVE) } text = spannableMessage movementMethod = LinkMovementMethod.getInstance() }また、追加でこんな形の引数をとる拡張関数も同時に生やしておくと便利かもしれません
HyperLinkExtension.ktfun TextView.addHyperLink(@StringRes vararg linkTextRes: Int, callback: ((view: View) -> Unit)) { linkTextRes.map { addHyperLink(it, callback) } } fun TextView.addHyperLink(@StringRes linkTextRes: Int, callback: ((view: View) -> Unit)) { addHyperLink(context.getString(linkTextRes), callback) } fun TextView.addHyperLink(vararg linkText: String, callback: ((view: View) -> Unit)) { linkText.map { addHyperLink(it, callback) } }制限
今回書いたコードでは利用側の簡潔さと引き換えに以下の制限が存在します。
- 判定に含まれる文字列が複数ある場合はすべてタップイベントの対象となります。
- 同じTextViewに対して
addHyperLink()
を複数使用しタップイベントを適用する文字列が重なった場合は、先にaddHyperLink()
したタップイベントが優先的に処理されます。- 内部的に
setText()
を行なっているため、改めて外側からsetText()
を行なった場合は条件に一致していたとしてもリンクが無効になります。この辺りが要件に合わない場合は内部の判定の仕方や代入の方法を調整してみてください
![]()
あと正規表現とか駆使して判定したい方はMatcherあたりをごにょってもらえるといいかもしれないです。参考
- 「TextView の一部のリンク化+クリックイベントの指定、をサクッと作る」
- TextViewのリンク化いろいろ
- 投稿日:2020-02-13T19:16:37+09:00
【Android】アプリを多言語化/ローカライズしたい(RTL対応あり)
概要
作成していたAndroidアプリを国外で配信する可能性があったため、アプリを日本語以外にも対応させたかった。
そこで、アプリを多言語化(ローカライズ)する方法や、アラビア語などの右読み言語(RTL; Right To Left)の場合はどうするかを調べてまとめた。多言語対応方法
リソースを用意する
<アプリプロジェクト>/app/src/main/res
の中には、例えば以下のようなリソースのディレクトリが格納されている。
drawable
(画像)values
(色や文字列などの値)layout
(レイアウト)対応したい言語のリソースディレクトリを用意すると、Android端末の設定を拾って、その言語に対応した表示になる。
ディレクトリの名前は次のようにする。
<リソースのタイプ>-<言語コード>[-r<地域コード>]
res/values-ja
、res/values-en-rGB
など<リソースのタイプ>-b+<言語コード>[+<地域コード>]
res/values-b+en+001
などそしてディレクトリの中に該当するリソースを用意する。
res/values/strings.xml<resources> <string name="title">Title</string> <resources>res/values-ja/strings.xml<resources> <string name="title">タイトル</string> <resources>※端末の設定に当てはまるものがない場合、読み込むリソースがなくなりアプリの強制終了などが起こり得るため、言語を指定しないデフォルトのリソースディレクトリを用意しておく。
多言語対応の要素を表示する
上で用意したリソースを要素に指定する。
指定の仕方は@<リソースのタイプ>/<リソースのname>
main.xml<TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/title" />これで、言語設定が日本語の場合は"タイトル"、それ以外の場合"Title"という文字列が表示される。
多言語対応の要素をコードで利用する
R.<リソースのタイプ>.<リソースのname>
で利用することができる。Main.kttextView.text = R.string.title // TextViewにテキストをセットするRTL、双方向テキストへの対応
アラビア語は右から左に読む(RTL)。
←مرحبا هناك
←しかし、テキスト中に数字・左から右に読む言語(LTR; 英語など)を含む場合は、その部分はLTRになる。
←مرحبا هناك
12345678←基本的にはシステムのデフォルト処理によってその通りに表示されるが、ローカライズしたテキストに逆方向のテキストを挿入した場合などは適当な表示にならないことがある。
- メッセージの先頭に挿入されている
- 数字や区切り記号で始まる
その場合は
BidiFormatter
クラスのunicodeWrap()
メソッドを用いる。また、Android 4.2(APIレベル17)以上ではレイアウトミラーリングもサポートされている。
AndroidManifest.xml
に以下を記述する。AndroidManifest.xml<application ...(略)... android:supportsRtl="true"> </application>参考記事
- 投稿日:2020-02-13T19:03:52+09:00
各OSのBluetoothの仕様
各OSのBluetoothの仕様
場所
25.5 Advertising Interval
The accessory should first use the recommended advertising interval of 20 ms for at least 30 seconds.
If it is not discovered within the initial 30 seconds, Apple recommends using one of the following longer
intervals to increase chances of discovery by the device:
● 152.5 ms
● 211.25 ms
● 318.75 ms
● 417.5 ms
● 546.25 ms
● 760 ms
● 852.5 ms
● 1022.5 ms
● 1285 ms推奨される広告パターンと広告間隔は次のとおりです。
最初に、20ミリ秒間隔で少なくとも30秒間アドバタイズします
30秒後に検出されない場合は、152.5ミリ秒、211.25ミリ秒、318.75ミリ秒、417.5ミリ秒、546.25ミリ秒、760ミリ秒、852.5ミリ秒、1022.5ミリ秒、1285ミリ秒のいずれかの長い間隔に変更できます。
重要: これらの特定の間隔は、正確に使用される間隔です!これらの実際の間隔からのわずかな偏差でさえ、発見までの時間を劇的に増加させる可能性があります。The blog post “What to keep in mind when developing your BLE Android app” contains a section with advertising. According to the blog, the allowed advertising intervals are the following:
a. ADVERTISE_MODE_LOW_LATENCY – Advertising interval: 100 ms
b. ADVERTISE_MODE_BALANCED – Advertising interval: 250 ms
c. ADVERTISE_MODE_LOW_POWER – Advertising interval: 1000 ms
- 投稿日:2020-02-13T19:01:29+09:00
BleManager option
BleManageroption
スキャンoptionでconnect成功率、速度向上できます
Android
まずAndroidじゃ!
scanningOptions - JSON - [Android only] after Android 5.0, user can control specific ble scan behaviors:
- numberOfMatches - Number - corresponding to setNumOfMatches
- matchMode - Number - corresponding to setMatchMode
- scanMode - Number - corresponding to setScanMode
- reportDelay - Number - corresponding to setReportDelay // 現在サポートされていない模様
scanningOptions - JSON - [Androidのみ] Android 5.0以降、ユーザーは特定のbleスキャン動作を制御できます。
- numberOfMatches - Number - Bluetooth LEスキャンフィルターのハードウェア一致の一致数を設定する
- matchMode - Number - Bluetooth LEスキャンフィルターの一致モードをハードウェア一致に設定する
- scanMode - Number - Bluetooth LEスキャンのスキャンモードを設定します。
### 詳細
numberOfMatches
- MATCH_NUM_MAX_ADVERTISEMENT
- hwのリソースの現在の機能と可用性に依存します。
- 定数値:3(0x00000003)
- MATCH_NUM_FEW_ADVERTISEMENT
- フィルターごとにいくつかの広告に一致します。hwのリソースの現在の機能と可用性に依存します
- 定数値:2(0x00000002)
- MATCH_NUM_ONE_ADVERTISEMENT
- フィルターごとに1つの広告に一致
- 定数値:1(0x00000001)
setMatchMode
- MATCH_MODE_AGGRESSIVE
- アグレッシブモードでは、信号強度が弱く、期間中に目撃/一致の数が少ない場合でも、hwは一致をより早く決定します。
- 定数値:1(0x00000001)
- MATCH_MODE_STICKY
- スティッキーモードの場合、hwでレポートする前に、信号強度と目撃のより高いしきい値が必要です。
定数値:2(0x00000002)
- scanMode
- SCAN_MODE_LOW_POWER
- 低電力モードでBluetooth LEスキャンを実行します。これは最小の電力を消費するため、デフォルトのスキャンモードです。このモードは、スキャンアプリケーションがフォアグラウンドにない場合に適用されます。
- 定数値:0(0x00000000)
- SCAN_MODE_BALANCED
- バランス電源モードでBluetooth LEスキャンを実行します。スキャン結果は、スキャン頻度と消費電力のバランスが取れたレートで返されます。
- 定数値:1(0x00000001)
- SCAN_MODE_LOW_LATENCY
- 最高のデューティサイクルを使用してスキャンします。アプリケーションがフォアグラウンドで実行されている場合にのみ、このモードを使用することをお勧めします。
- 定数値:2(0x00000002)
- SCAN_MODE_OPPORTUNISTIC
- 特別なBluetooth LEスキャンモード。このスキャンモードを使用するアプリケーションは、BLEスキャンを開始せずに他のスキャン結果を受動的にリッスンします。
- 定数値:-1(0xffffffff)
iOS
まず参考記事
- Core Bluetooth Background Processing for iOS Apps
- React-native native module possibilities for background task on IOS
For example, when your app is relaunched by system, you can retrieve all the restoration identifiers for the central manager objects the system was preserving for your app, like this:
After you have the list of restoration identifiers, simply loop through it and reinstantiate the appropriate central manager objects.システムがアプリのために保存していた中央マネージャーオブジェクトのすべての復元識別子を取得できます。
復元識別子のリストを取得したら、単純にループして、適切な中央マネージャーオブジェクトを再インスタンス化します。効果
前のスキャンでconnectしたBLEに対してスキャンすっ飛ばしてconnectするようになるので劇的に早くなる(3秒)
見つけられなければ通常スキャンに切り替わる使い方
- xcodebackgroud processing on
- infoにrestoreIdentifierKey任意のキー追加
- retrieveServicesooption指定すればokよ
例
if (!isAndroid) {
BleManager.start({ showAlert: false, restoreIdentifierKey: 'bitkey', forceLegacy: true });
}
気をつけないと行けないのがretrieveServicesooptionを指定してしまうとAndroidのスキャンが正常に動かないので
条件分岐などで避けてください!
- 投稿日:2020-02-13T14:31:12+09:00
TensorFlow Liteを使用して、Androidで人間のポーズをリアルタイムで追跡する
オンデバイスインテリジェンスのためのGoogle Coralソリューション:
✅姿勢推定:さまざまな身体の関節を特定することにより、画像内の人々の姿勢を推定します。
PoseNetは、主要な身体部分の位置を検出することにより、画像またはビデオ内の人物の姿勢を推定するビジョンモデルです。 例として、モデルは画像内の人の肘や膝の位置を推定できます。 ポーズ推定モデルは、画像内の人物を識別しません。 主要な身体部分の位置のみ。
TensorFlow Liteは、デバイスのカメラを使用してリアルタイムで1人の主要な身体部分を検出および表示するAndroidサンプルアプリケーションを共有しています。
このサンプルアプリケーションを使用すると、アプリ開発者や機械学習の専門家が軽量モバイルモデルの可能性を簡単に探ることができます。
PoseNetサンプルアプリケーション
Javaで記述された既存のAndroidサンプルとは対照的に、PoseNetサンプルアプリはKotlinで開発されました。 このアプリの開発目標は、誰でも簡単にPoseNetモデルを最小限のオーバーヘッドで使用できるようにすることでした。 サンプルアプリには、モデルの複雑さを抽象化するPoseNetライブラリが含まれています。 以下の図は、アプリケーション、PoseNetライブラリ、およびTensorFlow Liteライブラリ間のワークフローを示しています。
PoseNetライブラリ
PoseNetライブラリは、処理されたカメラ画像を取得し、人の主要な身体部位がどこにあるかに関する情報を返すインターフェースを提供します。 この機能は、推定されたRGBビットマップでTensorFlow Liteインタープリターを実行し、Personオブジェクトを返すメソッドであるtimateSinglePose()によって提供されます。 このページでは、PoseNetの入力と出力を解釈する方法について説明します。
// Estimate the body part positions of a single person. // Pass in a Bitmap and obtain a Person object. estimateSinglePose(bitmap: Bitmap): Person {...}Personクラスには、主要な身体部分の位置とそれに関連する信頼スコアが含まれています。 人の信頼スコアは、各キーポイントの信頼スコアの平均です。これは、キーポイントがその位置に存在する確率を示します。
// Person class holds a list of key points and an associated confidence score. class Person { var keyPoints: List<KeyPoint> = listOf<KeyPoint>() var score: Float = 0.0f }各KeyPointは、特定のBodyPartの位置とそのキーポイントの信頼スコアに関する情報を保持します。
// KeyPoint class holds information about each bodyPart, position, and score. class KeyPoint { var bodyPart: BodyPart = BodyPart.NOSE var position: Position = Position() var score: Float() = 0.0f } // Position class contains the x and y coordinates of a key point on the bitmap. class Position { var x: Int = 0 var y: Int = 0 } // BodyPart class holds the names of seventeen body parts. enum class BodyPart { NOSE, LEFT_EYE, RIGHT_EYE, ... RIGHT_ANKLE }PoseNetサンプルアプリ
PoseNetサンプルアプリは、カメラからフレームをキャプチャし、画像上のキーポイントをリアルタイムでオーバーレイするデバイス上のカメラアプリです。
アプリケーションは、着信カメラ画像ごとに次の手順を実行します。
- カメラプレビューから画像データをキャプチャし、YUV_420_888からARGB_888形式に変換します。
- RGB形式のフレームデータのピクセルを保持するBitmapオブジェクトを作成します。 モデルに渡すことができるように、ビットマップを切り取ってモデルの入力サイズに合わせます。
- PoseNetライブラリーのtimateSinglePose()関数を呼び出して、Personオブジェクトを取得します。
- ビットマップを画面サイズに戻します。 Canvasオブジェクトに新しいビットマップを描画します。
- Personオブジェクトから取得したキーポイントの位置を使用して、キャンバスにスケルトンを描画します。 特定のしきい値(既定では0.5)を超える信頼スコアを持つキーポイントを表示します。
ポーズレンダリングをカメラフレームと同期するために、ポーズとカメラの個別のViewインスタンスの代わりに、単一のSurfaceViewが出力表示に使用されました。 SurfaceViewは、Viewキャンバスを取得、ロック、およびペイントすることにより、画面を遅滞なく画面上に配置します。
デバイス上で実行する
GitHubからソースコードをダウンロードし、READMEを参照して実行方法を確認して、アプリを試してみることをお勧めします。
ロードマップ上
将来的には、このサンプルアプリのその他の機能を検討していきたいと考えています。
- マルチポーズ推定
- GPUデリゲートを使用したGPUアクセラレーション
- NNAPIデリゲートを使用したNNAPIアクセラレーション
- レイテンシを減らすためのモデルのトレーニング後の量子化
- ResNet PoseNetモデルなどの追加のモデルオプション(Eileen MaoとTanjin Prity、Googleのエンジニアリング実習インターン)
Google Coral海外代理店:Gravitylink (https://store.gravitylink.com/global)
- 投稿日:2020-02-13T14:19:35+09:00
【Android】Navigationの遷移先にあわせてToolbarをカスタマイズする
はじめに
みなさん、Navigation Component使ってますか?
Fragment間の画面遷移や値の受け渡しなどを非常に楽にしてくれるNavigationですが、ToolBarと併せて使用した時にいくつか詰まったポイントがありましたので、本記事で紹介したいと思います。
※2020/02/14 FragmentContainerViewについて追記しました
この記事で紹介すること
- Navigation + Toolbarを扱う時に困りがちなことの解決方法
この記事で紹介しないこと
- Navigationの基本的な使い方
Navigationの使い方を知りたい場合はこちらの記事が参考になります。
リポジトリ
こちらに今回のサンプルを置いておきます。
https://github.com/nanaten/Navigation-and-Toolbar-Sample前提
Activityのレイアウトはこんな感じです。
ActivityにToolbarとFragmentを乗っけています。
※style
はNoActionBar
にしています。activity_main.xml<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.google.android.material.appbar.AppBarLayout> <fragment android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/appbar" app:navGraph="@navigation/main_nav_graph" /> </androidx.constraintlayout.widget.ConstraintLayout>Tips
1. 特定のFragmentだけUpアイコンを表示させない
ログイン後のホーム画面など、この画面にはUpアイコン(←)を表示させたくない、という場合があると思います。
そういう時はAppBarConfigration
にUpアイコンを表示させたくないFragmentのIDをSet
で渡します。val navController = findNavController(R.id.nav_host_fragment) val appBarConfiguration = AppBarConfiguration(setOf(R.id.firstFragment)) setupActionBarWithNavController(navController, appBarConfiguration)アイコンを表示させたくないFragmentが複数ある場合は、以下のように複数のIDを渡せます。
val appBarConfiguration = AppBarConfiguration(setOf(R.id.firstFragment, R.id.secondFragment))2. 特定のFragmentだけToolbarを消したい
特定のFragmentに移動した時だけToolbarを表示させないようにしたい!と思ったことはありませんか?
Fragmentから操作しても良いのですが、なんとなくFragmentからActivityにあるコントロールを操作するのは気持ち悪い…
と思っていたら、navController.addOnNavigationDestinationListener
という便利なリスナーが存在しました。val navController = findNavController(R.id.nav_host_fragment) navController.addOnDestinationChangedListener { controller, destination, arguments -> }例えば
ThirdFragment
に移動した時だけToolbarを消したい、という場合は以下のように使えます。val navController = findNavController(R.id.nav_host_fragment) navController.addOnDestinationChangedListener { _, destination, _ -> toolbar.visibility = if(destination.id == R.id.thirdFragment) View.GONE else View.VISIBLE }3. タイトルを動的に切り替える
例えばリスト選択→詳細表示へ遷移した際に、選択した項目に合わせて動的にタイトルを設定したいという場合、以下の方法が使えます。
- NavGraphで設定する
addOnDestinationChangedListener
で設定する準備
どちらの方法も準備は同じです。
NavGraphでタイトル用のargumentを設定します。NavGraph<fragment android:id="@+id/fourthFragment" ...> <argument android:name="title" app:argType="string" /> </fragment>SafeArgsでFragmentに引数を渡してあげます。
SafeArgsについてはこちらの記事が参考になります。val title = "test title" val args = FirstFragmentDirections.actionFirstToFourth(title) findNavController().navigate(args)1. NavGraphで設定する
NavGraphで設定する場合は、fragmentのラベルに以下のように引数名を設定します。
NavGraph<fragment android:id="@+id/fourthFragment" ... android:label="{title}"> <argument android:name="title" app:argType="string" /> </fragment>これだけで引数に渡した値がタイトルとして表示されるようになります!
2. addOnDestinationChangedListenerで設定する
こちらは先ほどの
addOnDestinationChangedListener
を用いた方法になります。
3番目の引数arguments
からFragmentへ渡したargumentが取得できるため、そちらを利用します。navController.addOnDestinationChangedListener { _, _, arguments -> if(arguments?.getString("title") != null) { supportActionBar?.title = arguments.getString("title") } }
destination
でIDを判別しても良いですが、上記のようにargumentsの取得結果がnullかどうかで判別すれば、複数箇所でtitle
というargumentを使っている場合に対応が簡単になります。Toolbarの設定をActivityで一括管理したい場合は
addOnDestinationChangedListener
を使うのも一つの手かもしれません。おまけ FragmentContainerViewを使う場合のfindNavControllerのやり方
自分でやってみて詰まったのでメモ代わりに追記しておきます。
FragmentContainerView
とは、今までFragmentのコンテナとして利用されていたFrameLayout
の代替Viewとして登場したものです。
詳しい解説はこちらの記事が参考になります。レイアウトの
fragment
を以下のように置き換えることが可能ですが、普通にonCreateでfindNavController
すると「NavControllerが見つからない」というエラーが発生します。activity_main.xml<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> ... <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/appbar" app:navGraph="@navigation/main_nav_graph" /> </androidx.constraintlayout.widget.ConstraintLayout>エラーが出る書き方override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val navController = findNavController(R.id.nav_host_fragment) ... }こちらにIssueがありますが、
FragmentContainerView
に対して、onCreateの段階ではfragmentManager
がINITIALIZING状態にあるために起きているそうです(たぶん…)
FragmentContainerView
を使う場合は、navController
の取得方法を以下のように修正する必要があります。val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment val navController = navHostFragment.navController
FragmentContainerView
から直接ではなく、FragmentContainerView
にアタッチしたNavHostFragmentから取得しています(たぶん…)おわりに
本記事を書くにあたって、以下のリンク先を参考にさせて頂きました。
・[stsnブログ] メモ Android: Navigation Component + Toolbar(ActionBar)周りのコードを読んで見る
・[Qiita] 特定のFragmentが表示される時にAppBarLayoutとBottomNavigationViewを非表示にする
・[Kenji Abe] Navigationでタイトルを設定する