Floating Action Button attributes, usage, and parsing and resolving the problem of hiding and not displaying more than sdk25

Keywords: Android xml SDK Attribute

Floating Action Button, hereinafter referred to as fab, today we will talk about some of its attributes and usage, and resolve the problem that Floating Action Button above sdk25 only hides and does not show.

This development environment is based on sdk25.

Introduce design packages before using them

compile 'com.android.support:design:25.3.1'

xml attribute

<android.support.design.widget.FloatingActionButton
        android:id="@+id/contact_fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher"
        app:backgroundTint="@color/gray"
        app:backgroundTintMode="multiply"
        app:borderWidth="0dp"
        app:elevation="@dimen/activity_horizontal_margin"
        app:fabSize="auto"
        app:pressedTranslationZ="@dimen/activity_horizontal_margin"
        app:rippleColor="@color/gray"
        app:useCompatPadding="true" />
  • app:backgroundTint button background color, not set, default color Accent in theme
  • app:backgroundTintMode button background color mode, when setting screen, it is a little different from other modes, the difference is that the color changes, the others remain unchanged, specific and unknown, can be ignored.
  • app:borderWidth If this property is not set to 0dp, FAB will appear square on 4.1 sdk, and the SDK after 5.0 has no shadow effect. So set it to borderWidth="0dp"
  • app:elevation default shadow size.
  • app:fabSize sets the size, which has two values, normal and mini, corresponding to 56 DP and 40 dp, respectively.
  • Shadow Size in the Pressed TranslationZ Button Pressed State
  • app:rippleColor sets the background color when clicked
  • app:useCompatPadding Use Compatible Fill Size

usage

It can be used with Floating Action Menu or Coordinator Layout. Here's just Coordinator Layout for an example.

Use the top and bottom slides of recyclerView to display or hide fab, and click fab to display snackbar

layout

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/contact_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

    </android.support.v7.widget.RecyclerView>

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/contact_fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher"
        app:backgroundTint="@color/turquoise"
        app:backgroundTintMode="src_in"
        app:borderWidth="0dp"
        app:elevation="5dp"
        app:fabSize="auto"
        app:pressedTranslationZ="50dp"
        app:rippleColor="@color/gray"
        app:useCompatPadding="true"
        app:layout_anchor="@+id/contact_recyclerview"
        app:layout_anchorGravity="bottom|right|end"
        app:layout_behavior="com.voctex.ui.tablayout.other.ScrollingViewBehavior" />


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

code implementation

RecyclerView recyclerView= ((RecyclerView) findViewById(R.id.contact_recyclerview));

        recyclerView.setHasFixedSize(true);
        recyclerView.setItemAnimator(new DefaultItemAnimator());

        //Set up a vertical layout manager
        int orientation = LinearLayoutManager.VERTICAL;
        recyclerView.setLayoutManager(new LinearLayoutManager(mContext, orientation, false));

        List<String> mList=new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            mList.add("Location:"+i);
        }

        TabLayoutAdapter tabLayoutAdapter=new TabLayoutAdapter(recyclerView,mList);

        recyclerView.setAdapter(tabLayoutAdapter);


        FloatingActionButton fab= ((FloatingActionButton) findViewById(R.id.contact_fab));
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Snackbar.make(v,"floatingActionBtn",Snackbar.LENGTH_SHORT)
                        .setAction("action", new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                VtToast.s(mContext,"Fairy Sword and Chivalrous Legend");
                            }
                        }).show();
            }
        });

In the above code, just specify an attribute for fab in xml to display or hide fab when recyclerView slides up and down.

app:layout_behavior="com.voctex.ui.tablayout.other.ScrollingViewBehavior"

The value is a custom class inherited from FloatingActionButton.Behavior, rewriting the onStartNestedScroll and onNestedScroll methods. The relevant code is as follows:

package com.voctex.ui.tablayout.other;

import android.content.Context;
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;

/**
 * Created by mac_xihao on 17/7/3.
 * (~ ̄▽ ̄)~ It's a lot of fun.
 */
public class ScrollingViewBehavior extends FloatingActionButton.Behavior {

    /**
     * Because app:layout_behavior is used to define static behavior in XML.
     * A constructor must be implemented to make the layout work properly.
     * Otherwise, can not inflate Behavior subclass error messages.
     *
     * @param context
     * @param attrs
     */
    public ScrollingViewBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * Handling scrolling events in the vertical direction
     *
     * @param coordinatorLayout
     * @param child
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {
        // Ensure we react to vertical scrolling
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
                super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
                        nestedScrollAxes);
    }

    /**
     * Check the location of Y and decide whether the button animates to enter or exit
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dxConsumed
     * @param dyConsumed
     * @param dxUnconsumed
     * @param dyUnconsumed
     */
    @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);

        if (dyConsumed > 10 && child.getVisibility() == View.VISIBLE) {
            // User scrolled down and the FAB is currently visible -> hide the FAB
            //Perform hidden animations
            child.hide();
        } else if (dyConsumed < -10 && child.getVisibility() != View.VISIBLE) {
            // User scrolled up and the FAB is currently not visible -> show the FAB
            //Execute animation for display
            child.show();
        }
    }
}

