專案中有時候會讓一個界面出現多個RecyclerView來進行嵌套的情況,如淘寶,京東首頁的界面,
當我們自己的商城的首頁和淘寶、京東首頁效果類似,上面為配置資料,中間是各種分類頻道,下面是商品流資料,
商品流部分支持左右橫滑,分類頻道是支持吸頂的,下面的左圖是京東首頁效果圖,右圖是自己實作的,基本上都能實作,
先看效果圖,后面介紹該框架的使用情況:


一、使用原理
最早是用CoordinatorLayout實作,在AppBarLayout下放一個RecyclerView,下面部分則放一個ViewPager,ViewPager里則是商品流的RecyclerView,
CoordinatorLayout支持設定他的某個子View吸頂,這樣基本可以讓這種實作滿足商業性的專案需求,但是使用開發起來比較麻煩,
### 調研淘寶京東首頁
在呼叫了淘寶和京東的實作發現可以通過兩層RecyclerView來實作,可以看到京東首頁的實作是上面這樣的,
### 大致實作方式
- 外部RecyclerView為ParentRecyclerView,將需要吸頂的懸浮效果的TabLayout和ViewPager作為ParentRecyclerView的一個item,
- 內部RecyclerView為ChildRecyclerView,ParentRecyclerView和ChildRecyclerView相互協調:
- 當ParentRecyclerView滾動到底部的時候,讓ChildRecyclerView去滾動,
- 當ChildRecyclerView滾動到頂部的時候,讓ParentRecyclerView去滾動,
#### 利用canScrollVertically這個方法來判斷當前View是否滾動到底或者是否滾動到頂,
```java
//ParentRecyclerView
private boolean isScrollEnd() {
//RecyclerView.canScrollVertically(1)的值表示是否能向上滾動,false表示已經滾動到底部
return !canScrollVertically(1);
}
```java
//ChildRecyclerView
boolean isScrollTop() {
//RecyclerView.canScrollVertically(-1)的值表示是否能向下滾動,false表示已經滾動到頂部
return !canScrollVertically(-1);
}
#### ParentRecyclerView和ChildRecyclerView需要拿到對方的參考,以便能夠協調滾動,
自己寫的Demo效果如下:
外層ParentRecyclerView代碼如下(重要注釋已經寫好):
/**
* author : AndyYuan
* date : 2020/12/9 000915:02
* desc : 最外層的RecyclerView 即父類RecyclerView
* version: 2.0
*/
public class ParentRecyclerView extends RecyclerView {
FlingHelper mFlingHelper;
int mMaxDistance = 0;
/**
* 記錄當前滑動的y軸加速度
*/
int velocityY = 0;
/**
* 記錄上次Event事件的y坐標
*/
Float lastY = 0f;
int mTotalDy = 0;
/**
* 用于判斷RecyclerView是否在fling
*/
boolean isStartFling = false;
AtomicBoolean canScrollVertically;
public ParentRecyclerView(@NonNull Context context) {
super(context);
init(context);
}
public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
/**
* 初始化父類滾動事件
* @param context 背景關系當前界面
*/
private void init(Context context) {
mFlingHelper = new FlingHelper(context);
mMaxDistance = mFlingHelper.getVelocityByDistance((double) (UIUtils.getScreenHeight() * 4));
canScrollVertically = new AtomicBoolean(true);
addOnScrollListener(new OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
//如果父RecyclerView fling程序中已經到底部,需要讓子RecyclerView滑動神域的fling
if (newState == SCROLL_STATE_IDLE) {
dispatchChildFling();
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isStartFling) {
mTotalDy = 0;
isStartFling = false;
}
//記錄當前RecyclerView在y軸的偏移
mTotalDy += dy;
}
});
}
/**
* 初始化LayoutManager管理RecyclerView
* @param context 背景關系當前界面
*/
public void initLayoutManager(Context context) {
LinearLayoutManager layoutManager = new LinearLayoutManager(context) {
@Override
public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
try {
return super.scrollVerticallyBy(dy, recycler, state);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
@Override
public void onLayoutChildren(Recycler recycler, State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public boolean canScrollVertically() {
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
//子recycleview 未滑動到頂部父recycleview就開始滑動 按照他說的在父控制元件里面不判斷canScrollVertically.get()這個就可以了 不知道啥問題
//return (canScrollVertically.get() || childRecyclerView == null || childRecyclerView.isScrollTop());
return (childRecyclerView == null || childRecyclerView.isScrollTop());
}
@Override
public void addDisappearingView(View child) {
try {
super.addDisappearingView(child);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
};
layoutManager.setOrientation(VERTICAL);
setLayoutManager(layoutManager);
}
/**
* 事件分發處理
* @param ev 當前事件
* @return 回傳是否分發
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev != null && ev.getAction() == MotionEvent.ACTION_DOWN) {
//ACTION_DOWN的時候重置加速度
velocityY = 0;
stopScroll();
}
if (!(ev == null || ev.getAction() == MotionEvent.ACTION_MOVE)) {
//在非ACTION_MOVE的情況下,將lastY置為0
lastY = 0f;
}
try {
return super.dispatchTouchEvent(ev);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent e) {
if (lastY == 0f) {
lastY = e.getY();
}
if (isScrollEnd()) {
//如果父RecyclerView已經滑動到底部,需要讓子RecyclerView滑動剩余的距離
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (childRecyclerView != null) {
int deltaY = (int) (lastY - e.getY());
canScrollVertically.set(false);
childRecyclerView.scrollBy(0, deltaY);
}
}
if (e.getAction() == MotionEvent.ACTION_UP) {
canScrollVertically.set(true);
}
lastY = e.getY();
try {
return super.onTouchEvent(e);
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
}
/**
* 滑動事件
* @param velx 水平滑動距離
* @param velY 豎直滑動距離
* @return
*/
@Override
public boolean fling(int velx, int velY) {
boolean fling = super.fling(velx, velY);
if (!fling || velY <= 0) {
velocityY = 0;
} else {
isStartFling = true;
velocityY = velY;
}
return fling;
}
private void dispatchChildFling() {
if (isScrollEnd() && velocityY != 0) {
double splineFlingDistance = mFlingHelper.getSplineFlingDistance(velocityY);
if (splineFlingDistance > mTotalDy) {
childFling(mFlingHelper.getVelocityByDistance(splineFlingDistance - mTotalDy));
}
}
mTotalDy = 0;
velocityY = 0;
}
private void childFling(int velocityByDistance) {
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (childRecyclerView != null) {
childRecyclerView.fling(0, velocityByDistance);
}
}
private ChildRecyclerView findNestedScrollingChildRecyclerView() {
if (getAdapter() != null && (getAdapter() instanceof MultiTypeAdapter)) {
return ((MultiTypeAdapter) getAdapter()).getCurrentChildRecyclerView();
}
return null;
}
private boolean isScrollEnd() {
//RecyclerView.canScrollVertically(1)的值表示是否能向上滾動,false表示已經滾動到底部
return !canScrollVertically(1);
}
@Override
public void scrollToPosition(final int position) {
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (childRecyclerView != null) {
childRecyclerView.scrollToPosition(position);
}
postDelayed(new Runnable() {
@Override
public void run() {
ParentRecyclerView.super.scrollToPosition(position);
}
}, 50);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return (target != null) && (target instanceof ChildRecyclerView);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
//1.當前Parent RecyclerView沒有滑動底,且dy> 0 是下滑
boolean isParentCanScroll = dy > 0 && !isScrollEnd();
//2.當前Child RecyclerView滑到頂部了,且dy < 0,即上滑
boolean isChildCanNotScroll = !(dy >= 0
|| childRecyclerView == null
|| !childRecyclerView.isScrollTop());
//以上兩種情況都需要讓Parent RecyclerView去scroll,和下面onNestedPreFling機制類似
if (isParentCanScroll || isChildCanNotScroll) {
scrollBy(0, dy);
consumed[1] = dy;
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return true;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
boolean isParentCanFling = velocityY > 0f && !isScrollEnd();
boolean isChildCanNotFling = !(velocityY >= 0
|| childRecyclerView == null
|| !childRecyclerView.isScrollTop());
if (!isParentCanFling && !isChildCanNotFling) {
return false;
}
fling(0, (int) velocityY);
return true;
}
public boolean isChildRecyclerViewCanScrollUp() {
ChildRecyclerView childRecyclerView = findNestedScrollingChildRecyclerView();
if (childRecyclerView != null) {
return !childRecyclerView.isScrollTop();
}
return false;
}
}
目前ChildRecyclerView是直接通過往上查找的方式來進行的,而ParentRecyclerView需要通過ViewPager來找到當前顯示的是哪一個ChidRecyclerView,
```java
//ChildRecyclerView
private ParentRecyclerView findParentRecyclerView() {
ViewParent parentView = getParent();
while (!(parentView instanceof ParentRecyclerView)) {
parentView = parentView.getParent();
}
return (ParentRecyclerView)parentView;
}
```
```java
//ParentRecyclerView
private ChildRecyclerView findNestedScrollingChildRecyclerView() {
if(getAdapter()!= null && (getAdapter() instanceof MultiTypeAdapter)) {
return ((MultiTypeAdapter)getAdapter()).getCurrentChildRecyclerView();
}
return null;
}
```
內層ChildRecyclerView的代碼比較簡單,如下:
/**
* author : AndyYuan
* date : 2020/12/9 000915:02
* desc : 子類RecyclerView 嵌套在父類中
* version: 2.0
*/
public class ChildRecyclerView extends RecyclerView {
private FlingHelper mFlingHelper;
private int mMaxDistance = 0;
private int mVelocity = 0;
private boolean isStartFling = false;
private int totalDy = 0;
//持有父類的參考
private ParentRecyclerView mParentRecyclerView = null;
public ChildRecyclerView(@NonNull Context context) {
super(context);
init(context);
}
public ChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ChildRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
mFlingHelper = new FlingHelper(context);
mMaxDistance = mFlingHelper.getVelocityByDistance((double)(UIUtils.getScreenHeight() * 4));
setOverScrollMode(OVER_SCROLL_NEVER);
initScrollListener();
}
//設定滑動監聽
private void initScrollListener() {
addOnScrollListener(new OnScrollListener() {
//滾動狀態變化時回呼
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if(newState == SCROLL_STATE_IDLE) {
//靜止沒有滾動
dispatchParentFling();
}
super.onScrollStateChanged(recyclerView, newState);
}
//滾動時回呼
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if(isStartFling) {
totalDy = 0;
isStartFling = false;
}
totalDy += dy;
}
});
}
private void dispatchParentFling() {
mParentRecyclerView = findParentRecyclerView();
if(isScrollTop() && mVelocity != 0) {
//當前ChildRecyclerView已經滑動到頂部,且豎直方向加速度不為0,如果有多余的需要交由父RecyclerView繼續fling
double flingDistance = mFlingHelper.getSplineFlingDistance(mVelocity);
if(flingDistance > (Math.abs(totalDy))) {
mParentRecyclerView.fling(0,-mFlingHelper.getVelocityByDistance(flingDistance + totalDy));
}
totalDy = 0;
mVelocity = 0;
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(ev != null && ev.getAction() == MotionEvent.ACTION_DOWN) {
mVelocity = 0;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean fling(int velocityX, int velocityY) {
if(!isAttachedToWindow()) return false;
boolean fling = super.fling(velocityX, velocityY);
if(!fling || velocityY >=0) {
mVelocity =0;
} else {
isStartFling = true;
mVelocity = velocityY;
}
return fling;
}
boolean isScrollTop() {
//RecyclerView.canScrollVertically(-1)的值表示是否能向下滾動,false表示已經滾動到頂部
return !canScrollVertically(-1);
}
//獲取當前父類RecyclerView
private ParentRecyclerView findParentRecyclerView() {
ViewParent parentView = getParent();
while (!(parentView instanceof ParentRecyclerView)) {
parentView = parentView.getParent();
}
return (ParentRecyclerView)parentView;
}
}
處理fling效果的原理
在RecyclerView的onScrolled記錄總的偏移,當ParentRecyclerView滑動到底部的時候,將多余的需要消費的總偏移轉換成加速度,從而交給子View去Fling,反之,型別,
#### 處理嵌套滾動
如果不處理嵌套滾動,在某些邊界場景下,當我們滑動不松手時會出現無法滑動的問題,
二、框架使用
(1)在專案的最外層build.gradle中的repositories 閉包中新增支持jitpack的庫如下:
maven { url 'https://jitpack.io' }
(2)在app目錄下的build.gradle的dependencies閉包中添加(目前最新版本):
implementation 'com.github.AndyYuan317:NestedVVRecyclerView:2.0.2'
然后點擊Sync Now同步下載,AndroidStudio會自動下載該遠程依賴,完成后在External Libraries中會出現如下圖:

(3)這樣基本集成就完成了,下面開始講解使用,比如直接新建一個Demo只有Hello World!,打開主界面的xml檔案改成如下:
<?xml version="1.0" encoding="utf-8"?>
<com.andyyuan.nestedvvrecyclerview.java.view.StoreSwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true">
<!-- android:focusable="true"
android:focusableInTouchMode="true"
出現滾動的時候在外層獲取焦點來實作初始化后的自動滾動-->
<com.andyyuan.nestedvvrecyclerview.java.view.ParentRecyclerView
android:id="@+id/javaRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never" />
</com.andyyuan.nestedvvrecyclerview.java.view.StoreSwipeRefreshLayout>
(4):主界面的代碼為:
/**
* author : AndyYuan
* date : 2020/12/9 000915:02
* desc : 主界面,只有兩層的RecyclerView嵌套
* version: 2.0
*/
public class MainJavaActivity extends AppCompatActivity {
//父類資料
ArrayList<Object> mDataList = new ArrayList<Object>();
//子類資料
ArrayList<String> mChildDataList = new ArrayList<>();
//子類Fragment
ArrayList<MyFragment> mChildFragmentList = new ArrayList<>();
MultiTypeAdapter adapter = new MultiTypeAdapter(mDataList, mChildDataList, getSupportFragmentManager());
StoreSwipeRefreshLayout storeSwipeRefreshLayout;
//SmartRefreshLayout smartRefreshLayout;
private boolean isRefresh = false;
ParentRecyclerView javaRecyclerView;
Long lastBackPressedTime = 0L;
String[] strArray = new String[]{"推薦", "視頻", "直播", "圖片", "精華", "熱門"};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_java);
javaRecyclerView = findViewById(R.id.javaRecyclerView);
storeSwipeRefreshLayout = findViewById(R.id.swipeRefreshLayout);
//smartRefreshLayout = findViewById(R.id.refreshLayout);
javaRecyclerView.setAdapter(adapter);
javaRecyclerView.initLayoutManager(this);
refresh();
storeSwipeRefreshLayout.setColorSchemeColors(Color.RED);
storeSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
Toast.makeText(MainJavaActivity.this, "資料已重繪", Toast.LENGTH_SHORT).show();
refresh();
}
});
}
@Override
public void onBackPressed() {
if (System.currentTimeMillis() - lastBackPressedTime < 2000) {
super.onBackPressed();
} else {
javaRecyclerView.scrollToPosition(0);
Toast.makeText(this, "再按一次退出程式", Toast.LENGTH_SHORT).show();
lastBackPressedTime = System.currentTimeMillis();
}
}
private void refresh() {
initParentData();
initChildData();
CategoryBean categoryBean = new CategoryBean();
categoryBean.getTabTitleList().clear();
categoryBean.getTabTitleList().addAll(Arrays.asList(strArray));
mDataList.add(categoryBean);
adapter.notifyDataSetChanged();
storeSwipeRefreshLayout.setRefreshing(false);
//第一次滑動出現的卡頓問題
// javaRecyclerView.smoothScrollToPosition(adapter.getItemCount()-1);
// javaRecyclerView.smoothScrollToPosition(0);
//自己新增的,初始化進入會吸頂,
// 也可以直接在父布局中添加android:focusable="true"
// android:focusableInTouchMode="true"
//javaRecyclerView.smoothScrollToPosition(0);
}
//初始化父類資料
private void initParentData() {
mDataList.clear();
for (int i = 0; i < 7; i++) {
mDataList.add("parent item text " + i);
}
}
//初始化子類資料
private void initChildData() {
mChildDataList.clear();
mChildFragmentList.clear();
mChildDataList.addAll(Arrays.asList(strArray));
for (int i = 0; i < mChildDataList.size(); i++) {
//mChildDataList.add("child item " + i );
mChildFragmentList.add(new MyFragment());
}
}
@Override
protected void onDestroy() {
super.onDestroy();
adapter.destroy();
}
}
(5):其中MultiTypeAdapter是連接外層RecyclerView與內層的關鍵,代碼如下:
/**
* author : AndyYuan
* date : 2020/12/9 000915:02
* desc : 主界面中的ParentAdapter 子item都放在該層中顯示,子recyclerView放在最后一個item中
* version: 2.0
*/
public class MultiTypeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private ArrayList<Object> mDataList;
private ArrayList<String> mChildDataList;
public static final int TYPE_TEXT = 0;
public static final int TYPE_CATEGORY = 1;
public static final int TYPE_FIRST = 3;
MyCategoryViewHolder mCategoryViewHolder;
//主界面傳遞過來的FragmentManager
private FragmentManager fragmentManager;
public MultiTypeAdapter(ArrayList<Object> dataList, ArrayList<String> childData, FragmentManager fragmentManager) {
mDataList = dataList;
mChildDataList = childData;
this.fragmentManager = fragmentManager;
}
@Override
public int getItemViewType(int position) {
if (mDataList.get(position) instanceof String) {
if (position == 0) {
return TYPE_FIRST;
}
return TYPE_TEXT;
} else {
return TYPE_CATEGORY;
}
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
if (viewType == TYPE_FIRST) {
return new SimpleFristViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.layout_item_first_view_holer
, viewGroup, false));
} else if (viewType == TYPE_TEXT) {
return new SimpleTextViewHolder(LayoutInflater.from(
viewGroup.getContext()
).inflate(R.layout.layout_item_text, viewGroup, false));
} else {
// SimpleCategoryViewHolder simpleCategoryViewHolder =
// new SimpleCategoryViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(
// R.layout.layout_item_category_default,
// viewGroup,
// false
// ));
// mCategoryViewHolder = simpleCategoryViewHolder;
// return simpleCategoryViewHolder;
//自己替換Fragment的ViewPager
MyCategoryViewHolder simpleCategoryViewHolder =
new MyCategoryViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(
R.layout.layout_item_category_holder,
viewGroup,
false
), fragmentManager, mChildDataList);
mCategoryViewHolder = simpleCategoryViewHolder;
return simpleCategoryViewHolder;
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int pos) {
if (viewHolder instanceof SimpleTextViewHolder) {
// Log.d("gaohui","pos " + pos + mDataList.get(pos) + mDataList.get(pos));
((SimpleTextViewHolder) viewHolder).mTv.setText((String) mDataList.get(pos));
} else if (viewHolder instanceof MyCategoryViewHolder) {
//((SimpleCategoryViewHolder)viewHolder).bindData(mDataList.get(pos));
((MyCategoryViewHolder) viewHolder).bindData();
}
}
@Override
public int getItemCount() {
return mDataList.size();
}
public ChildRecyclerView getCurrentChildRecyclerView() {
if (mCategoryViewHolder != null) {
return mCategoryViewHolder.getCurrentChildRecyclerView();
}
return null;
}
public void destroy() {
if (mCategoryViewHolder != null) {
//mCategoryViewHolder.destroy();
}
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/237680.html
標籤:其他
上一篇:深入Android系統(八)Android的資源管理
下一篇:Android 9.0自動更新 安裝包決議錯誤 java.lang.SecurityException: Permission Denial
