Анимация Floating Action Button в Android

Original author: Valdio Veliu
  • Translation


С момента возникновения концепции Material design одним из самых простых в реализации элементов стала плавающая кнопка действия — FAB, Floating Action Button. Этот элемент быстро обрёл широчайшую популярность среди разработчиков и дизайнеров. В этой публикации мы рассмотрим, как можно анимировать FAB и сделать её интерактивной. Но сначала разберём, как вообще добавить этот элемент в ваш проект.

FAB выглядит как цветной круг в правом нижнем углу экрана. Если в Android Studio создать новый проект Blank Activity, то в нём автоматически будет сгенерирована плавающая кнопка действия.



<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_menu_help"
    />

Floating Action Button


FAB может быть одного из двух размеров: 56 dp (по умолчанию) или 40 dp. Если вы хотите подробнее изучить принципы использования FAB в дизайне приложения, то обратите внимание на официальные гайдлайны Google.

В самых свежих Android-приложениях FAB реагирует на прокручивание списка элементов. Было бы логичнее скрывать её во время прокручивания. Вот что имеется в виду:



Для отображения этой анимации создадим recyclerView, благодаря которому FAB реагирует на прокручивание. Сегодня доступно немало библиотек, позволяющих добиться этого с помощью пары строк кода. Например:

public class FAB_Hide_on_Scroll extends FloatingActionButton.Behavior {

    public FAB_Hide_on_Scroll(Context context, AttributeSet attrs) {
        super();
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);

        //child -> Floating Action Button
        if (child.getVisibility() == View.VISIBLE && dyConsumed > 0) {
            child.hide();
        } else if (child.getVisibility() == View.GONE && dyConsumed < 0) {
            child.show();
        }
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
    }
}

Здесь использован класс FloatingActionButton.Behavior(), чья основная задача, согласно официальной документации, заключается в перемещении видов FloatingActionButton, чтобы ни один из Snackbar их не перекрывал. Но в нашем случае этот класс является расширенным, так что мы можем его использовать для реализации нужного поведения кнопки.

Что же делает данный класс? При каждой инициализации прокрутки вниз метод onStartNestedScroll() возвращает значение true. После этого метод onNestedScroll() отображает или прячет кнопку, в зависимости от её текущей видимости. Конструктор класса FloatingActionButton.Behavior() является важной частью описанного поведения вида (view) и извлекается из XML-файла.

    public FAB_Hide_on_Scroll(Context context, AttributeSet attrs) {
        super();
    }

Добавим в FAB атрибут layout_behavior, содержащий название пакета, а в конце — имя класса. Иначе говоря, в атрибуте должно быть указано точное размещение класса в проекте. Например:

app:layout_behavior="com.valdio.valdioveliu.floatingactionbuttonproject.Scrolling_Floating_Action_Button.FAB_Hide_on_Scroll"

Анимация выглядит хорошо, но можно сделать ещё лучше. Например, чтобы кнопка уходила за пределы экрана во время прокрутки — это более реалистичное поведение:



Здесь используется та же логика, что и в предыдущем варианте, за исключением способа исчезновения FAB. Анимация довольно проста. Кнопка уходит вниз с помощью LinearInterpolator. Расстояние, которое ей нужно пройти, равно высоте кнопки плюс ширина нижнего поля.

Обратите внимание, что в выражениях if отсутствуют проверки View.VISIBLE и View.GONE, поскольку в данном случае вид не скрывается, а лишь уплывает за пределы экрана.

public class FAB_Float_on_Scroll extends FloatingActionButton.Behavior {

    public FAB_Float_on_Scroll(Context context, AttributeSet attrs) {
        super();
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);

        //child -> Floating Action Button
        if (dyConsumed > 0) {
            CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            int fab_bottomMargin = layoutParams.bottomMargin;
            child.animate().translationY(child.getHeight() + fab_bottomMargin).setInterpolator(new LinearInterpolator()).start();
        } else if (dyConsumed < 0) {
            child.animate().translationY(0).setInterpolator(new LinearInterpolator()).start();
        }
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
    }
}

