Material Design. Динамический Toolbar на живом примере

Уверен, что те, кто следят за изменениями в мире Android, заметили, что Toolbar начинает играть в приложениях всё более значимую роль. Например в последней версии Gmail клиента в Toolbar вынесен почти весь функционал по работе с почтой, а в новом Google Chrome Toolbar отвечает за работу с текстом страниц.

В данной статье я постараюсь рассказать о создании динамического Toolbar, который позволит пользователю работать с контентом четырьмя различными способами в рамках одного Activity. Мы рассмотрим весь процесс разработки Toolbar-a начиная с xml файлов стилей и заканчивая анимацией иконок, а в конце статьи я оставлю ссылку на GitHub репозиторий с примером полностью рабочего приложения.

Начнём с постановки задачи


Мы будем разрабатывать Toolbar для приложения, которое позволит пользователю следить за изменениями цен на акции. На главном экране будет расположен список всех акций, за которыми следит пользователь, мы также должны реализовать базовый функционал: удаление, поиск и сортировку акций. Вот так я реализовал этот функционал с помощью динамического Toolbar-a:

Стандартный режим Режим поиска Режим удаления Режим сортировки

Создаём xml файлы конфигураций


Итак, в первую очередь нам нужно создать xml файл самого Toolbar-a. Я советую сделать это в отдельном файле, так как в будущем мы скорее всего захотим использовать один и тот же (или похожий) Toolbar во всех Activity нашего приложения.

res/layout/toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/toolbar_actionbar"
    android:layout_width="match_parent"
    app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    android:layout_height="?android:actionBarSize"
    android:background="@color/toolbar_orange"/>

Теперь мы можем добавить toolbar.xml в xml Activity следующим образом:
res/layout/activity_main.xml
<include layout="@layout/toolbar" />

Поскольку в нашем Toolbar будет располагаться виджет поиска, мы можем настроить его внешний в вид в файле styles.xml нашего приложения. В 21 версии Android SDK появилось гораздо больше возможностей для кастомизации виджета поиска (SearchView Widget), вы можете посмотреть полный список атрибутов по этой ссылке: AppCompat v21 — Material Design for Pre-Lollipop Devices! В этом же файле мы зададим цвет нашего Toolbar.

res/values/styles.xml
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimaryDark">@color/status_bar_orange</item>
        <item name="searchViewStyle">@style/AppSearchViewStyle</item>
    </style>
    <style name="AppSearchViewStyle" parent="Widget.AppCompat.SearchView">
        <item name="android:textCursorDrawable">@drawable/white_cursor</item>
        <item name="queryBackground">@android:color/transparent</item>
        <item name="searchIcon">@drawable/icon_toolbar_search</item>
        <item name="closeIcon">@drawable/icon_toolbar_clear</item>
        <item name="queryHint">@string/search_hint</item>
        <item name="android:imeActionId">6</item>
    </style>
</resources>

И наконец создадим файл со списком всех элементов нашего Toolbar-а. Тут у нас есть несколько вариантов:
  • В начале создать только те элементы, которые будут видны в стандартном режиме, а затем в коде добавлять или удалять элементы при переходе между режимами.
  • Сразу создать все существующие элементы в xml файле, а в коде просто управлять их видимостью.

Я выбрал второй вариант так как у нас не так много элементов внутри Toolbar и нам нет смысла экономить память храня в ней только видимые элементы.

Также существует два способа создания элементов Toolbar:
  • Создавать элементы внутри меню (Menu), как экземпляры класса MenuItem. Этот способ использовался в предыдущих версиях Анрдроид (API Level < 21), когда еще не было Toolbar.
  • Создавать все элементы, как обычные View внутри файла toolbar.xml.

Я решил использовать способ с меню потому, что во-первых, так нам не нужно создавать свой лейаут для Toolbar-а. Во-вторых, у нас не будет проблем с обратной совместимостью и в-третьих, мы избежим конфликтов между Toolbar и Navigation Drawer (боковое меню присутствующие в приложении и управляющиеся с помощью ActionBarDrawerToggle, который в свою очередь работает с MenuItem)

