FloatingActionButton属性、用法,以及解析并解决sdk25以上只隐藏不显示的问题

来源(CSDN博客)

From: https://blog.csdn.net/chen_xi_hao/article/details/74347023

FloatingActionButton属性、用法,以及解析并解决sdk25以上只隐藏不显示的问题

标签: fab material-design behavior

2017年07月04日 17:38:52 5918人阅读

分类:material-design(2)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/chen_xi_hao/article/details/74347023

目录

悬浮按钮(FloatingActionButton),在下文简称fab,今天我们来讲讲它的一些属性与用法,以及解析并解决sdk25以上FloatingActionButton只隐藏不显示的问题。

先展示一下动态图



这里写图片描述

本次开发环境基于sdk25.

使用之前要先引入design包

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

xml属性

<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 按钮的背景颜色,不设置,默认使用theme中colorAccent的颜色

app:backgroundTintMode 按钮背景颜色的模式,在设置screen的时候就跟其他模式有点区别,区别在颜色变了,其他不变,具体不详,可忽略

app:borderWidth 该属性如果不设置0dp,那么在4.1的sdk上FAB会显示为正方形,而且在5.0以后的sdk没有阴影效果。所以设置为borderWidth=”0dp”

app:elevation 默认状态下阴影大小。

app:fabSize 设置大小,该属性有两个值,分别为normal和mini,对应的大小分别为56dp和40dp

app:pressedTranslationZ 按钮按下去的状态下的阴影大小

app:rippleColor 设置点击时的背景颜色

app:useCompatPadding 是否使用兼容的填充大小

用法

可与FloatingActionMenu或者CoordinatorLayout一起使用。在这里只拿CoordinatorLayout来做示例。

利用recyclerView的上下滑动来使fab显示或隐藏,点击fab显示snackbar

布局

<?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>


代码实现

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

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

//设置一个垂直方向的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("位置为:"+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,"仙剑奇侠传");
					}
			}).show();
		}
});


在以上代码中,只需要在xml中为fab指定一个属性就可以实现recyclerView在上下滑动时fab的显示或隐藏了。

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

而值就是自定义的一个类,继承于FloatingActionButton.Behavior,重写onStartNestedScroll和onNestedScroll这两个方法,相关代码如下:

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.
 * (~ ̄▽ ̄)~ 嘛哩嘛哩哄
 */
public class ScrollingViewBehavior extends FloatingActionButton.Behavior {

	/**
	 * 因为是在XML中使用app:layout_behavior定义静态的这种行为,
	 * 必须实现一个构造函数使布局的效果能够正常工作。
	 * 否则 Could not inflate Behavior subclass error messages.
	 *
	 * @param context
	 * @param attrs
	 */
	public ScrollingViewBehavior(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	/**
	 * 处理垂直方向上的滚动事件
	 *
	 * @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);
	}

	/**
	 * 检查Y的位置,并决定按钮是否动画进入或退出
	 *
	 * @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
			//执行隐藏的动画
			child.hide();
		} else if (dyConsumed < -10 && child.getVisibility() != View.VISIBLE) {
			// User scrolled up and the FAB is currently not visible -> show the FAB
			//执行显示的动画
			child.show();
		}
	}
}


其实,到这里是应该结束的了,实现起来是很简单的,但是我在测试的时候却发现了一个比较坑的问题,就是RecylerView在滑动的时候,只能隐藏,却不显示fab。

解析并解决sdk25 FloatingActionButton只隐藏不显示的问题

这个问题坑呀,上网查看了不少人都是这么实现的,都是可以隐藏显示的,我就针对这个问题百度一下,发现有人在网上说sdk25以上的会出现不显示fab的问题,具体问题出现在了一下代码中:

sdk25以上,CoordinatorLayout的onNestedScroll方法会多出一段代码

@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);
		//sdk25以上会多出这个判断
		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);
	}
}


也就是CoordinatorLayout在滑动的时候,判断子view是否被设置为GONE,如果是,直接执行下一次循环,然后就不回调onNestedScroll,而我们自定义的那个类的onNestedScroll方法就不走了。

而在fab执行hide方法的时候,默认是把fab设置为GONE的,我们来看看fab中的hide方法

/**
 * 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);
}


最后是调用了以下代码,并且fromUser默认为true,这个值很关键

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

接下来我们继续走下去,getImpl()是获得哪个对象

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);
	}
}


在这里我们默认用4.0以上的手机进行测试,所以获得的对象是FloatingActionButtonIcs的实例,然后就进入看看它的hide方法是如何实现的。

@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();
		}
	}
}


直接看onAnimationEnd方法里面,默认fromUser为true,所以在这里fab是直接被设置为了Gone,而CoordinatorLayout的onNestedScroll方法里的循环又判断子view为Gone的时候直接跳出执行下次循环,这里是很矛盾的。

那么在什么情况下fromUser为false呢?

因为hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser)方法访问权限为default,只有同个包里面的类才能调用,我直接搜索了fab这个方法的调用,发现了还有两个方法调用了这个方法,分别是

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;
}


可以发现,这两个类里面,hide的方法都是传入false的,还有,从名字可以发现,这两个方法应该分别是针对appbarlayout和bottomSheet的,访问权限是私有的,所以可以继续在fab类里面搜索调用的地方,发现这两个方法,都在另外两个方法里面一起被调用了

@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;
}


也就是说,只有在(AppBarLayout或者BottomSheet)与fab同为兄弟布局的时候,然后在他们滑动的时候,fab才会正常显示和隐藏,那么其他布局怎么办?像RecyclerView这种情况下该怎么解决?

其实也很简单,只要不执行hide方法就行了,自己实现隐藏动画,我这里直接拿了fab隐藏的动画,进行了修改了一下,然后就变成了我自己的,接下来直接贴代码好了。

继承FloatingActionButton.Behavior,另外实现了一个类ScrollAwareFABBehavior,代码如下

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.
* (~ ̄▽ ̄)~ 嘛哩嘛哩哄
*/
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {

	/**
	* 因为是在XML中使用app:layout_behavior定义静态的这种行为,
	* 必须实现一个构造函数使布局的效果能够正常工作。
	* 否则 Could not inflate Behavior subclass error messages.
	*
	* @param context
	* @param attrs
	*/
	public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
		super();
	}

	/**
	* 处理垂直方向上的滚动事件
	*
	* @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);
	}

	/**
	* 检查Y的位置,并决定按钮是否动画进入或退出
	*
	* @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
			//执行隐藏的动画
			hide(child);
		} else if (dyConsumed < -10 && child.getVisibility() != View.VISIBLE) {
			// User scrolled up and the FAB is currently not visible -> show the FAB
			//执行显示的动画
			show(child);
		}

	}

	/**
	* 显示的动画
	*/
	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) {

				}
		});
	}

	/**
	* 隐藏的动画
	*/
	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);
					}
				}
		});
	}
}


然后在xml那里直接改成

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

发现看源码还是一件比较有趣的事,就是有点耗时间。

如果想仔细查看所有代码的话,可以直接导下我的项目自己运行,并测试。

项目地址:https://github.com/voctex/Kepler

QQ:361561789

有事可以直接加Q联系

Link: http://www.asm32.net/article_details.aspx?id=7285


浏览次数 0 发布时间 2018/4/11 2:03:14 从属分类 Android 安卓 【评论】【 】【打印】【关闭
 
| www.asm32.net | 2006版 | 资料中心 | linux | asm/asm32 | C/C++ | VC++ | java | Python | 书签 | ASP.Net书签 | 京ICP备09029108号-1