Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

BottomNavigationView

Обновлено 24 октября 2023 года

Компонент из раздела Containers. Размещается в нижней части экрана. Может содержать от трёх до пяти элементов.

Подключаем стандартным способом, либо студия сама предложит установить библиотеку, если она ещё не прописана.


implementation("com.google.android.material:material:1.10.0")

По названию видно, что компонент должен размещаться в нижней части экрана. В теории ничто не мешает разместить его в верхней части, но народ вас не поймёт.

Основные требования к компоненту:

  • От трёх до пяти элементов на панели
  • Значок активного элемента должен использовать первичный цвет (primary). Используйте чёрный или белый цвет, если панель окрашена в цвет
  • Ширина каждой секции вычисляется делением ширины компонента на число элементов (максимум 168dp и минимум 80dp)
  • Высота: 56dp
  • Размер значка: 24 x 24dp

Создадим простой вариант.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        style="@style/Widget.Material3.Toolbar.Surface"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/true_solid" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment_container"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:menu="@menu/menu_bottom" />

</LinearLayout>

Для кастомизации можете задействовать следующие атрибуты

  • app:itemBackground - фоновый цвет. Он заменит цвет атрибута android:background, если вы использовали
  • app:itemIconTint - цвет значка
  • app:itemTextColor - цвет текста
  • app:menu - ресурс меню. Обязательный атрибут

Меню создаётся стандартным способом в папке res/menu.


<?xml version="1.0" encoding="utf-8"?>
<menu
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/inboxFragment"
        android:icon="@android:drawable/ic_dialog_email"
        android:title="Inbox" />
    <item
        android:id="@+id/sentItemsFragment"
        android:icon="@android:drawable/ic_menu_send"
        android:title="Sent Items" />
    <item
        android:id="@+id/aboutFragment"
        android:icon="@android:drawable/ic_menu_help"
        android:title="About" />
</menu>

При попытке добавить шестой элемент в меню вы получите ошибку при запуске программы Caused by: java.lang.IllegalArgumentException: Maximum number of items supported by BottomNavigationView is 5. Limit can be checked with BottomNavigationView#getMaxItemCount().

Навигация между фрагментами происходит стандартным способом. Идентификаторы в меню должны совпадать с идентификаторами фрагментов. Никакого кода писать не нужно. Процесс создания файла навигации здесь я опущу.

В старых примерах, когда навигация ещё широко не использовалась, нужно было вручную писать код. Далее старые примеры ещё на Java.

Для отслеживания нажатия на определённый значок используется соответствующий слушатель BottomNavigationView.OnNavigationItemSelectedListener.


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    BottomNavigationView navigation = findViewById(R.id.bottom_navigation);
    navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);

}

// Выбор происходит по идентификаторам меню (R.id.action_search и т.д)
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
        = item -> {
    switch (item.getItemId()) {
        case R.id.action_search:
            setTitle("Search");
            return true;
        case R.id.action_settings:
            setTitle("Settings");
            return true;
        case R.id.action_navigation:
            setTitle("Navigation");
            return true;
    }
    return false;
};

BottomNavigationView

Также был старый способ переключаться между фрагментами приблизительно следующим образом.


Fragment currentFragment = null;
switch (item.getItemId()) {
    case R.id.menu_alarm_add:
        currentFragment = FragmentOne.newInstance();
        break;
    case R.id.menu_alarm_list:
        currentFragment = FragmentTwo.newInstance();
        break;
    case R.id.menu_alarm_off:
        currentFragment = FragmentThree.newInstance();
        break;
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.framelayout, currentFragment);
transaction.commit();
return true;

Можно скрывать нижнюю панель при прокрутке. Для этого придётся создать отдельный класс и использовать макет CoordinatorLayout.


public class BottomNavigationBehavior extends CoordinatorLayout.Behavior<BottomNavigationView> {}

Пример на Kotlin + setOnNavigationItemReselectedListener

Старое: Напишем аналогичный пример на Kotlin и добавим обработку событий щелчка на уже выбранном элементе через setOnNavigationItemReselectedListener.


// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.