res/menu/menu_activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:yourapp="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_search"
        android:title="@android:string/search_go"
        android:icon="@drawable/icon_toolbar_search"
        yourapp:showAsAction="always|collapseActionView"
        yourapp:actionViewClass="android.support.v7.widget.SearchView" />
    <item
        android:id="@+id/action_edit"
        android:title="@string/edit"
        android:icon="@drawable/icon_toolbar_edit"
        yourapp:showAsAction="ifRoom" />
    <item
        android:id="@+id/action_micro"
        android:title="@string/microphone"
        android:icon="@drawable/icon_toolbar_micro"
        yourapp:showAsAction="always" />
    <item
        android:id="@+id/action_remove"
        android:title="@string/remove"
        android:icon="@drawable/icon_toolbar_remove"
        yourapp:showAsAction="always" />
    <item
        android:id="@+id/action_sort"
        android:title="@string/sort_ab"
        android:icon="@drawable/icon_toolbal_sort"
        yourapp:showAsAction="always" />
</menu>

Добавляем Toolbar к Activity


Все xml файлы созданы и теперь мы можем добавить Toolbar к Activity. Для начала перезапишем метод onCreateOptionsMenu, который отвечает за инициализацию меню.
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_activity_main, menu);
        return true;
    }

Теперь зададим Toolbar, как ActioanBar, это обеспечит нам обратную совместимость с предыдущими версиями Android (API Level < 21).
 @Override
    protected void onCreate(Bundle savedInstanceState) {
        Toolbar mActionBarToolbar = (Toolbar) findViewById(R.id.toolbar_actionbar);
        setSupportActionBar(mActionBarToolbar);
}

Управление элементами Toolbar


Полный код отвечающий за управление элементами вы можете посмотреть в репозитории примера на github, а в статье мы остановимся на основных методах:
  • Метод onOptionsItemSelected(MenuItem item) — вызывается при любом нажатии по элементу внутри Toolbar
  • Метод onMenuItemActionExpand(MenuItem item) — вызывается, когда виджет поиска переходит в активное состояние. Для того, чтобы этот метод вызвался необходимо реализовать интерфейс MenuItemCompat.OnActionExpandListener, и затем задать его для MenuItem поиска:
    MenuItemCompat.setOnActionExpandListener(searchMenuItem, this);
    //this - интерфейс MenuItemCompat.OnActionExpandListener
    
  • Метод onMenuItemActionCollapse — вызывается при закрытии виджета поиска, для него также необходимо реализовать интерфейс OnActionExpandListener. Вы также можете вызвать его искусственно, например когда пользователь нажимает кнопку «назад». Пример:
    @Override
        public void onBackPressed() {
            if (mode == Mode.SEARCH) 
                searchMenuItem.collapseActionView();
    }
    

Анимирование элементов Toolbar


Для создания анимации я использовал библиотеку AndroidViewAnimations. Эта библиотека умеет анимировать любые объекты класса View или объекты классов, которые наследуются от View. Основная проблема с которой мы сталкиваемся когда хотим создать анимацию для элементов Toolbar это отсутствие объекта класса View. У нас есть только MenuItem элемента, который мы хотим анимировать.

Если мы будем работать с MenuItem, который мы создали сами, к примеру иконкой поиска, то получить его View довольно легко:
MenuItem searchMenuItem = menu.findItem(R.id.action_search);
View searchItemView = findViewById(searchMenuItem.getItemId()); 

