Делаем свой почти Extended Floating Action Button

в 13:07, , рубрики: floating action button, material design, дизайн мобильных приложений, Разработка под android

Всем привет.

Не прошло и полгода как работает мое приложение, в котором Floating Action Button меню было реализовано сторонней библиотекой.

В какой-то момент захотелось сделать его более приятным и удобным.

Было

Делаем свой почти Extended Floating Action Button - 1

Стало

Делаем свой почти Extended Floating Action Button - 2

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

История создания приложения здесь

Реализация

1. Разметка подменю кнопки

fab_layout.xml

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

    <android.support.v7.widget.CardView
        android:id="@+id/fab_spending"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:visibility="invisible"
        cardView:cardBackgroundColor="@color/red"
        cardView:cardCornerRadius="12dp"
        cardView:cardElevation="8dp">

        <TextView
            style="@style/White16"
            android:layout_width="120dp"
            android:layout_height="36dp"
            android:drawableRight="@drawable/remove"
            android:drawablePadding="6dp"
            android:gravity="center"
            android:padding="4dp"
            android:text="@string/spending"/>
    </android.support.v7.widget.CardView>

    <android.support.v7.widget.CardView
        android:id="@+id/fab_income"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:visibility="invisible"
        cardView:cardBackgroundColor="@color/green"
        cardView:cardCornerRadius="12dp"
        cardView:cardElevation="8dp">

        <TextView
            style="@style/White16"
            android:layout_width="120dp"
            android:layout_height="36dp"
            android:drawableRight="@drawable/add"
            android:drawablePadding="6dp"
            android:gravity="center"
            android:text="@string/income"/>
    </android.support.v7.widget.CardView>

    <android.support.v7.widget.CardView
        android:id="@+id/fab_transfer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:visibility="invisible"
        cardView:cardBackgroundColor="@color/gunmetal"
        cardView:cardCornerRadius="12dp"
        cardView:cardElevation="8dp">

        <TextView
            style="@style/White16"
            android:layout_width="120dp"
            android:layout_height="36dp"
            android:drawableRight="@drawable/autorenew"
            android:drawablePadding="6dp"
            android:gravity="center"
            android:text="@string/transfer"/>
    </android.support.v7.widget.CardView>

</FrameLayout>

2.анимация

2.1 появление


нижнего подменю

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

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

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

</set>
среднего 

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

    <!--Move-->
    <translate
        android:duration="300"
        android:fromXDelta="75%"
        android:fromYDelta="210%"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="0%"
        android:toYDelta="0%"></translate>

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

верхнее 

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

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

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

</set>

2.1 исчезание

нижнего подменю

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

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

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

</set>

среднее

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

    <!--Move-->
    <translate
        android:duration="300"
        android:fromXDelta="-75%"
        android:fromYDelta="-210%"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="0%"
        android:toYDelta="0%"></translate>

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

</set>

верхнее

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

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

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

</set>

3. Разметка экрана

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ua.karelov.beans.ui.custom.MyRelativeLayout
        android:id="@+id/l_root"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">


            <android.support.v7.widget.RecyclerView
                android:id="@+id/rv_transactions"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:clickable="true"
                android:fadingEdgeLength="20dp"
                android:focusable="true"
                android:requiresFadingEdge="vertical"/>
        
    </ua.karelov.beans.ui.custom.MyRelativeLayout>

    <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_marginRight="16dp"
        android:layout_marginBottom="46dp"
        android:rotation="45"
        android:src="@drawable/close"
        app:layout_behavior=".ui.custom.FABScroll"
        />

    <include
        layout="@layout/fab_layout"
        android:layout_width="match_parent"
        android:layout_height="500dp"
        android:layout_gravity="bottom|end"
        android:layout_marginBottom="46dp"/>


</android.support.design.widget.CoordinatorLayout>

4. FAB scroll behavior

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;


public class FABScroll extends FloatingActionButton.Behavior {


    public FABScroll(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;
    }

}

5. Наследуемся от RelativeLayout для обработки нажатия по свободному месту, чтобы закрыть меню

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.RelativeLayout;

public class MyRelativeLayout extends RelativeLayout {
    private boolean layoutClickable = true;

    public void setLayoutClickable(boolean layoutClickable) {
        this.layoutClickable = layoutClickable;
    }

    public boolean isLayoutClickable() {
        return layoutClickable;
    }

    public MyRelativeLayout(Context context) {
        super(context);
    }

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

    public MyRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

      @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // true if you do not want the children to be clickable.
        return !layoutClickable;
    }
}

6. Соединяем все во фрагменте