package ru.alexanderklimov.bottomnavigation

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.bottomnavigation.BottomNavigationView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        val textView: TextView = findViewById(R.id.textView)
        val bottom_navigation_view: BottomNavigationView = findViewById(R.id.bottom_navigation_view)

        bottom_navigation_view.setOnNavigationItemSelectedListener {
            when (it.itemId) {
                R.id.action_one -> {
                    textView.text = "Выбран элемент One"
                    true
                }
                R.id.action_two -> {
                    textView.text = "Выбран элемент Two"
                    true
                }
                R.id.action_three -> {
                    textView.text = "Выбран элемент Three"
                    true
                }else -> false
            }
        }

        bottom_navigation_view.setOnNavigationItemReselectedListener {
            when (it.itemId) {
                R.id.action_one -> textView.text = "Повторно выбран элемент One"
                R.id.action_two -> textView.text = "Повторно выбран элемент Two"
                R.id.action_three -> textView.text = "Повторно выбран элемент Three"
            }
        }
    }
}

Элементы можно выбрать программно. Добавим кнопку в макет и напишем код.


button.setOnClickListener {
    bottom_navigation_view.menu.getItem(1).isChecked = true
    textView.text = "Программно выбран второй элемент"
}

Также можно программно установить цвет для BottomNavigationView.


button.setOnClickListener {
    bottom_navigation_view.setBackgroundColor(Color.GREEN)
    textView.text = "Программно установлен цвет"
}

Пример у меня не заработал, если явно установлен цвет в атрибутах app:itemBackground="@android:color/darker_gray", поэтому его нужно убрать.

Если у вас нет особой надобности использовать свой цвет для нижней панели, то используйте рекомендованный вариант в стиле Material Design:


app:itemBackground="@color/colorPrimary"

В этом случае вы можете использовать свои цвета для значков и текста вместо белого/чёрного. Но программно потом изменить цвет не сможете (см. предыдущий пример).

Также можно сделать элемент меню недоступным. Добавим в селектор новую настройку до предыдущих настроек в файле bottom_navigation_item_background_colors.


<item android:color="@android:color/darker_gray" android:state_enabled="false" />

Сделаем недоступным первый элемент меню


<item
    android:id="@+id/action_one"
    android:icon="@android:drawable/ic_dialog_map"
    android:enabled="false"
    android:title="One" />

Теперь первый элемент не будет реагировать на нажатия.

BottomNavigationView

labelVisibilityMode

Видимость текста к значкам регулируется атрибутом app:labelVisibilityMode, в скобках приводится программный вариант. Если установить значение unlabeled (LABEL_VISIBILITY_UNLABELED), то текст выводиться не будет. Соответственно, значение labeled (LABEL_VISIBILITY_LABELED) будет выводить текст у всех значков. Значение selected (LABEL_VISIBILITY_SELECTED) будет выводить текст только у выбранного элемента меню. По умолчанию используется значение auto (LABEL_VISIBILITY_AUTO), когда при наличии трёх и меньше значков текст выводится у всех элементов, а если значков четыре-пять, то выводится текст только у выбранного элемента.


// XML
<com.google.android.material.bottomnavigation.BottomNavigationView
    ...
    app:labelVisibilityMode="selected" />

// Code
bottom_navigation_view.labelVisibilityMode = LabelVisibilityMode.LABEL_VISIBILITY_SELECTED

itemHorizontalTranslationEnabled

Атрибут app:itemHorizontalTranslationEnabled приподнимает элемент меню, когда он выбран. Работает в связке с предыдущим атрибутом, который должен иметь либо значение selected либо auto с 4-5 элементами.


// XML
<com.google.android.material.bottomnavigation.BottomNavigationView
    ...
    app:labelVisibilityMode="selected"
    app:itemHorizontalTranslationEnabled="true" />

// Code
bottom_navigation_view.isItemHorizontalTranslationEnabled = true

Бейдж

У элементов меню можно выводить бейджики. В простом случае это простой кружочек, который может сигнализировать о поступлении нового письма, рождении котёнка и т.д. Выводим бейдж у первого элемента меню.


val badge = bottom_navigation_view.getOrCreateBadge(R.id.action_one);
badge.isVisible = true
BottomNavigationView

Переменная badge является экземпляром класса BadgeDrawable. Есть несколько методов для работы с классом. Не забывайте удалять бейдж, когда он больше не требуется.


bottom_navigation_view.getOrCreateBadge(R.id.item1)     // Show badge
bottom_navigation_view.removeBadge(R.id.item1)          // Remove badge
val badge = bottom_navigation_view.getBadge(R.id.item1) // Get badge