Меню из FAB'ов


Существует немало приложений, авторы которых создали красивые и хорошо работающие меню, состоящие из плавающих кнопок действия.



Давайте сделаем нечто подобное. Для начала создадим макет, содержащий три маленькие кнопки. Они невидимы и расположены в самом низу макета, под главной FAB. Содержимое fab_layout.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_menu_compass"
        android:visibility="invisible"
        app:backgroundTint="@color/colorFAB"
        app:fabSize="mini" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_menu_myplaces"
        android:visibility="invisible"
        app:backgroundTint="@color/colorFAB"
        app:fabSize="mini" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab_3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_menu_share"
        android:visibility="invisible"
        app:backgroundTint="@color/colorFAB"
        app:fabSize="mini" />
</FrameLayout>

Этот макет нужно включить в макет activity под главной FAB.

<include layout="@layout/fab_layout" />

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

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

Саму анимацию вы можете посмотреть в конце этой публикации. Порядок действий для всех кнопок один и тот же, различаются лишь координаты перемещения.

Отображение меню:

  FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) fab1.getLayoutParams();
  layoutParams.rightMargin += (int) (fab1.getWidth() * 1.7);
  layoutParams.bottomMargin += (int) (fab1.getHeight() * 0.25);
  fab1.setLayoutParams(layoutParams);
  fab1.startAnimation(show_fab_1);
  fab1.setClickable(true);

fab1 перемещается с помощью добавления в layoutParams полей справа и снизу, после чего инициируется анимация.

Скрывание меню:

FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) fab1.getLayoutParams();
layoutParams.rightMargin -= (int) (fab1.getWidth() * 1.7);
layoutParams.bottomMargin -= (int) (fab1.getHeight() * 0.25);
fab1.setLayoutParams(layoutParams);
fab1.startAnimation(hide_fab_1);
fab1.setClickable(false);

Процесс скрывания представляет собой обратное воспроизведение предыдущей анимации.

//Анимации одной из малых кнопок
Animation show_fab_1 = AnimationUtils.loadAnimation(getApplication(), R.anim.fab1_show);
Animation hide_fab_1 = AnimationUtils.loadAnimation(getApplication(), R.anim.fab1_hide);

Теперь создадим в папке res/anim/ файлы для каждой из анимаций. Делается это просто, но если у вас возникнут затруднения, то можете обратиться к документации.

Содержимое fab1_show.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">

    <!-- Rotate -->
    <rotate
        android:duration="500"
        android:fromDegrees="30"
        android:interpolator="@android:anim/linear_interpolator"
        android:pivotX="50%"
        android:pivotY="50%"
        android:repeatCount="4"
        android:repeatMode="reverse"
        android:toDegrees="0"></rotate>

    <!--Move-->
    <translate
        android:duration="1000"
        android:fromXDelta="170%"
        android:fromYDelta="25%"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="0%"
        android:toYDelta="0%"></translate>

    <!--Fade In-->
    <alpha
        android:duration="2000"
        android:fromAlpha="0.0"
        android:interpolator="@android:anim/decelerate_interpolator"
        android:toAlpha="1.0"></alpha>

</set>

Содержимое fab1_hide.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">

    <!--Move-->
    <translate
        android:duration="1000"
        android:fromXDelta="-170%"
        android:fromYDelta="-25%"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="0%"
        android:toYDelta="0%"></translate>

    <!--Fade Out-->
    <alpha
        android:duration="2000"
        android:fromAlpha="1.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="0.0"></alpha>

</set>

Если вы посмотрите на тэг перевода (translate tag), отвечающий за движение вида, то увидите, что коэффициент перемещения (170% и 25%) соответствует коэффициентам, использованным при добавлении полей и извлечённым в Java-код.

