BottomNavigationViewとは#

アプリ内の主要な遷移先への移動を可能にするコンポーネントです。
上記スクリーンショットのような、画面下部にボタンを配置するという特徴から"Bottom"Navigationと呼ばれているのではないかと考えています。
BottomNavigationの詳細については、公式サイトをご覧ください。
BottomNavigationItemView長押し時の挙動を制御したい#
特別な処理を行わず、シンプルにBottomNavigationを用いてMenuをinflateさせ表示すると、アイテムを長押しした際に、Tooltipが表示されます。

(上記スクリーンショットはGoogle Payアプリの画面です。)
レアケースかもしれませんが、(というか、アクセシビリティ観点などから見ると非推奨なのかもしれません。) たとえばどうしてもこのTooltipを非表示にしたかったり、ユーザーがアイテムを長押しした際に独自の処理を行いたい場合には、このようにできますよ、という雰囲気のメモです。
ちなみに今確認したところ、Twitterアプリは長押し時にTooltipが表示されていないようなので、もしBottomNavigationViewを利用している場合は制御を行っている可能性があります。
先にコードだけ書いておくと、以下のようにすることで制御が可能です
bottomNavigation.menu.forEach {
val view = bottomNavigation.findViewById<View>(it.itemId)
view.setOnLongClickListener {
// よしなに行いたい処理を記述
true
}
}この記事の後半では、BottomNavigationViewの内部のコードなどを見ていきます。
Tooltipはどのように表示されているのか#
冒頭にも書きましたが、BottomNavigationViewを用いると、長押しした際の標準の挙動としてこのTooltipが表示されます。 このTooltipが、どのように表示されているのか、ざっくり処理を追っていきます。
処理を追うBottomNavigationViewが入っているMaterial Componentsのバージョンです:
com.google.android.material:material:1.2.0-alpha05
BottomNavigationView#inflateMenu#
まず、BottomNavigationViewをXML上で定義する際に、menuを指定します。(もしくはコード上からinflateMenu関数を呼び出すことも可能です。)
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
...
app:menu="@menu/bottom_navigation_menu" // menuResを指定
/>こうすると、BottomNavigationViewのコンストラクタ内で、inflateMenu関数が呼ばれます。
public void inflateMenu(int resId) {
presenter.setUpdateSuspended(true);
getMenuInflater().inflate(resId, menu);
presenter.setUpdateSuspended(false);
presenter.updateMenuView(true);
}BottomNavigationPresenter#updateMenuView#
BottomNavigationView.inflateMenu関数内部では、MenuInflaterクラスのinflate関数を呼び出しMenuをinflateしています。 また、BottomNavigationPresenterというPresenterクラスの関数をいくつか呼び出しています。
注目したいのは、BottomNavigationPresenter.updateMenuViewです。この関数を見ていきます。
@Override
public void updateMenuView(boolean cleared) {
if (updateSuspended) {
return;
}
if (cleared) {
menuView.buildMenuView();
} else {
menuView.updateMenuView();
}
}BottomNavigationView.inflateMenu関数内部では、
presenter.updateMenuView(true);という呼び出しをしているため、引数のclearedの値はtrueが入っています。そのため、(もしupdateSuspendedというフィールドの値がtrueでなければ、)以下のブロックが実行されます。
menuView.buildMenuView();BottomNavigationItemView#initialize#
BottomNavigationMenuView.buildMenuView関数の内部では、BottomNavigationViewで表示する各アイテムのView(=BottomNavigationItemView)を初期化します。 一部抜粋すると、以下のようになっています:
BottomNavigationItemView child = getNewItem();
...
child.initialize((MenuItemImpl) menu.getItem(i), 0);
child.setItemPosition(i);
child.setOnClickListener(onClickListener);
if (selectedItemId != Menu.NONE && menu.getItem(i).getItemId() == selectedItemId) {
selectedItemPosition = i;
}
setBadgeIfNeeded(child);
addView(child);ここでは、setOnClickListenerなどを呼んでいる箇所はありますが、長押し時のためのリスナーをセットするsetOnLongLickListenerなどは呼び出ししていません。 ということで、BottomNavigationItemView.initializeのコード内部を読みます。まるっと抜粋すると・・
@Override
public void initialize(@NonNull MenuItemImpl itemData, int menuType) {
this.itemData = itemData;
setCheckable(itemData.isCheckable());
setChecked(itemData.isChecked());
setEnabled(itemData.isEnabled());
setIcon(itemData.getIcon());
setTitle(itemData.getTitle());
setId(itemData.getItemId());
if (!TextUtils.isEmpty(itemData.getContentDescription())) {
setContentDescription(itemData.getContentDescription());
}
CharSequence tooltipText = !TextUtils.isEmpty(itemData.getTooltipText())
? itemData.getTooltipText()
: itemData.getTitle();
TooltipCompat.setTooltipText(this, tooltipText);
setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
}ここでtooltipTextという文字が見えます。何やらここでいろいろやってそうです。
TooltipCompat.setTooltipText#
CharSequence tooltipText = !TextUtils.isEmpty(itemData.getTooltipText())
? itemData.getTooltipText()
: itemData.getTitle();
TooltipCompat.setTooltipText(this, tooltipText);TooltipCompat.setTooltipTextというのは、それっぽい名前ですね。内部を見ると、
public static void setTooltipText(@NonNull View view, @Nullable CharSequence tooltipText) {
if (Build.VERSION.SDK_INT >= 26) {
view.setTooltipText(tooltipText);
} else {
TooltipCompatHandler.setTooltipText(view, tooltipText);
}
}SDKバージョンが26以上の場合はView.setTooltipTextを呼び出しています。
TooltipCompatHandler.setTooltipText#
if (Build.VERSION.SDK_INT >= 26) {
view.setTooltipText(tooltipText);
} else {
TooltipCompatHandler.setTooltipText(view, tooltipText);
}elseの分岐時に実行される、TooltipCompatHandler.setTooltipTextの処理を見ていきます。
if (TextUtils.isEmpty(tooltipText)) {
if (sActiveHandler != null && sActiveHandler.mAnchor == view) {
sActiveHandler.hide();
}
view.setOnLongClickListener(null);
view.setLongClickable(false);
view.setOnHoverListener(null);
} else {
new TooltipCompatHandler(view, tooltipText);
}なにやらtooltipTextが空の場合はviewに対してsetOnLongClickListener(null)しています。tooltipTextが空ではない場合は、TooltipCompatHandlerクラスを初期化しています。 このクラスのコンストラクタでなにかやっていそうです。
TooltipCompatHandler#
private TooltipCompatHandler(View anchor, CharSequence tooltipText) {
mAnchor = anchor;
mTooltipText = tooltipText;
mHoverSlop = ViewConfigurationCompat.getScaledHoverSlop(
ViewConfiguration.get(mAnchor.getContext()));
clearAnchorPos();
mAnchor.setOnLongClickListener(this);
mAnchor.setOnHoverListener(this);
}実行されているのは上記のコンストラクタです。 引数に渡ってきたViewのsetOnLongClickListenerを呼び出し、自身をListenerとしてセットしています。お察しかもしれませんが、TooltipCompatHandlerはView.OnLongClickListenerを実装しています。
@Override
public boolean onLongClick(View v) {
mAnchorX = v.getWidth() / 2;
mAnchorY = v.getHeight() / 2;
show(true /* from touch */);
return true;
}SDK26未満のバージョンの場合には、こちらのコードが実行されていそう、ということがわかりました。
つまり、もともとやりたかったことに話を戻すと、ここに渡ってくるViewに対してOnLongClickListenerを上書きでセットすればよさそうです。 ここに渡ってくるViewというのは、BottomNavigationMenuView.buildMenuView関数の内部で初期化していたBottomNavigationItemViewです。 これらのViewは、BottomNavigation.getMenu関数で取得したMenuのgetItem関数にてアクセスできます。一つのMenuアイテムを取得する場合にはこれでもよいですが、たとえばすべてのMenuにたいしてアクセスしたい場合などには、androidx.corecore-ktxライブラリに便利なKotlin拡張関数が用意されているため、こちらを活用できます。
/** Performs the given action on each item in this menu. */
inline fun Menu.forEach(action: (item: MenuItem) -> Unit) {
for (index in 0 until size()) {
action(getItem(index))
}
}上記の拡張関数を利用すると、最終的には以下のようなコードで、BottomNavigationItemView長押し時の処理を制御できます。
bottomNavigation.menu.forEach {
val view = bottomNavigation.findViewById<View>(it.itemId)
view.setOnLongClickListener {
// よしなに行いたい処理を記述
true
}
}以上です。 もしなにか間違いやご指摘等ありましたら、コメントやTwitterのDM等いただけますと幸いです🙏
参考リンクなど#
- 参考にしたStackoverflow
BottomNavigation material.io
Tooltips developer.android.com