In fact, it should be over here, and it is very simple to implement, but I found a pit in the test, that is, RecylerView can only hide, but not display fab when it is sliding.

Resolve and resolve the problem of sdk25 Floating Action Button hiding only but not showing

This problem pit ah, the Internet view many people are so realized, are hidden display, I aimed at this problem Baidu, found that someone said on the Internet sdk25 above will not show fab problems, specific problems appear in the code:

Over sdk25, Coordinator Layout's onNestedScroll method adds an extra piece of code

 @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed) {
        final int childCount = getChildCount();
        boolean accepted = false;

        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            //More than sdk25 will make this judgment
            if (view.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
                accepted = true;
            }
        }

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

That is, when Coordinator Layout slides, it determines whether the child view is set to GONE, if so, it executes the next loop directly, then it does not call back onNestedScroll, and the onNestedScroll method of our custom class does not go away.

When Fab implements hide method, the default is to set fab to GONE. Let's look at hide method in fab.

/**
     * Hides the button.
     * <p>This method will animate the button hide if the view has already been laid out.</p>
     */
    public void hide() {
        hide(null);
    }

    /**
     * Hides the button.
     * <p>This method will animate the button hide if the view has already been laid out.</p>
     *
     * @param listener the listener to notify when this view is hidden
     */
    public void hide(@Nullable OnVisibilityChangedListener listener) {
        hide(listener, true);
    }

    void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
        getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser);
    }

Finally, the following code is invoked and fromUser defaults to true, which is critical

getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser);

Next, let's move on, which object does getImpl() get?

private FloatingActionButtonImpl getImpl() {
        if (mImpl == null) {
            mImpl = createImpl();
        }
        return mImpl;
    }

    private FloatingActionButtonImpl createImpl() {
        final int sdk = Build.VERSION.SDK_INT;
        if (sdk >= 21) {
            return new FloatingActionButtonLollipop(this, new ShadowDelegateImpl(),
                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
        } else if (sdk >= 14) {
            return new FloatingActionButtonIcs(this, new ShadowDelegateImpl(),
                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
        } else {
            return new FloatingActionButtonGingerbread(this, new ShadowDelegateImpl(),
                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
        }
    }

Here we test with mobile phones over 4.0 by default, so we get an example of Floating Action Button Ics, and then we go to see how its hide method is implemented.

@Override
    void hide(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) {
        if (isOrWillBeHidden()) {
            // We either are or will soon be hidden, skip the call
            return;
        }

        mView.animate().cancel();

        if (shouldAnimateVisibilityChange()) {
            mAnimState = ANIM_STATE_HIDING;

            mView.animate()
                    .scaleX(0f)
                    .scaleY(0f)
                    .alpha(0f)
                    .setDuration(SHOW_HIDE_ANIM_DURATION)
                    .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
                    .setListener(new AnimatorListenerAdapter() {
                        private boolean mCancelled;

                        @Override
                        public void onAnimationStart(Animator animation) {
                            mView.internalSetVisibility(View.VISIBLE, fromUser);
                            mCancelled = false;
                        }

                        @Override
                        public void onAnimationCancel(Animator animation) {
                            mCancelled = true;
                        }

                        @Override
                        public void onAnimationEnd(Animator animation) {
                            mAnimState = ANIM_STATE_NONE;

                            if (!mCancelled) {
                                mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE,
                                        fromUser);
                                if (listener != null) {
                                    listener.onHidden();
                                }
                            }
                        }
                    });
        } else {
            // If the view isn't laid out, or we're in the editor, don't run the animation
            mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE, fromUser);
            if (listener != null) {
                listener.onHidden();
            }
        }
    }

Looking directly at the onAnimationEnd method, the default fromUser is true, so here the fab is set directly to Gone, while the loop in Coordinator Layout's onNestedScroll method judges that the child view is Gone and jumps out directly to execute the next loop, which is very paradoxical.

So under what circumstances is fromUser false?

Because hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) method has default access and can only be invoked by classes in the same package, I searched directly for the call of fab method and found two other methods calling this method, respectively.

private boolean updateFabVisibilityForAppBarLayout(CoordinatorLayout parent,
                AppBarLayout appBarLayout, FloatingActionButton child) {
            if (!shouldUpdateVisibility(appBarLayout, child)) {
                return false;
            }

            if (mTmpRect == null) {
                mTmpRect = new Rect();
            }

            // First, let's get the visible rect of the dependency
            final Rect rect = mTmpRect;
            ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect);

            if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
                // If the anchor's bottom is below the seam, we'll animate our FAB out
                child.hide(mInternalAutoHideListener, false);
            } else {
                // Else, we'll animate our FAB back in
                child.show(mInternalAutoHideListener, false);
            }
            return true;
        }

        private boolean updateFabVisibilityForBottomSheet(View bottomSheet,
                FloatingActionButton child) {
            if (!shouldUpdateVisibility(bottomSheet, child)) {
                return false;
            }
            CoordinatorLayout.LayoutParams lp =
                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            if (bottomSheet.getTop() < child.getHeight() / 2 + lp.topMargin) {
                child.hide(mInternalAutoHideListener, false);
            } else {
                child.show(mInternalAutoHideListener, false);
            }
            return true;
        }

