Android v26.x support library中升级了NestedScrolling API,以此修复了CoordinatorLayout+AppBarLayout的fling卡顿的问题,戳这里看Chris大佬的文章说明。
所以我们可以仅通过布局就能实现以下效果:
一切看起来跟初恋一样美好,但是在实际的应用中却遇到了些问题。
一图胜千言,所以还是看图:
因为业务需求导致AppBarLayout高度很大,这样的情况下很容易触发一个操作:
在粉色或绿色部分施加一个向上的fling,紧接着在白色列表部分施加一个向下的fling,就会出现上图这样的激烈的抖动回弹。
到这里,问题也描述的差不多了,有遇到同样问题&心急的朋友可能反手就是一拖鞋
Demo在文章底部
public class FixBounceV26Behavior extends AppBarLayout.Behavior {
private OverScroller mScroller1;
public FixBounceV26Behavior() {
super();
}
public FixBounceV26Behavior(Context context, AttributeSet attrs) {
super(context, attrs);
bindScrollerValue(context);
}
/**
* 反射注入Scroller以获取其引用
*
* @param context
*/
private void bindScrollerValue(Context context) {
if (mScroller1 != null) return;
mScroller1 = new OverScroller(context);
try {
Class> clzHeaderBehavior = getClass().getSuperclass().getSuperclass();
Field fieldScroller = clzHeaderBehavior.getDeclaredField("mScroller");
fieldScroller.setAccessible(true);
fieldScroller.set(this, mScroller1);
} catch (Exception e) {}
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
//fling上滑appbar然后迅速fling下滑list时, HeaderBehavior的mScroller并未停止, 会导致上下来回晃动
if (mScroller1.computeScrollOffset()) {
mScroller1.abortAnimation();
}
//当target滚动到边界时主动停止target fling,与下一次滑动产生冲突
if (getTopAndBottomOffset() == 0) {
ViewCompat.stopNestedScroll(target, type);
}
}
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
}
这就完了?
是的。
上面的代码已经能简单地解决问题了,想知道的更详细的话可以往下看看。
- Round 1
CoordinatorLayout和子View的联动时通过CoordinatorLayout.Behavior实现的,AppBarLayout使用的Behavior继承了HeaderBehavior。
问题就在这里。
HeaderBehavior的onTouchEvent中使用Scroller实现了fling操作,但是没有通过NestedScrolling API对外开放,也就说一旦HeaderBehavior的fling动作形成,无法由外部主动中断。
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
switch (ev.getActionMasked()) {
//剔除了多余部分
case MotionEvent.ACTION_UP:
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
}
}
return true;
}
final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
int maxOffset, float velocityY) {
if (mFlingRunnable != null) {
layout.removeCallbacks(mFlingRunnable);
mFlingRunnable = null;
}
if (mScroller == null) {
mScroller = new OverScroller(layout.getContext());
}
mScroller.fling(
0, getTopAndBottomOffset(), // curr
0, Math.round(velocityY), // velocity.
0, 0, // x
minOffset, maxOffset); // y
if (mScroller.computeScrollOffset()) {
mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
ViewCompat.postOnAnimation(layout, mFlingRunnable);
return true;
} else {
onFlingFinished(coordinatorLayout, layout);
return false;
}
}
-
Round 2
与AppBarLayout同层级的RecyclerView可以通过升级过的NestedScrolling API对AppBarLayout产生影响,比如RecyclerView向下fling时滑动到item 0之后,如果AppBarLayout可以滑动时会给AppBarLayout施加一个同样向下的fling动作,以此形成一个连贯的下滑fling。
那么问题来了。
当HeaderBehavior产生的向上的fling没有结束时,RecyclerView又送来向下的fling,抖动就产生了。
- Round 3
既然知道了问题所在,那么我们就来解决问题。
看一哈HeaderBehavior的代码发现mScroller没有提供对外的调用接口而且使用默认的修饰符,但是有一个好消息就是mScroller是在fling()方法中通过判空来初始化的并且没有重置mScroller的操作。
所以我们可以通过子类继承之后在构建时就为mScroller注入实例,这样我们就可以获得mScroller的引用了。
然后在RecyclerView产生fling时主动中断mScroller.fling。
/**
* 反射注入Scroller以获取其引用
*
* @param context
*/
private void bindScrollerValue(Context context) {
if (mScroller1 != null) return;
mScroller1 = new OverScroller(context);
try {
//android.support.design.widget.HeaderBehavior
Class> clzHeaderBehavior = getClass().getSuperclass().getSuperclass();
Field fieldScroller = clzHeaderBehavior.getDeclaredField("mScroller");
fieldScroller.setAccessible(true);
fieldScroller.set(this, mScroller1);
} catch (Exception e) {}
}
/**
* 来自RecyclerView的fling会触发NestedScrolling API
*/
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
if (type == ViewCompat.TYPE_NON_TOUCH) {
//fling上滑appbar然后迅速fling下滑list时, HeaderBehavior的mScroller并未停止, 会导致上下来回晃动
if (mScroller1.computeScrollOffset()) {
mScroller1.abortAnimation();
}
//当target滚动到边界时主动停止target fling,与下一次滑动产生冲突
if (getTopAndBottomOffset() == 0) {
ViewCompat.stopNestedScroll(target, type);
}
}
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
最后把写好的Behavior应用到AppBarLayout上就OK了。
代码很少,实现方式也很简单,主要是分享下解决此类问题的一种思路,希望能对大家有所帮助。
Demo传送阵