С тех пор как был разработан дизайн материалов, кнопка «Плавающее действие» (FAB) стала одним из самых простых компонентов для реализации, быстро завоевав популярность среди дизайнеров и разработчиков.
В этом уроке я покажу вам, как сделать ваши приложения FAB интерактивными и как создавать свои собственные анимации. Но давайте начнем с простого, добавив кнопку с плавающим действием в проект Android.
Кнопка с плавающим действием выглядит примерно так в файле макета и генерируется автоматически, если создать проект Android Studio с пустым действием :
<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" />
Плавающая кнопка действия
Плавающие кнопки действий могут быть одного из двух размеров. По умолчанию (56dp) и мини (40dp). Для дальнейшего обсуждения принципов разработки с использованием FAB, я рекомендую вам прочитать официальные рекомендации Google .
В большинстве последних приложений для Android FAB реагирует на прокрутку списка элементов и, на мой взгляд, должен быть скрыт при прокрутке. Вот что я имею в виду:
Чтобы показать эту анимацию, я создал recyclerView
чтобы FAB мог реагировать на прокрутку.
Есть много библиотек, которые могут помочь достичь этого в 1 или 2 строках кода, но для тех, кто интересуется, вот пример:
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
таким образом, чтобы отображаемые Snackbars
не покрывали их. Но в нашем случае этот класс расширен, чтобы мы могли реализовать свое собственное поведение.
Давайте подробнее рассмотрим этот класс поведения. onStartNestedScroll()
что всякий раз, когда запускается прокрутка, метод onStartNestedScroll()
возвращает true
если прокрутка вертикальная, и оттуда метод onNestedScroll()
будет скрывать или показывать onNestedScroll()
кнопку действия в зависимости от текущего состояния видимости.
Конструктор этого класса является важной частью поведения этого представления, делая его надувным из файла XML
public FAB_Hide_on_Scroll(Context context, AttributeSet attrs) { super(); }
Чтобы использовать это поведение, добавьте атрибут layout_behavior
к layout_behavior
с плавающим действием. Атрибут содержит имя пакета плюс имя класса в конце или, иначе говоря, точное местоположение этого класса в проекте. В моем случае это выглядит так:
app:layout_behavior="com.valdio.valdioveliu.floatingactionbuttonproject.Scrolling_Floating_Action_Button.FAB_Hide_on_Scroll"
Эта анимация выглядит круто, но может быть и лучше. Лично я предпочитаю вывод FAB за пределы экрана, пока я прокручиваю контент приложений, это более реалистично. Вот что я имею в виду:
Применяется та же логика, что и ранее, меняется только то, как скрывается FAB.
Анимация проста. FAB смещается с экрана по вертикали с помощью LinearInterpolator . FAB перемещает расстояние вниз, рассчитанное по его высоте плюс нижнее поле, чтобы полностью убрать его с экрана, и перемещается обратно в исходное положение при прокрутке вверх.
Если вы внимательно посмотрите на код, я удалил View.VISIBLE
и View.GONE
проверяет в операторах if
потому что представление в этом случае не скрывается, а просто плывет за пределами экрана.
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; } }
Создание меню из кнопок с плавающим действием
Я видел много приложений для Android, которые создают впечатляющие меню с плавающей кнопкой действий, которые выглядят и работают хорошо. Вот пример:
Теперь у вас есть представление о том, что мы делаем, давайте начнем строить.
Первым шагом в создании этого меню является макет, содержащий 3 маленькие кнопки.
Все маленькие кнопки невидимы и расположены в нижней части макета, под основным 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>
Включите этот макет в макет действия под основным FAB.
<include layout="@layout/fab_layout" />
Теперь, когда макет установлен, следующим шагом является создание анимации, чтобы показать и скрыть каждый из маленьких FAB.
Внимание!
При создании этих анимаций я столкнулся с проблемой сенсорных событий и маленьких FAB. Когда анимация заканчивается, фактическая позиция маленьких FAB не изменяется, только вид отображается в новой позиции, поэтому вы не можете фактически выполнять сенсорные события в правильной позиции. Чтобы решить эту проблему, я установил параметры макета каждого FAB на новое место, а затем выполнил анимацию перемещения представления на новую позицию.
В оставшейся части этого урока я покажу процесс анимации одного из маленьких FAB. Процесс аналогичен для остальных, но с другими параметрами перемещения.
Отображение меню кнопок с плавающим действием
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);
Процесс сокрытия является обратной к предыдущей анимации.
Анимации, используемые на этом FAB:
//Animations 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, отвечающий за перемещение представления, коэффициент, с помощью которого я переместил FAB (170% и 25%), соответствует факторам с полями, которые добавляются и вычитаются в коде Java.
Тот же процесс применяется и к другим FAB, но с коэффициентами перемещения (150% и 150%) fab2
и (25% и 170%) fab3
.
Окончательный проект выглядит так:
Новая круговая анимация
Если вы хотите сделать какую-то особенную анимацию с помощью FAB, вы можете использовать класс ViewAnimationUtils
для ViewAnimationUtils
анимации в представлениях.
Остальная часть этой статьи будет посвящена конкретно этому классу и тому, как создавать с его помощью анимации. К сожалению, этот класс доступен только для API версии 21 (LOLLIPOP) и выше.
Создать новую активность
Поскольку оставшаяся часть кода в этой статье отделена от предыдущего примера, я использовал новую активность. Если вы решили продолжить, создайте новое пустое действие с именем RevealActivity
. Убедитесь, что в файле макета этого действия есть кнопка с плавающим действием, так как мы будем использовать ее для запуска нашей анимации. В моем примере у этого FAB есть android:id="@+id/fab"
.
Построить открытый интерфейс
Чтобы показать анимацию в представлении, вам понадобится макет, который будет отображаться после выполнения анимации.
Макет зависит от вида, который вы хотели бы показать в приложении, но для простоты я создал пример макета для «animate-show».
В папке раскладок создайте новый файл fab_reveal_layout.xml
и вставьте следующий код.
<?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" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/fabContainerLayout" android:layout_gravity="center_vertical|center_horizontal" android:background="@color/colorPrimary" android:gravity="center" android:visibility="gone" android:orientation="horizontal"> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content"> <android.support.design.widget.FloatingActionButton android:id="@+id/f2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/fab_margin" android:layout_marginRight="@dimen/fab_margin" android:src="@android:drawable/ic_dialog_email" app:backgroundTint="@color/colorFAB" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:elevation="8dp" android:text="Fab2" android:textColor="#fff" /> </FrameLayout> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/fab_margin" android:layout_marginTop="@dimen/fab_margin"> <android.support.design.widget.FloatingActionButton xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/f1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/fab_margin" android:layout_marginRight="@dimen/fab_margin" android:elevation="0dp" android:src="@android:drawable/ic_dialog_map" app:backgroundTint="@color/colorFAB" app:borderWidth="0dp" app:fabSize="normal" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:elevation="8dp" android:text="Fab1" android:textColor="#fff" /> </FrameLayout> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/fab_margin" android:layout_marginTop="@dimen/fab_margin"> <android.support.design.widget.FloatingActionButton android:id="@+id/f4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/fab_margin" android:layout_marginRight="@dimen/fab_margin" android:src="@android:drawable/ic_dialog_alert" app:backgroundTint="@color/colorFAB" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:elevation="8dp" android:text="Fab4" android:textColor="#fff" /> </FrameLayout> </LinearLayout> <FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content"> <android.support.design.widget.FloatingActionButton android:id="@+id/f3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/fab_margin" android:layout_marginRight="@dimen/fab_margin" android:src="@android:drawable/ic_dialog_dialer" app:backgroundTint="@color/colorFAB" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="right" android:elevation="8dp" android:text="Fab3" android:textColor="#fff" /> </FrameLayout> </LinearLayout>
На самом деле, контейнер LinearLayout
в этом файле имеет атрибут visibility="gone"
поэтому ничего не будет видно, когда вы вставите этот код в ваш файл. Чтобы проверить этот макет, удалите атрибут видимости из LinearLayout
или взгляните на следующее изображение.
После создания этого файла включите его в файл макета упражнения.
activity_reveal.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" //...> //... <include layout="@layout/content_reveal" /> <include layout="@layout/fab_reveal_layout" /> <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="@drawable/ic_add" /> </android.support.design.widget.CoordinatorLayout>
Настройте RevealActivity
После включения потрясающего макета в макет Activity нам нужно настроить анимацию CircularReveal
.
В классе RevealActivity
создайте следующие глобальные экземпляры кнопки с плавающим действием, boolean
для отслеживания состояния анимации и fab_reveal_layout
контейнера fab_reveal_layout
.
private LinearLayout fabContainer; private FloatingActionButton fab; private boolean fabMenuOpen = false;
Затем в RevealActivity
onCreate()
найдите ссылки на представление FAB и добавьте прослушиватель onCreate()
. Я только инициировал FAB Activity, потому что это все, что нужно для запуска анимации.
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //... fabContainer = (LinearLayout) findViewById(R.id.fabContainerLayout); fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { toggleFabMenu(); } }); }
Функция toggleFabMenu()
используется для создания и toggleFabMenu()
анимации раскрытия. Просто добавьте следующий код в класс RevealActivity
и я подробно опишу, что он делает.
@TargetApi(Build.VERSION_CODES.LOLLIPOP) private void toggleFabMenu() { if (!fabMenuOpen) { fab.setImageResource(R.drawable.ic_close); int centerX = fabContainer.getWidth() / 2; int centerY = fabContainer.getHeight() / 2; int startRadius = 0; int endRadius = (int) Math.hypot(fabContainer.getWidth(), fabContainer.getHeight()) / 2; fabContainer.setVisibility(View.VISIBLE); ViewAnimationUtils .createCircularReveal( fabContainer, centerX, centerY, startRadius, endRadius ) .setDuration(1000) .start(); } else { fab.setImageResource(R.drawable.ic_add); int centerX = fabContainer.getWidth() / 2; int centerY = fabContainer.getHeight() / 2; int startRadius = (int) Math.hypot(fabContainer.getWidth(), fabContainer.getHeight()) / 2; int endRadius = 0; Animator animator = ViewAnimationUtils .createCircularReveal( fabContainer, centerX, centerY, startRadius, endRadius ); animator.setDuration(1000); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { fabContainer.setVisibility(View.GONE); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animator.start(); } fabMenuOpen = !fabMenuOpen; }
Как я упоминал ранее, метод createCircularReveal()
работает только в LOLLIPOP и более новой версии Android, поэтому эта функция имеет TargetApi
для версии сборки LOLLIPOP. Это означает, что эта функция не будет вызываться при запуске в устройстве до LOLLIPOP.
Первое, что делает эта функция, это проверяет, виден ли вид анимации, используя Boolean
значения fabMenuOpen
.
В этой функции я изменяю изображение fab
объекта с помощью setImageResource
(). Если вы добавите эту функцию в свой класс, убедитесь, что вы добавили отсутствующие изображения в папку drawable
, или просто закомментируйте эти строки кода из функции.
Если вы запустите проект сейчас, он будет работать должным образом, как показано в следующем GIF, но если вы заинтересованы в том, чтобы узнать, как работает createCircularReveal()
обратитесь к следующему разделу этой статьи.
Метод ViewAnimationUtils.createCircularReveal()
Метод createCircularReveal()
используется для настройки анимации. Он принимает пять параметров, на основании которых он создает анимацию в представлении.
Первый параметр — это ссылка на вид, который будет показан кружком анимации.
Следующие два параметра — это координаты X и Y экрана, с которого начнется анимация. Эти координаты относятся к представлению, которое открывается анимированным.
Так как анимация представляет собой круг, необходимо указать радиус круга, который она рисует, поэтому следующие два параметра — начальный и конечный радиус анимации.
Что касается примера из этой статьи, как показано в GIF, анимация начнется в центре представления с начальным радиусом «0», а конечный радиус рассчитывается методом Math.hypot()
. Чтобы изменить анимацию, просто измените значения начального и конечного радиуса друг с другом.
Сложная часть анимации с круговым раскрытием заключается в поиске скоординированного начала анимации, связанного с представлением, которое отображается анимированным.
Например, я вычислил координаты X и Y анимации, соответственно ширину / 2 и высоту / 2 вида, чтобы найти центр вида для запуска анимации.
Посмотрите на следующее изображение, чтобы узнать, как определить координаты вашей собственной анимации.
Что дальше?
Надеюсь, я дал вам представление о том, как анимировать Floating Action Buttons в вашем собственном проекте. Отсюда вы должны прочитать Android- ресурсы по анимации и создавать собственные анимации для своих приложений. Вы можете найти окончательный код этого проекта на GitHub, и я приветствую любые вопросы или комментарии, которые могут у вас возникнуть.