It can be found that hide's methods are passed into false in both classes. Moreover, from the name, it can be found that the two methods should be for appbarlayout and bottomSheet respectively. The access rights are private, so you can continue to search for the places called in the fab class and find that both methods are called in the other two methods together.

@Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
                View dependency) {
            if (dependency instanceof AppBarLayout) {
                // If we're depending on an AppBarLayout we will show/hide it automatically
                // if the FAB is anchored to the AppBarLayout
                updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child);
            } else if (isBottomSheet(dependency)) {
                updateFabVisibilityForBottomSheet(dependency, child);
            }
            return false;
        }
@Override
        public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
                int layoutDirection) {
            // First, let's make sure that the visibility of the FAB is consistent
            final List<View> dependencies = parent.getDependencies(child);
            for (int i = 0, count = dependencies.size(); i < count; i++) {
                final View dependency = dependencies.get(i);
                if (dependency instanceof AppBarLayout) {
                    if (updateFabVisibilityForAppBarLayout(
                            parent, (AppBarLayout) dependency, child)) {
                        break;
                    }
                } else if (isBottomSheet(dependency)) {
                    if (updateFabVisibilityForBottomSheet(dependency, child)) {
                        break;
                    }
                }
            }
            // Now let the CoordinatorLayout lay out the FAB
            parent.onLayoutChild(child, layoutDirection);
            // Now offset it if needed
            offsetIfNeeded(parent, child);
            return true;
        }

That is to say, only when (AppBar Layout or Bottom Sheet) and fab are brothers, and then when they slide, fab will display and hide normally. What about other layouts? How to solve this situation like Recycler View?

In fact, it's very simple, as long as you don't implement hide method, you can realize hide animation by yourself. I took the fab hide animation directly here, made some modifications, and then turned it into my own. Then I pasted the code directly.

Inheriting FloatingActionButton.Behavior, we also implemented a class ScrollAware FABBehavior with the following code

package com.voctex.ui.tablayout.other;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by mac_xihao on 17/7/3.
 * (~ ̄▽ ̄)~ It's a lot of fun.
 */
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {


    /**
     * Because app:layout_behavior is used to define static behavior in XML.
     * A constructor must be implemented to make the layout work properly.
     * Otherwise, can not inflate Behavior subclass error messages.
     *
     * @param context
     * @param attrs
     */
    public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
        super();
    }

    /**
     * Handling scrolling events in the vertical direction
     *
     * @param coordinatorLayout
     * @param child
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                                       FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {

        // Ensure we react to vertical scrolling
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
                super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
                        nestedScrollAxes);
    }

    /**
     * Check the location of Y and decide whether the button animates to enter or exit
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dxConsumed
     * @param dyConsumed
     * @param dxUnconsumed
     * @param dyUnconsumed
     */
    @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);

        if (dyConsumed > 10 && child.getVisibility() == View.VISIBLE) {
            // User scrolled down and the FAB is currently visible -> hide the FAB
            //Perform hidden animations
            hide(child);
        } else if (dyConsumed < -10 && child.getVisibility() != View.VISIBLE) {
            // User scrolled up and the FAB is currently not visible -> show the FAB
            //Execute animation for display
            show(child);
        }

    }

    /**
     * Display animation
     */
    private void show(final View view) {
        view.animate().cancel();

        // If the view isn't visible currently, we'll animate it from a single pixel
        view.setAlpha(0f);
        view.setScaleY(0f);
        view.setScaleX(0f);

        view.animate()
                .scaleX(1f)
                .scaleY(1f)
                .alpha(1f)
                .setDuration(200)
                .setInterpolator(new LinearOutSlowInInterpolator())
                .setListener(new AnimatorListenerAdapter() {

                    @Override
                    public void onAnimationStart(Animator animation) {
                        view.setVisibility(View.VISIBLE);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {

                    }
                });
    }

    /**
     * Hidden animation
     */
    private void hide(final View view) {
        view.animate().cancel();
        view.animate()
                .scaleX(0f)
                .scaleY(0f)
                .alpha(0f)
                .setDuration(200)
                .setInterpolator(new FastOutLinearInInterpolator())
                .setListener(new AnimatorListenerAdapter() {
                    private boolean mCancelled;

                    @Override
                    public void onAnimationStart(Animator animation) {
                        view.setVisibility(View.VISIBLE);
                        mCancelled = false;
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        mCancelled = true;
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (!mCancelled) {
                            view.setVisibility(View.INVISIBLE);
                        }
                    }
                });
    }
}

Then change it directly to xml

app:layout_behavior="com.voctex.ui.tablayout.other.ScrollAwareFABBehavior"

It's interesting to find that looking at the source code is a bit time-consuming.

If you want to see all the code carefully, you can directly run my project and test it.

Project address: https://github.com/voctex/Kepler

Posted by bigfunkychief on Sun, 16 Jun 2019 12:19:37 -0700