在9.0之前版本中首页没有作AppBarLayout与底部RecyclerView的Fling链接处理,致使在AppBarLayout往上Fling时当滚动到AppBarLayout底部时会当即停住,致使动画会比较生硬,当咱们在9.0新版首页改版时有用户反馈这块的问题,因而咱们花时间进行了一些优化处理,下面先看一下老版本与新版本首页效果的对比。
能够很明显的看到老版本在滚动到AppBarLayout底部时瞬间停住,给人一种很生硬的感受,下面咱们就来说一讲如何进行优化。java
为了搞清楚为何会出现这样的问题,咱们分析了一下AppBarLayout的源码。下面是一个大体的流程图:
下面咱们进行详细的源码分析:
首先AppBarLayout之因此能够折叠实际上是依赖了CoordinatorLayout的能力,用户事件会被CoordinatorLayout感知而后传递给AppBarLayout的Behavior,AppBarLayout的Behavior继承自HeaderBehavior,咱们阅读onTouchEvent方法,发现其处理fling的代码以下:算法
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); }
再看fling方法的实现,咱们发现了其使用了OverScroller来实现fing效果的算法实现,具体的View滚动由FlingRunnable承担。代码以下:ide
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; } }
经过以上代码能够发现,在使用OverScroller计算fling事件时,其设置了minOffset(Y轴向上滚动的边界),经过向上跟踪代码发现这个minOffset刚好就是AppBarLayout的高度取反。源码分析
int getScrollRangeForDragFling(V view) { return view.getHeight(); }
这就能解释了为何滚动到顶部后中止的问题了。下面再看一下具体的fling实现:布局
private class FlingRunnable implements Runnable { private final CoordinatorLayout mParent; private final V mLayout; FlingRunnable(CoordinatorLayout parent, V layout) { mParent = parent; mLayout = layout; } @Override public void run() { if (mLayout != null && mScroller != null) { if (mScroller.computeScrollOffset()) { setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY()); // Post ourselves so that we run on the next animation ViewCompat.postOnAnimation(mLayout, this); } else { onFlingFinished(mParent, mLayout); } } } }
FlingRunnable是具体的滚动实现,run方法中并无发现其将fling事件传递给父View CoordinatorLayout,所以这个fling事件由AppBarLayout消费,没法带动底部的RecyvlerView fling。post
上文中已经找到了具体的缘由,可是咱们没法修改AppBarLayout代码,所以这里咱们要明确一点:若是想让AppBarLayout的Fling链接上RecyclerView就必须自定义Behavior或者修改HeaderBehavior。
因为自定义Behavior必须继承CoordinatorLayout.Behavior,而后把
AppBarLayout.Behavior与其父类一直到ViewOffsetBehavior的代码所有复制出来,而且涉及相关类比较多,所以咱们直接把AppBarLayout相关代码所有复制出来,效果以下:
下面进行具体代码的修改。
上文中也提到在使用OverScroller计算fling事件时,其设置了minOffset这个minOffset刚好就是向上滚动到AppBarLayout底部的位置。所以第一步咱们要把这个值设置的足够小,让OverScroller计算出更长的fling时间与距离。这里判断若是是向上fling时就把minOffset设置为Integer.MIN_VALUE,具体代码以下:优化
int fixedMin = velocityY < 0 ? Integer.MIN_VALUE : minOffset; mScroller.fling( 0, getTopAndBottomOffset(), // curr 0, Math.round(velocityY), // velocity. 0, 0, // x fixedMin, maxOffset); // y
第二步就是要修改FlingRunnable了,让其在fling时带动AppBarLayout下面的View同时fling。
咱们知道CoordinatorLayout就是为了解决嵌套滚动而生,咱们应该调用CoordinatorLayout的能力,把这个fling分发给下面的View就能够了。
CoordinatorLayout嵌套滚动的原理以下:
CoordinatorLayout实现了NestedScrollingParent,当CoordinatorLayout内有一个支持NestedScroll的子View时,它的嵌套滑动事件经过NestedScrollingParent的回调分发到各直接子View的Behavior处理。RecyclerView就是实现了NestedScrollingChild2的子View(NestedScrollingChild2继承于NestedScrollingChild),而AppBarLayout却没有实现NestedScrollingChild接口。所以若是咱们想经过调用CoordinatorLayout分发嵌套事件会存在如下两个问题:动画
所以通过调研咱们放弃了这种方案。
下面说一下咱们最终使用的方案,首先咱们经过id或者tag的方式获取到须要须要被fling带动的目标View,相关代码以下:this
public class NestedScrollTarget { private NestedScrollView mNestedScrollView; private LinearLayoutManager mLayoutManager; /** * 带动RecyclerView fling时的position,默认为0,滚动时不停增长 */ private int recyclerPosition = 0; /** * RecyclerView最后已偏移的Y轴位置,默认为0 */ private int recyclerLastOffset = 0; public NestedScrollTarget(View v) { findScrollTarget(v); } /** * 查找须要嵌套fling的目标 * @param v */ protected void findScrollTarget(View v) { if (findNestedScrollTarget(v)) return; if (v instanceof ViewPager) { View root = findCurrentPagerView((ViewPager) v); if (root == null) return; View child = root.findViewWithTag("nested_fling"); findNestedScrollTarget(child); } } private View findCurrentPagerView(ViewPager vp) { int position = vp.getCurrentItem(); PagerAdapter adapter = vp.getAdapter(); if (adapter instanceof FragmentStatePagerAdapter) { FragmentStatePagerAdapter fsp = (FragmentStatePagerAdapter) adapter; return fsp.getItem(position).getView(); } else if (adapter instanceof FragmentPagerAdapter) { FragmentPagerAdapter fp = (FragmentPagerAdapter) adapter; return fp.getItem(position).getView(); } return null; } private boolean findNestedScrollTarget(View v) { if (v instanceof NestedScrollView) { mNestedScrollView = (NestedScrollView) v; stopScroll(mNestedScrollView); return true; } if (v instanceof RecyclerView) { RecyclerView.LayoutManager lm = ((RecyclerView) v).getLayoutManager(); if (lm instanceof LinearLayoutManager) { mLayoutManager = (LinearLayoutManager) lm; stopScroll((RecyclerView) v); return true; } } return false; } /** * 中止NestedScrollView滚动 * * @param v */ private void stopScroll(NestedScrollView v) { try { Field field = ReflectUtil.getDeclaredField(v, "mScroller"); if (field == null) return; field.setAccessible(true); OverScroller scroller = (OverScroller) field.get(v); if (scroller != null) scroller.abortAnimation(); } catch (Exception e) { e.printStackTrace(); } } /** * 中止RecyclerView滚动 * * @param */ private void stopScroll(RecyclerView rv) { try { Field field = ReflectUtil.getDeclaredField(rv, "mViewFlinger"); if (field == null) return; field.setAccessible(true); Object obj = field.get(rv); if (obj == null) return; Method method = obj.getClass().getDeclaredMethod("stop"); method.setAccessible(true); method.invoke(obj); } catch (Exception e) { e.printStackTrace(); } } public void scrollToY(int dy) { if (mNestedScrollView != null) { mNestedScrollView.scrollTo(0, dy); } else if (mLayoutManager != null) { //动态计算RecyclerView滑动偏移量,以及依赖的位置 if (mLayoutManager != null) { View view = mLayoutManager.findViewByPosition(recyclerPosition); int offset = dy - recyclerLastOffset; if (view != null) { int height = view.getHeight(); if (dy > (recyclerLastOffset + height)) { recyclerPosition++; offset = dy - recyclerLastOffset - height; recyclerLastOffset += height; } } mLayoutManager.scrollToPositionWithOffset(recyclerPosition, -offset); } } } }
实际滚动时须要注意,RecyclerView并比支持直接滚动到某一个点,可是提供了scrollToPositionWithOffset方法,这个方法的意思是滚动到某一个Position而且偏移部分像素。咱们能够基于此方法来实现滚动到某一个位置,调用这个方法时须要注意第一个参数position必定要传屏幕中显示的position,不然会致使已经再也不屏幕中的position不回收,而后很容易引发OOM。具体代码以下:spa
public void scrollToY(int dy) { if (mNestedScrollView != null) { mNestedScrollView.scrollTo(0, dy); } else if (mLayoutManager != null) { //动态计算RecyclerView滑动偏移量,以及依赖的位置 if (mLayoutManager != null) { View view = mLayoutManager.findViewByPosition(recyclerPosition); int offset = dy - recyclerLastOffset; if (view != null) { int height = view.getHeight(); if (dy > (recyclerLastOffset + height)) { recyclerPosition++; offset = dy - recyclerLastOffset - height; recyclerLastOffset += height; } } mLayoutManager.scrollToPositionWithOffset(recyclerPosition, -offset); } } }
AppBarLayout并不支持滚动,只是依附于CoordinatorLayout这个强大的协调布局才有了偏移的功能,所以不少功能并支持,须要咱们去看源码分析其中的缘由而后再对症修改。