Всё усложняется, когда мы хотим получить View системного элемента Toolbar, к примеру стрелки «назад», которая становиться видна в режиме поиска (см. скриншот режима поиск). Поскольку мы не знаем id этой стрелки, нам придётся использовать Field, который позволяет нам получать динамический доступ к любому члену класса. В нашем случаи стрелка «назад» является членом класса Toolbar, но перед тем как мы начнём нам понадобиться узнать имя стрелки внутри класса. Идём в исходный код Android, открываем класс Toolbar и находим нашу стрелку на 100-й строчке под именем «mNavButtonView». Пример кода, в котором мы получаем View стрелки и анимируем его:
// Получение View 
ImageButton  btnToolbarButton = null;
 try {
            Toolbar mActionBarToolbar = (Toolbar) findViewById(R.id.toolbar_actionbar);
            Field fNavBtn = mActionBarToolbar.getClass().getDeclaredField("mNavButtonView");
            fNavBtn.setAccessible(true);
            btnToolbarButton = (ImageButton) fNavBtn .get(mActionBarToolbar);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
//Анимация
if(btnToolbarButton!=null)
   YoYo.with(Techniques.FlipInX).duration(850).playOn(btnToolbarButton);

Заключение


В данной статье мы рассмотрели процесс создания динамического Toolbar и методы работы с его элементами. К сожалению в рамках одной статьи нельзя рассмотреть абсолютно все нюансы и разобрать весь код, поэтому если вам интересно узнать больше вы можете взглянуть на код приложения, которое мы использовали в качестве живого примера на GitHub.
  • +17
  • 125k
  • 4
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 4

    0
    А не могли бы вы пояснить в чем заключается проблема обратной совмести и конфликт между Toolbar и NavigationDrawer при использовании варианта со вьюхами? Второй момент, что то мне кажется, что мы можем получить вьюху стрелки по id android.R.id.home.
      0
      Хорошие вопросы.

      По поводу конфликта: Проблема в том, что NavigationDrawer использует ActionBarDrawerToggle для управления меню и когда мы его создаём, то просто передаём ему Toolbar, а он сам решает где отобразить иконку и даже как эта иконка будет выглядеть. Пример:

       ActionBarDrawerToggle mDrawerToggle = new ActionBarDrawerToggle(activity, mDrawerLayout,
                      (Toolbar) activity.findViewById(R.id.toolbar_actionbar),
                      R.string.sliding_menu, R.string.sliding_menu)
      

      В теории мы можем сами задать иконку + её место расположение и размеры, но на практике, в случаи кастомного layout, мне так и не удалось заставить ActionBarDrawerToggle выглядеть одинаково хорошо на Nexus 5 и Samsung Galaxy S2. У нас просто нет возможности нормально выровнять иконку с кастомным layout Toolbar-a. Возможно я что-то делал не так, но вариант с MenuItem показался мне более простым.

      Ещё одна проблема совместимости с которой я сталкивался — это телефоны на которых язык по умолчанию с письмом справа налево (в моём случае иврит). Многие прошивки стараются адаптировать приложения и переворачивают Toolbar. В случаях кастомного layout, на Android 4.0 — 4.1 это приводило к непредвидимым последствиям (всё просто перемешивалось).

      Уверен, что с этими проблемами можно справиться, поэтому и указал в статье такую возможность создания Toolbar, но мне кажется если Toolbar будет простым и можно уложиться в функционал MenuItem, лучше использовать этот вариант.

      По поводу android.R.id.home: в идеале мы можем получить вью стрелки через это id, но на практике «стрелка» это виджет и никто не гарантирует нам, что её id будет именно android.R.id.home, всё зависит action bar для конкретной версии Android и версии support library. Кроме того, мы не можем быть уверены, что android.R.id.home всегда вернём нам View, а не MenuItem или другой обьект. На stackoverflow есть много жалоб на то, что findViewById(R.android.id.home) или findViewById(item.getItemId()) возвращает null. Например, хорошее объяснение проблемы есть в этом вопросе: stackoverflow

      Вариант с Field видится мне более надёжным, если у класса Toolbar есть член с именем «mNavButtonView» мы его всегда получим.
      +1
      Теперь зададим Toolbar, как ActioanBar, это обеспечит нам обратную совместимость с предыдущими версиями Android (API Level < 21).
      Это не так, Toolbar будет работать с любой версией андроида. Запихивать его в Actionbar нужно тогда, когда разработчик ленится переносить логику по работе с Actionbar в новый API, или когда проект имеет много скринов и переход будет трудозатратен.
      По новым гайдам NavigationDrawer закрывает Header, потому нету смысла более иметь ActionBarDrawerToggle — обходитесь без него.
        0
        Спасибо за комментарий, попробую больше поэкспериментировать с Toolbar не задавая его как ActioanBar и проверю как это будет работать.

      Only users with full accounts can post comments. Log in, please.