BottomNavigationViewとは
アプリ内の主要な遷移先への移動を可能にするコンポーネントです。
上記スクリーンショットのような、画面下部にボタンを配置するという特徴から"Bottom"Navigationと呼ばれているのではないかと考えています。
BottomNavigationの詳細については、公式サイトをご覧ください。
material.io
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関数を呼び出すことも可能です。)
<comgoogleandroidmaterialbottomnavigationBottomNavigationView
androidid="@+id/bottomNavigation"
androidlayout_width="0dp"
androidlayout_height="wrap_content"
...
appmenu="@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 );
return true;
}
SDK26未満のバージョンの場合には、こちらのコードが実行されていそう、ということがわかりました。
つまり、もともとやりたかったことに話を戻すと、ここに渡ってくるViewに対してOnLongClickListenerを上書きでセットすればよさそうです。
ここに渡ってくるViewというのは、BottomNavigationMenuView.buildMenuView関数の内部で初期化していたBottomNavigationItemViewです。
これらのViewは、BottomNavigation.getMenu関数で取得したMenuのgetItem関数にてアクセスできます。一つのMenuアイテムを取得する場合にはこれでもよいですが、たとえばすべてのMenuにたいしてアクセスしたい場合などには、androidx.corecore-ktxライブラリに便利なKotlin拡張関数が用意されているため、こちらを活用できます。
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.com
stackoverflow.com