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