最近因为Android Studio 升级到2.3正式版,将之前的老项目也进行了依赖更新,在这个过程中,发现了一些问题,将其记录如下。
FloatingActionButton.Behavior View的可见性
这个标题比较抽象,其实就是当RecyclerView 上滑的时候隐藏FloatingActionButton,在下滑时将其展示出来。然而,从23.3.0版本的Support Design包到25.2.0后,发现FloatingActionButton 隐藏后无法将其设置为Visible,在onStartNestedScroll() 方法中也无法再次接收到事件序列。主要逻辑代码如下:
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if ((dyConsumed > 0 || dyUnconsumed > 0) && child.getVisibility() == View.VISIBLE && !isTakingAnimation) {
//手指往上滑动
AnimationUtil.scaleHide(child, mViewPropertyAnimatorListener);
if (mOnStateChangedListener != null) {
mOnStateChangedListener.onChanged(false);
}
} else if ((dyConsumed < 0 || dyUnconsumed < 0) && child.getVisibility() != View.VISIBLE) {
//手指往下滑动
AnimationUtil.scaleShow(child, null);
if (mOnStateChangedListener != null) {
mOnStateChangedListener.onChanged(true);
}
}
}
//其中mViewPropertyAnimatorListener 主要代码
@Override
public void onAnimationEnd(View view) {
isTakingAnimation = false;
view.setVisibility(View.GONE);
}
Google一番过后,在Android 官方issue上找到了这个问题,找到了其中的原因:
It's because this: if (view.getVisibility() == View.GONE) { // If it's GONE, don't dispatch continue; } in CoordinatorLayout.onStartNestedScroll() sources. Temporary solution is to add listener to FAB: child.hide( new FloatingActionButton.OnVisibilityChangedListener() { @Override public void onHidden(FloatingActionButton fab) { super.onHidden(fab); fab.setVisibility(View.INVISIBLE); } } );
很多人跟我一样认为这是一个bug,但是Chris Banes大神表示这并不是bug,而是feature,并且将其标注为WorkAsIntended。所以这个问题暂时的解决办法就是将GONE 换成INVISIBLE。
BottomSheetBehavior 的setState() 无效
出现问题的还有底部的View,其展现效果和上面的FloatingActionButton 一样,并且是一个联动的效果。然而,上面的问题解决之后,发现下面的View 毫无动静,一动也不动。原代码如下:
mBottomSheetBehavior = BottomSheetBehavior.from(mContainer);
mBottomSheetBehavior.setState(isShow ? BottomSheetBehavior.STATE_EXPANDED : BottomSheetBehavior.STATE_COLLAPSED);
//布局文件
<LinearLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:orientation="vertical"
app:layout_behavior="@string/bottom_sheet_behavior" />
检查代码并没有发现什么问题,只好从BottomSheetBehavior 的源码中寻找答案。
public final void setState(final @State int state) {
//篇幅所限,省略参数检查部分代码
// Start the animation; wait until a pending layout if there is one.
ViewParent parent = child.getParent();
if (parent != null && parent.isLayoutRequested() && ViewCompat.isAttachedToWindow(child)) {
child.post(new Runnable() {
@Override
public void run() {
startSettlingAnimation(child, state);
}
});
} else {
startSettlingAnimation(child, state);
}
}
setState() 方法中判断了parentView 即CoordinatorLayout是否添加到了窗口中,最终都会执行到startSettlingAnimation 方法中。
void startSettlingAnimation(View child, int state) {
int top;
if (state == STATE_COLLAPSED) {
top = mMaxOffset;
} else if (state == STATE_EXPANDED) {
top = mMinOffset;
} else if (mHideable && state == STATE_HIDDEN) {
top = mParentHeight;
} else {
throw new IllegalArgumentException("Illegal state argument: " + state);
}
setStateInternal(STATE_SETTLING);
if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
}
}
这里根据设置的不同状态对top(即偏移量)进行了赋值,接着设置内部状态为STATE_SETTLING,同时设置了onStateChanged 的回调。我们知道BottomSheetBehavior 的动画是利用了ViewDragHelper 来实现的,因此接下就调用了ViewDragHelper的smoothSlideViewTo 方法,它的返回值决定了是否能够执行状态改变的动画。
/**
* Settle the captured view at the given (left, top) position.
*
* @param finalLeft Target left position for the captured view
* @param finalTop Target top position for the captured view
* @param xvel Horizontal velocity
* @param yvel Vertical velocity
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
//这里的mCapturedView为child,即执行动画的View
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
从smoothSlideViewTo 方法中最终会走到forceSettleCapturedViewAt 方法,根据传入的x 轴,y 轴起始位置计算出真正的偏移量,当x轴,y轴的偏移量均为0时则表示不移动,那么返回回去使得动画无法执行。这样看来问题就缩小到了传入的距离参数上面。
从上面代码得知,上面的finalTop 即之前根据状态设置的top,也就是mMaxOffset 以及mMinOffset,其中peekHeight 这个参数对mMaxOffset 的值起到了决定性的作用。
// Offset the bottom sheet
mParentHeight = parent.getHeight();
int peekHeight;
if (mPeekHeightAuto) {
if (mPeekHeightMin == 0) {
mPeekHeightMin = parent.getResources().getDimensionPixelSize(
R.dimen.design_bottom_sheet_peek_height_min);
}
peekHeight = Math.max(mPeekHeightMin, mParentHeight - parent.getWidth() * 9 / 16);
} else {
peekHeight = mPeekHeight;
}
mMinOffset = Math.max(0, mParentHeight - child.getHeight());
mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);
//...构造函数中初始化
if (value != null && value.data == PEEK_HEIGHT_AUTO) {
setPeekHeight(value.data);
} else {
setPeekHeight(a.getDimensionPixelSize(
R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO));
}
/**
* Sets the height of the bottom sheet when it is collapsed.
*
* @param peekHeight The height of the collapsed bottom sheet in pixels, or
* {@link #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically
* at 16:9 ratio keyline.
*/
public final void setPeekHeight(int peekHeight) {
boolean layout = false;
if (peekHeight == PEEK_HEIGHT_AUTO) {
if (!mPeekHeightAuto) {
mPeekHeightAuto = true;
layout = true;
}
} else if (mPeekHeightAuto || mPeekHeight != peekHeight) {
mPeekHeightAuto = false;
mPeekHeight = Math.max(0, peekHeight);
mMaxOffset = mParentHeight - peekHeight;
layout = true;
}
}
看到上面的代码,一切都明白了。Behavior 初始化的时候会去设置peekHeight,即collapse(收起)时的高度,默认值为-1,