material-components / material-components-android

Modular and customizable Material Design UI components for Android
Apache License 2.0
16.37k stars 3.07k forks source link

[BottomNavigationView] Allow adding menu items without onItemsChanged being called #563

Open clhols opened 5 years ago

clhols commented 5 years ago

I want to add menu items to BottomNavigationView in code as their configuration is retrieved from the backend. And that can be done like this:

bottomNavigationItems.forEachIndexed { index, item ->
    bottomNavigationView.menu.apply {
      add(0, item.menuId, index, item.title).apply {
        setIcon(item.icon)
      }
    }
  }

But when you do that, MenuBuilder.addInternal calls onItemsChanged requesting a relayout after each item is added with is only needed after the last item has been added.

To handle this, MenuBuilder has stopDispatchingItemsChanged.

But BottomNavigationMenu.addInternal prevents this, as it calls startDispatchingItemsChanged internally.

clhols commented 5 years ago

Would it be acceptable if BottomNavigationView had these two methods:

 /**
   * Suspend layout of the {@link Menu} before adding or updating items.
   */
  public void suspendMenuUpdates() {
    presenter.setUpdateSuspended(true);
  }

  /**
   * Resume layout of the {@link Menu} after adding or updating items.
   */
  public void resumeMenuUpdates() {
    presenter.setUpdateSuspended(false);
    presenter.updateMenuView(true);
  }
drchen commented 3 years ago

You can call bottomNavigationView.menu.stopDispatchingItemsChanged() and bottomNavigationView.menu.startDispatchingItemsChanged() to achieve that.

clhols commented 3 years ago

@drchen The Menu interface doesn't have those functions. https://developer.android.com/reference/android/view/Menu

MenuBuilder has them, but like I wrote at the top, BottomNavigationMenu (which is a MenuBuilder) calls startDispatchingItemsChanged in its addInternal function and it triggers the relayout after each item is added.

drchen commented 3 years ago

Ah ok. The method it's a library only one.

We can add a public method to support that then. Reopen the issue.

clhols commented 3 years ago

In case anyone else needs this, our current hacky workaround is:

/**
 * Adds items to [BottomNavigationView] without layout after each item.
 * https://github.com/material-components/material-components-android/issues/563
 */
@SuppressLint("RestrictedApi")
fun BottomNavigationView.addItems(items: List<BottomNavigationItem>) {
  val presenter = getPresenter()
  presenter?.setUpdateSuspended(true)
  items.forEachIndexed { index, item ->
    menu.apply {
      add(0, item.menuId, index, item.title).apply {
        setIcon(item.icon)
      }
    }
  }
  presenter?.setUpdateSuspended(false)
  presenter?.updateMenuView(true)
}

fun BottomNavigationView.getPresenter(): BottomNavigationPresenter? {
  try {
    val field = this.javaClass.getDeclaredField("presenter")
    field.isAccessible = true
    return field.get(this) as BottomNavigationPresenter
  } catch (e: Exception) {
    Timber.e(e, "Exception when getting presenter field from BottomNavigationView")
  }
  return null
}