最近看到京東,淘寶都有RecyclerView嵌套ViewPager嵌套RecyclerView商品展示的效果,效果挺好,廢話不多說先看效果圖:
技能點:
1.Android事件分發(fā)機制等
需求點:
1.列表嵌套,內(nèi)層的列表可以左右切換
2.ViewPager可以點擊和滑動切換
最近在淘寶京東看到類似的效果,有時間就寫了一下,效果實現(xiàn)了,但是感覺解決問題的思路和代碼有很多瑕疵,寫出來拋磚引玉,希望大佬們不吝賜教,寫的不好不喜勿噴!
下面進入正題,先看下布局結(jié)構(gòu):
就是標(biāo)題所說的布局結(jié)構(gòu) RecyclerView+ViewPager+RecyclerView`
很多同學(xué)看到這里肯定想到要處理滑動沖突,沒錯,我們簡單分析一下好擼代碼(雖然是擼好的代碼)
- 橫向滑動
- 橫向滑動很簡單,RecyclerView不需要處理,ViewPager處理
- 縱向滑動
- 縱向滑動就稍微復(fù)雜點,本文的解決滑動沖突主要就就是解決外層RecyclerView以及內(nèi)層RecyclerView的滑動沖突,仔細看下交互效果,不難發(fā)現(xiàn)我們需要用Tab是否吸頂作為判斷的節(jié)點來將滑動事件交給外層或內(nèi)層RecyclerView處理. 即: 1.Tab未吸頂時外層RecyclerView處理滑動事件,2.Tab吸頂時內(nèi)層RecyclerView處理滑動事件. 這里解釋一下,原來的方案是吸頂,后來我想了一下如果這個ViewPager下面沒有跟多其它的樣式的話,可以不用吸頂?shù)?不能再有了,交互處理也太麻煩,有的話排版應(yīng)該也不好看),
大概就是這樣,思路很清晰,這里先提幾個接下來遇到的問題:
- RecyclerView嵌套ViewPager時ViewPager的高度為0
- 滑動沖突
-
操作步驟:滑動到Tab吸頂->滑動內(nèi)層RecyclerView至中間->切換一個Tab(內(nèi)層RecyclerView的狀態(tài)已經(jīng)滑動到頂部,就是初始狀態(tài))->這時候?qū)ab滑動到非吸頂->切換到最初內(nèi)層RecyClerView滑動到中間的Tab,這時候展示的就是Tab未吸頂,內(nèi)層RecyclerView不在頂部的尷尬局面.說了這么多應(yīng)該需要一張gif解釋一下上圖:
對于上圖所提到的情況,這個時候用戶手指縱向滑動紅色區(qū)域,滑動事件交給誰都不合適
.那先說下淘寶和京東采取的方式:
- 淘寶和京東部分頁面切換ViewPager時候重新拉取數(shù)據(jù)(可能沒有重新拉數(shù)據(jù),只是notify了一下)將RecyclerView直接展示到初始狀態(tài)
- 京東的部分界面(京東->我的->下拉->為你推薦)處理方式為:當(dāng)Tab為非吸頂狀態(tài)時候切換ViewPager,外層RecyclerView滑動到Tab吸頂
- demo因為用的是假數(shù)據(jù),所以沒做處理,但是代碼中有在tab非吸頂狀態(tài)時候,外層RecyclerView優(yōu)先處理滑動事件的代碼
個人感覺第一種處理方式比較好一點,demo的代碼如下(需要請自行修改,PagerFragment.java)
if(! ((MainActivity)getActivity()).isStick){
((MainActivity)getActivity()).adjustScroll(true);
return false;
}
下面說下實現(xiàn)方式,以及問題的解決(布局等細節(jié)就不貼出來了,詳情見demo):
- 外部的RecyclerView為自定義的View繼承自RecyclerView重寫onInterceptTouchEvent方法
處理滑動事件:
private float downX ; //按下時 的X坐標(biāo)
private float downY ; //按下時 的Y坐標(biāo)
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
float x= e.getX();
float y = e.getY();
switch (e.getAction()){
case MotionEvent.ACTION_DOWN:
//將按下時的坐標(biāo)存儲
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方法供外部調(diào)用.代碼可以看出,橫向的滑動直接放行,讓ViewPager處理,向上滑動時候如果tab吸頂了且已經(jīng)滑動到底部,交給內(nèi)部的RecyclerView處理,否則自己處理.
我們對內(nèi)層的RecyclerView進行處理,重寫其onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent e) {
float x= e.getX();
float y = e.getY();
switch (e.getAction()){
case MotionEvent.ACTION_DOWN:
//將按下時的坐標(biāo)存儲
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':
//內(nèi)層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;
}
其中的回調(diào)是為了告訴外層的RecyclerView需不需要攔截事件.
滑動沖突到這里基本上處理完了,下面說下吸頂?shù)膯栴},其實只是思路的問題,這里采取的方式是將TabLayout和ViewPager當(dāng)做一個外層RecyclerView的最后一個item,并且高度為屏幕高度-狀態(tài)欄高度,這樣當(dāng)外層RecyclerView滑動到底部,Tab看上去是吸頂?shù)?
簡單說下:這個demo之前是按真正的吸頂做的,所以文章改動過,哪里說得不清楚的請直接看demo,主要是處理滑動事件沖突,難度不大,純屬拋磚引玉.
最后暴露一個問題,在外層RecyclerView滑動到底部時,需要將觸摸事件交給內(nèi)層的RecyclerView處理時,按照Demo里的處理方式,手指抬起之后重新滑動,內(nèi)層RecyclerView才能拿到事件,原因是Demo判斷外層RecyclerView是否滑動到底部的代碼寫在onInterceptTouchEvent里面,這個方法并不會實時調(diào)用,試過將判斷寫在onTouchEvent里面,實時判斷再調(diào)用onInterceptTouchEvent,但是好像因為內(nèi)層的RecyclerView并沒有消費掉事件,所以這么做并沒有效果,并沒有實時的將觸摸事件交給內(nèi)層RecyclerView處理,這里嘗試了很多方式,都不太理想,希望有思路的大佬給指點一下,效果如下:
項目地址:Github