Все вышеописанные шаги мы повторяем и для остальных малых кнопок. Различаются только коэффициенты перемещения: fab2 — 150% и 150%, fab3 — 25% и 170%.

Результат наших усилий:



Это не конец


Надеюсь, эта статья помогла вам понять, как можно анимировать Floating Action Button в вашем проекте. Дальше вы можете изучить ресурсы, посвящённые анимации в Android, и начать создавать собственные виды анимаций. Весь код описанного в этой публикации проекта доступен на GitHub.
NIX Solutions
133.38
Company
Share post

Comments 11

    +3
    > Этот элемент быстро обрёл широчайшую популярность среди разработчиков и дизайнеров
    Правильно вы написали — разработчиков и дизайнеров. Юзерам она нафиг не нужна в 90% случаев, а её пихают туда часто «чтобы было, ибо надо по спеке». Для начала надо очень хорошо обдумать функцию, которая активируется по этой кнопке, а не пихать просто так. Если приложение, или экран решает одну конкретную проблему, то можно (не нужно) её сделать, а если там несколько функций, пусть даже с одной чуть более используемой, то лучше пользоваться ActionBar.
      0
      Спецификация (скорее даже пособие по стилю) не гласит располагать этот элемент на всех страницах. Скорее это проблемы отдельных приложений и их разработчиков. +, там есть различные вариации, в том числе обыгрывается ситуация с несколькими значимыми стилями.
        0
        Я это понимаю, только разработчики клеют эту кнопку везде к месту и не к месту.
          0
          Как пример — приложение авито. Что бы открыть фильтр поиска, нужно немного прокрутить список вверх объявлений. Лично меня это раздражает. Это можно было сделать обычным контекстом, рядом с иконкой поиска.
      +1
      Тянуться к экшен бару большим пальцем на текущих гиганстких смартах — та еще мука. Нет уж, пусть любой наиболее востребованный функционал будет снизу. И actionbutton/menu наиболее удобный вариант на текущий момент.
        0
        Терпеть не могу эту кнопку. И вообще расположение контролов в андроиде.
        Кнопка расположена в правильном месте, но в 99.97% случаев она там просто отъедает место на экране.
          0
          в 99.97% случаев она там просто отъедает место на экране
          и это бесит. К примеру, у меня в андроид-приложении хабра комменты закрывает эта кнопка

          кликабельно
          image
            0
            Нарушение гайдлайна ж:

            www.google.com/design/spec/components/buttons-floating-action-button.html#buttons-floating-action-button-behavior

            Lists underneath floating action buttons should have enough padding beneath them so their content isn’t blocked by the button.
            Списки под FAB должны иметь достаточный отступ снизу, чтобы их содержимое не закрывалось кнопкой.

            Да и скрывать при скроле можно.
              0
              Как символично она закрывает )
              А по существу, где-то читал что при использовании FAB и списков надо делать еще один элемент в конце, который будет пустым (либо несколько, главное что бы по высоте было как эта кнопка). как например сделано в приложении GMail. В таком случае она ничего не перекрывает.
            0
            Функционал забавный, немного поправил для своих нужд.
            Есть вопрос: судя по всему Вы пример создавали для api 21+. Как быть например с api 14-20? В зависимости от Api создавать расчет margin'ов и проценты для fromXDelta и fromYDelta в анимациях?
            Там все это выглядит неприятно — кнопки после анимации оказываются практически посередине экрана =) Грешил на размер экрана, но на маленьком устройстве с api 14 выглядит так же.
              0
              Последнее время все больше замечаю, что ios TabBar — классная штука. Если у вас 3-5 экранов, то лучше сделать навигацию не пихая в Humburger menu, а в табы внизу.
              Даже у гугла в последнем гайдлайне по Material Design добавили свой крутой аналог. Он по мне так сделать очень даже классно.

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