У класса BadgeDrawable есть несколько полезных методов: setNumber/getNumber/hasNumber/clearBadgeNumber. Например, мы можем вывести число.


badge.number = 9
BottomNavigationView

Методы setMaxCharacterCount/getMaxCharacterCount устанавливают максимальное число символов, после которого большие значения выводятся с плюсом. По умолчанию используется четыре символа.


badge.maxCharacterCount = 2
badge.number = 102
BottomNavigationView

Управлять местоположением выводимых чисел в бейдже можно через методы setBadgeGravity/getBadgeGravity, используя константы TOP_END (по умолчанию), TOP_START, BOTTOM_END, BOTTOM_START.

Также можно задать отступы через методы setHorizontalOffset/getHorizontalOffset/setVerticalOffset/setVerticalOffset.

Можно настроить цвет бейджей через атрибуты или методы класса BadgeDrawable.


badge.badgeTextColor = Color.MAGENTA
badge.backgroundColor = Color.CYAN

Подсказка

При долгом нажатии на элементе меню выводится подсказка (либо прохождении мышки на соответствующих устройствах). По умолчанию выводится текст, определённый в атрибуте android:title. Можно переопределить текст через атрибут app:tooltipText.


<item
    android:id="@+id/action_three"
    android:icon="@android:drawable/ic_dialog_email"
    android:title="Three"
    app:tooltipText="Письмо коту"/>
BottomNavigationView

После какого-то обновления подсказка у меня перестала работать. Не разбирался.

Внутри BottomAppBar

Можно получить интересный результат, если поместить BottomNavigationView внутри BottomAppBar с использованием FloatingActionButton. Но тут не обошлось без хитростей.

Сделаем разметку экрана.


<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:layout_weight="1"
        android:textAppearance="@style/TextAppearance.AppCompat.Display1"
        tools:text="Text" />

    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottom_app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:fabCradleMargin="20dp"
        app:fabCradleRoundedCornerRadius="20dp"
        app:fabCradleVerticalOffset="10dp">

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/bottom_navigation_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:itemIconTint="@color/teal_200"
            app:itemTextColor="@color/purple_200"
            app:contentInsetStart="0dp"
            app:contentInsetLeft="0dp"
            android:background="@android:color/transparent"
            app:menu="@menu/menu_bottom_navigation" />
    </com.google.android.material.bottomappbar.BottomAppBar>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_action_cat"
        app:layout_anchor="@id/bottom_app_bar"/>


</androidx.coordinatorlayout.widget.CoordinatorLayout>

Здесь важно обратить внимание на три атрибута. Атрибут android:background устанавливает прозрачность, иначе подложка компонента некрасиво будет видна на экране. Два другие атрибута (выделены в коде жирным) убирают смещение. Оно хорошо видно, если убрать на время прозрачность. Как вариант, можно было использовать вариант android:layout_marginEnd="16dp", но нет гарантии, что значение 16dp будет неизменным в других обновлениях.

На этом хитрости не заканчиваются. Если элементов меню BottomNavigationView будет нечётным, то один из значков может оказаться под кнопкой FloatingActionButton и будет опять некрасиво. Чтобы избежать этой проблемы, мы добавим фейковый элемент с пустым заголовком.


<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_one"
        android:icon="@android:drawable/ic_dialog_map"
        android:title="One" />

    <item
        android:id="@+id/action_two"
        android:icon="@android:drawable/ic_dialog_info"
        android:title="Two" />

    <item
        android:id="@+id/placeholder"
        android:title="" />

    <item
        android:id="@+id/action_three"
        android:icon="@android:drawable/ic_dialog_email"
        android:title="Three"
        app:tooltipText="Письмо коту" />

    <item
        android:id="@+id/action_four"
        android:icon="@android:drawable/ic_btn_speak_now"
        android:title="Four" />

</menu>

Хотя мы убрали прозрачность в XML, этого недостаточно. А также надо убрать возможность щёлкать по фейковому элементу. Поэтому добавляем код.


val bottomNavigationView: BottomNavigationView = findViewById(R.id.bottom_navigation_view)

bottomNavigationView.background = null
bottomNavigationView.menu.getItem(2).isEnabled = false

bottomNavigationView.setOnNavigationItemSelectedListener {
    ...
}

bottomNavigationView.setOnNavigationItemReselectedListener {
    ...
}
BottomNavigationView
Реклама