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