Android 仿京東,淘寶RecyclerView巢狀ViewPager巢狀RecyclerView商品展示
最近看到京東,淘寶都有RecyclerView巢狀ViewPager巢狀RecyclerView商品展示的效果,效果挺好,廢話不多說先看效果圖:
GIF.gif
技能點:
1.Android事件分發機制等
需求點:
1.列表巢狀,內層的列表可以左右切換
2.ViewPager可以點選和滑動切換
最近在淘寶京東看到類似的效果,有時間就寫了一下,效果實現了,但是感覺解決問題的思路和程式碼有很多瑕疵,寫出來拋磚引玉,希望大佬們不吝賜教,寫的不好不喜勿噴!
下面進入正題,先看下佈局結構:
screen.png
就是標題所說的佈局結構 RecyclerView+ViewPager+RecyclerView
很多同學看到這裡肯定想到要處理滑動衝突,沒錯,我們簡單分析一下好擼程式碼(雖然是擼好的程式碼)
- 橫向滑動
- 橫向滑動很簡單,RecyclerView不需要處理,ViewPager處理
- 縱向滑動
- 縱向滑動就稍微複雜點,本文的解決滑動衝突主要就就是解決外層RecyclerView以及內層RecyclerView的滑動衝突,仔細看下互動效果,不難發現我們需要用Tab是否吸頂作為判斷的節點來將滑動事件交給外層或內層RecyclerView處理. 即: 1.Tab未吸頂時外層RecyclerView處理滑動事件,2.Tab吸頂時內層RecyclerView處理滑動事件. 這裡解釋一下,原來的方案是吸頂,後來我想了一下如果這個ViewPager下面沒有跟多其它的樣式的話,可以不用吸頂的(不能再有了,互動處理也太麻煩,有的話排版應該也不好看),
大概就是這樣,思路很清晰,這裡先提幾個接下來遇到的問題:
- RecyclerView巢狀ViewPager時ViewPager的高度為0
- 滑動衝突
-
操作步驟:滑動到Tab吸頂->滑動內層RecyclerView至中間->切換一個Tab(內層RecyclerView的狀態已經滑動到頂部,就是初始狀態)->這時候將Tab滑動到非吸頂->切換到最初內層RecyClerView滑動到中間的Tab,這時候展示的就是Tab未吸頂,內層RecyclerView不在頂部的尷尬局面.說了這麼多應該需要一張gif解釋一下上圖:
GIF1.gif
對於上圖所提到的情況,這個時候使用者手指縱向滑動紅色區域,滑動事件交給誰都不合適
.那先說下淘寶和京東採取的方式:
- 淘寶和京東部分頁面切換ViewPager時候重新拉取資料(可能沒有重新拉資料,只是notify了一下)將RecyclerView直接展示到初始狀態
- 京東的部分介面(京東->我的->下拉->為你推薦)處理方式為:當Tab為非吸頂狀態時候切換ViewPager,外層RecyclerView滑動到Tab吸頂
- demo因為用的是假資料,所以沒做處理,但是程式碼中有在tab非吸頂狀態時候,外層RecyclerView優先處理滑動事件的程式碼
個人感覺第一種處理方式比較好一點,demo的程式碼如下(需要請自行修改,PagerFragment.java)
if(! ((MainActivity)getActivity()).isStick){
((MainActivity)getActivity()).adjustScroll(true);
return false;
}
下面說下實現方式,以及問題的解決(佈局等細節就不貼出來了,詳情見demo):
- 外部的RecyclerView為自定義的View繼承自RecyclerView重寫onInterceptTouchEvent方法
處理滑動事件:
private float downX ; //按下時 的X座標
private float downY ; //按下時 的Y座標
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
float x= e.getX();
float y = e.getY();
switch (e.getAction()){
case MotionEvent.ACTION_DOWN:
//將按下時的座標儲存
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
//獲取到距離差
float dx= x-downX;
float dy = y-downY;
//通過距離差判斷方向
int orientation = getOrientation(dx, dy);
switch (orientation) {
//左右滑動交給ViewPager處理
case 'r':
setNeedIntercept(false);
break;
//左右滑動交給ViewPager處理
case 'l':
setNeedIntercept(false);
break;
}
return isNeedIntercept;
}
return super.onInterceptTouchEvent(e);
}
public void setNeedIntercept(boolean needIntercept) {
isNeedIntercept = needIntercept;
}
private int getOrientation(float dx, float dy) {
if (Math.abs(dx)>Math.abs(dy)){
//X軸移動
return dx>0?'r':'l';//右,左
}else{
//Y軸移動
return dy>0?'b':'t';//下//上
}
}
isNeedIntercept為是否攔截滑動事件,自己處理.並提供了一個setNeedIntercept方法供外部呼叫.程式碼可以看出,橫向的滑動直接放行,讓ViewPager處理,向上滑動時候如果tab吸頂了且已經滑動到底部,交給內部的RecyclerView處理,否則自己處理.
我們對內層的RecyclerView進行處理,重寫其onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent e) {
float x= e.getX();
float y = e.getY();
switch (e.getAction()){
case MotionEvent.ACTION_DOWN:
//將按下時的座標儲存
downX = x;
downY = y;
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
//獲取到距離差
float dx= x-downX;
float dy = y-downY;
//通過距離差判斷方向
int orientation = getOrientation(dx, dy);
int[] location={0,0};
getLocationOnScreen(location);
switch (orientation) {
case 'b':
//內層RecyclerView下拉到最頂部時候不再處理事件
if(!canScrollVertically(-1)){
getParent().requestDisallowInterceptTouchEvent(false);
if(needIntercepectListener!=null){
needIntercepectListener.needIntercepect(false);
}
}else{
getParent().requestDisallowInterceptTouchEvent(true);
if(needIntercepectListener!=null){
needIntercepectListener.needIntercepect(true);
}
}
break;
case 't':
if(location[1]<=maxY){
getParent().requestDisallowInterceptTouchEvent(true);
if(needIntercepectListener!=null){
needIntercepectListener.needIntercepect(true);
}
}else{
getParent().requestDisallowInterceptTouchEvent(false);
if(needIntercepectListener!=null){
needIntercepectListener.needIntercepect(false);
return true;
}
}
break;
case 'r':
getParent().requestDisallowInterceptTouchEvent(false);
break;
//左右滑動交給ViewPager處理
case 'l':
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
break;
}
return super.onTouchEvent(e);
}
private int getOrientation(float dx, float dy) {
if (Math.abs(dx)>Math.abs(dy)){
//X軸移動
return dx>0?'r':'l';//右,左
}else{
//Y軸移動
return dy>0?'b':'t';//下//上
}
}
public void setMaxY(int height) {
this.maxY=height;
}
public interface NeedIntercepectListener{
void needIntercepect(boolean needIntercepect);
}
public void setNeedIntercepectListener(NeedIntercepectListener needIntercepectListener) {
this.needIntercepectListener = needIntercepectListener;
}
其中的回撥是為了告訴外層的RecyclerView需不需要攔截事件.
滑動衝突到這裡基本上處理完了,下面說下吸頂的問題,其實只是思路的問題,這裡採取的方式是將TabLayout和ViewPager當做一個外層RecyclerView的最後一個item,並且高度為螢幕高度-狀態列高度,這樣當外層RecyclerView滑動到底部,Tab看上去是吸頂的.
簡單說下:這個demo之前是按真正的吸頂做的,所以文章改動過,哪裡說得不清楚的請直接看demo,主要是處理滑動事件衝突,難度不大,純屬拋磚引玉.
最後暴露一個問題,在外層RecyclerView滑動到底部時,需要將觸控事件交給內層的RecyclerView處理時,按照Demo裡的處理方式,手指擡起之後重新滑動,內層RecyclerView才能拿到事件,原因是Demo判斷外層RecyclerView是否滑動到底部的程式碼寫在onInterceptTouchEvent裡面,這個方法並不會實時呼叫,試過將判斷寫在onTouchEvent裡面,實時判斷再呼叫onInterceptTouchEvent,但是好像因為內層的RecyclerView並沒有消費掉事件,所以這麼做並沒有效果,並沒有實時的將觸控事件交給內層RecyclerView處理,這裡嘗試了很多方式,都不太理想,希望有思路的大佬給指點一下,效果如下:
GIF2.gif
作者:大名鼎鼎劉小廚
連結:https://www.jianshu.com/p/a5100ac471ae
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。