private static final double transitionSmall = 0.25;
    private static final double transitionMediumX = 0.75;
    private static final double transitionMediumY = 2.1;
    private static final double transitionBigY = 4;

    @BindView(R.id.fab)
    FloatingActionButton fab;
    @BindView(R.id.fab_spending)
    CardView fabSpending;
    @BindView(R.id.fab_income)
    CardView fabIncome;
    @BindView(R.id.fab_transfer)
    CardView fabTransfer;

    //Animations
    Animation fadeIn;
    Animation fadeOut;
    Animation showFabSpending;
    Animation hideFabSpending;
    Animation showFabIncome;
    Animation hideFabIncome;
    Animation showFabTransfer;
    Animation hideFabTransfer;

   @Override
    protected void initView() {
        super.initView();

        //Animations
        fadeIn = AnimationUtils.loadAnimation(getActivity(), R.anim.fade_in);
        fadeOut = AnimationUtils.loadAnimation(getActivity(), R.anim.fade_out);
        showFabSpending = AnimationUtils.loadAnimation(getActivity(), R.anim.fab1_show);
        hideFabSpending = AnimationUtils.loadAnimation(getActivity(), R.anim.fab1_hide);
        showFabIncome = AnimationUtils.loadAnimation(getActivity(), R.anim.fab2_show);
        hideFabIncome = AnimationUtils.loadAnimation(getActivity(), R.anim.fab2_hide);
        showFabTransfer = AnimationUtils.loadAnimation(getActivity(), R.anim.fab3_show);
        hideFabTransfer = AnimationUtils.loadAnimation(getActivity(), R.anim.fab3_hide);

    }

    @OnTouch(R.id.l_root)
    public boolean clickRoot() {
        if(!lRoot.isLayoutClickable()) {
            //Close FAB menu
            hideFAB();
        }
        return true;
    }

    private void expandFAB() {
        ViewCompat.animate(fab).
                rotation(-90).
                withLayer().
                setDuration(300).
                start();
        lRoot.setLayoutClickable(false);

        lRoot.startAnimation(fadeOut);
        //Floating Action Button 1
        FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) fabSpending.getLayoutParams();
        layoutParams.rightMargin += (int) (fabSpending.getWidth() * (1 + transitionSmall));
        layoutParams.bottomMargin += (int) (fabSpending.getHeight() * transitionSmall);
        fabSpending.setLayoutParams(layoutParams);
        fabSpending.startAnimation(showFabSpending);
        fabSpending.setClickable(true);

        //Floating Action Button 2
        FrameLayout.LayoutParams layoutParams2 = (FrameLayout.LayoutParams) fabIncome.getLayoutParams();
        layoutParams2.rightMargin += (int) (fabIncome.getWidth() * transitionMediumX);
        layoutParams2.bottomMargin += (int) (fabIncome.getHeight() * transitionMediumY);
        fabIncome.setLayoutParams(layoutParams2);
        fabIncome.startAnimation(showFabIncome);
        fabIncome.setClickable(true);

        //Floating Action Button 3
        FrameLayout.LayoutParams layoutParams3 = (FrameLayout.LayoutParams) fabTransfer.getLayoutParams();
        layoutParams3.rightMargin += (int) (fabTransfer.getWidth() * transitionSmall);
        layoutParams3.bottomMargin += (int) (fabTransfer.getHeight() * transitionBigY);
        fabTransfer.setLayoutParams(layoutParams3);
        fabTransfer.startAnimation(showFabTransfer);
        fabTransfer.setClickable(true);
    }


    private void hideFAB() {
        ViewCompat.animate(fab).
                rotation(45f).
                withLayer().
                setDuration(300).
                start();
        lRoot.setLayoutClickable(true);
       
        lRoot.startAnimation(fadeIn);
        //Floating Action Button 1
        FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) fabSpending.getLayoutParams();
        layoutParams.rightMargin -= (int) (fabSpending.getWidth() * (1 + transitionSmall));
        layoutParams.bottomMargin -= (int) (fabSpending.getHeight() * transitionSmall);
        fabSpending.setLayoutParams(layoutParams);
        fabSpending.startAnimation(hideFabSpending);
        fabSpending.setClickable(false);

        //Floating Action Button 2
        FrameLayout.LayoutParams layoutParams2 = (FrameLayout.LayoutParams) fabIncome.getLayoutParams();
        layoutParams2.rightMargin -= (int) (fabIncome.getWidth() * transitionMediumX);
        layoutParams2.bottomMargin -= (int) (fabIncome.getHeight() * transitionMediumY);
        fabIncome.setLayoutParams(layoutParams2);
        fabIncome.startAnimation(hideFabIncome);
        fabIncome.setClickable(false);

        //Floating Action Button 3
        FrameLayout.LayoutParams layoutParams3 = (FrameLayout.LayoutParams) fabTransfer.getLayoutParams();
        layoutParams3.rightMargin -= (int) (fabTransfer.getWidth() * transitionSmall);
        layoutParams3.bottomMargin -= (int) (fabTransfer.getHeight() * transitionBigY);
        fabTransfer.setLayoutParams(layoutParams3);
        fabTransfer.startAnimation(hideFabTransfer);
        fabTransfer.setClickable(false);
    }

    private void initMenu() {
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(lRoot.isLayoutClickable()) {
                    //Display FAB menu
                    expandFAB();
                } else {
                    //Close FAB menu
                    hideFAB();
                }
            }
        });

    }

// выполняем когда экран обновился (если записей будет мало или не будет то нужно вернуть кнопку)
fab.animate().translationY(0).setInterpolator(new LinearInterpolator()).start();

Автор: karelov_m

Источник

* - обязательные к заполнению поля