在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這個強大的協調佈局纔有了偏移的功能,所以不少功能並支持,須要咱們去看源碼分析其中的緣由而後再對症修改。