先上效果圖吧:
第一個想到的實作方式是上面使用horizontalScrollview,下面使用Viewpager,經過嘗試之後發現二者API有限,不能達到理想效果。幾經折騰,最後上下都使用了自定義的RecyclerView。效果圖如下:
現在來分析技術點,首先是上下關聯,思路是在Recycleview的onScrolled回調方法中操作另一個Recycleview的滑動。
1 @Override
2 public void onScrolled(int dx, int dy) {
3 super.onScrolled(dx, dy);
4 sx = sx +dx;
5 if (scrollViewListener != null && isMark) {
6 scrollViewListener.onScrollChanged(this, sx, 0);
7 }
8 }
其中onScrollChanged方法在首頁面中實作
1 @Override
2 public void onScrollChanged(Object scrollView, int x, int y) {
3 int width1 = CommonUtil.getScreenWidth(this) - DensityUtils.dip2px(this, 60);
4 int width2 = CommonUtil.getScreenWidth(this);
5 if (scrollView == rvHead) {
6 rvFoot.setmark(false);
7 rvFoot.scrollTo(x * width2 / width1, y);
8 } else if (scrollView == rvFoot) {
9 rvHead.setmark(false);
10 rvHead.scrollTo(x * width1 / width2, y);
11 }
12 rvHead.setmark(true);
13 rvFoot.setmark(true);
14 }
上下View的滑動速率差即為上下RecyclerView中item的寬度差,上面view中item的寬度為螢幕寬度-60dp,詳見對應的adapter。
由于RecyclerView中scrollTo方法沒有實作,是以直接想到的是用scroolBy代替,但由于滑動回調傳回的是Int值,經過速率差處理後精度丢失,得不到準确值,導緻關聯效果達不到,痛定思痛,最後還是自己來重寫scrollTo方法:
1 @Override
2 public void scrollTo(int x, int y) {
3 scrollBy(x-sx,0);
4 }
sx為自己在onScrolled方法中記錄,具體見文末給出的源碼。
滑動之後,還要進行回調處理,以達到像viewPager那樣的回彈效果,具體邏輯在自定義的RecyclerView中的回調方法onScrollStateChanged中實作:
1 public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
2 super.onScrollStateChanged(recyclerView, newState);
3 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
4 int mmSelected;
5 //當控件停止滾動時,擷取可視範圍第一個item的位置,滾動調整控件以使選中的item剛好處于正中間
6 int firstVisiblePos = mLayoutManager.findFirstVisibleItemPosition();
7 if (firstVisiblePos == RecyclerView.NO_POSITION) {
8 return;
9 }
10 Rect rect = new Rect();
11 mLayoutManager.findViewByPosition(firstVisiblePos).getHitRect(rect);
12 if (Math.abs(rect.left) > mItemWidth / 2) {
13 smoothScrollBy(rect.right, 0);
14 mmSelected = firstVisiblePos + 1;
15 } else {
16 smoothScrollBy(rect.left, 0);
17 mmSelected = firstVisiblePos;
18 }
19 if (Math.abs(rect.left) == 0 && mOnSelectListener != null && mmSelected != mSelected) {
20 mSelected = mmSelected;
21 mOnSelectListener.onSelect(mSelected);
22 }
23 }
24 }
25 }
為了讓滑動效果更為自然且支援fling效果,本項目還重寫了RecyclerView的fling方法,使得每次fling都恰好能滑動整數個item,大緻思路為調整fling初始速率,代碼如下:
1 @Override
2 public boolean fling(int velocityX, int velocityY) {
3 int v;
4 int touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
5 if (Math.abs(velocityX) <= 3*touchSlop) {
6 return false;
7 }
8 mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
9 * 39.37f // inch/meter
10 * getContext().getResources().getDisplayMetrics().density * 160.0f // pixels per inch
11 * 0.84f;
12 int firstVisiblePos = mLayoutManager.findFirstVisibleItemPosition();
13 if (firstVisiblePos == RecyclerView.NO_POSITION) {
14 return false;
15 }
16 Rect rect = new Rect();
17 mLayoutManager.findViewByPosition(firstVisiblePos).getHitRect(rect);
18 double n = getSplineFlingDistance(velocityX) / mItemWidth;
19 int num = Double.valueOf(n).intValue();
20 if (velocityX > 0)
21 v = Double.valueOf(getVelocityByDistance(num * mItemWidth + Math.abs(rect.right)- DensityUtils.dip2px(getContext(), 20))).intValue();
22 else
23 v = Double.valueOf(getVelocityByDistance(num * mItemWidth + Math.abs(rect.left)+ DensityUtils.dip2px(getContext(), 20))).intValue();
24 if (velocityX < 0) {
25 v = -v;
26 }
27 return super.fling(v, velocityY);
28 }