先放一個效果圖
Demo是基于MVVM模式來編寫的,歡迎大家給予批評和指正悴侵。
其中Banner的無限輪播用了PageSnapHelper,后續(xù)RecycleView也可以實現(xiàn)更多類似ViewPage的效果了
項目鏈接:https://github.com/ly85206559/demo4Fish
如果能對你有幫助那就最好了
可以看出頁面大概可以分為這幾個部分
1.最上面是一個輪播的Banner
2.中間可能有些其他的功能列表
3.最后是Tab頁(這里是新鮮的和附近的兩個列表)
OK豌习,看到這樣的布局需求的時候可能有兩種思路
- 整體是一個RefreshLayout布局步责,內(nèi)嵌RecycleView,而Banner頁叠纷,其他功能列表以及TabLayout都當(dāng)成RecycleView的頭加入到RecycleView中刻帚,TabLayout下面是真正的列表項
- 整體還是一個RefreshLayout布局潦嘶,內(nèi)部是一個NestScrollView涩嚣,Banner頁,其他功能列表掂僵,TabLayout依次布局在NestScrollView中航厚,然后最下面布局一個FrameLayout,TabLayout切換的時候切換不同的Fragment
Demo中使用的是第一種方式锰蓬,第二種方式考慮到SwipeRefreshLayout和內(nèi)部FrameLayout的滑動會有沖突幔睬,后續(xù)再嘗試編寫
接下來考慮需要考慮的問題
- TabLayout需要固定到頂部
- 第一次加載數(shù)據(jù)的時候需要有個Loading提示,Demo中就是一個小魚的空白等待頁
- 因為使用一個數(shù)據(jù)集芹扭,在TabLayout來回切換的時候需要保證數(shù)據(jù)集合所在的位置是正確的(比如新鮮的這個列表當(dāng)前在Position1的位置麻顶,切換到附近的列表我滑到了Position2的位置,當(dāng)我再切回新鮮的時候需要回到Position1的位置)
下面就一些核心的代碼和思路講解一下
首先是布局舱卡,布局很簡單辅肾,SwipeRefreshLayout中包了一個FrameLayout,然后在FrameLayout中包含了一個RecycleView
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/layout_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</android.support.v4.widget.SwipeRefreshLayout>
接下來看下StickyHead如何實現(xiàn)
//正常的TabLayout布局
private TabLayout mTabLayout;
//粘性 TabLayout布局(用于固定在頂部)
private TabLayout mStickyTabLayout;
//粘性布局的Y坐標(biāo)(用戶判斷粘性布局是否顯示)
private int mStickyPositionY;
//主列表布局
private RecyclerView mHomeList;
這是變量的定義轮锥,下面的這個類是我將一些頁面邏輯涉及的變量抽離出來
public class HomeEntity extends BaseObservable {
//列表類型 0:新鮮的 1:附近的
public static final int LIST_TYPE_FRESH = 0;
public static final int LIST_TYPE_NEAR = 1;
private int bannerCount;
private int listType = LIST_TYPE_FRESH;
//新鮮的和附近的首次加載的loading狀態(tài)
private boolean refreshLoading;
private boolean nearLoading;
//首頁是否正在下拉刷新
private boolean refreshing;
//新鮮的和附近的 獲取更多的View的狀態(tài)值(用戶記錄TabLayout切換的時候矫钓,LoadingMore的狀態(tài))
private int refreshMoreStatus;
private int nearMoreStatus;
//首頁的活動更多的狀態(tài)
private int loadingMoreStatus;
}
這是變量的定義,然后初始化兩個TabLayout舍杜,主要在于需要監(jiān)聽TabLayout的切換
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
int position = tab.getPosition();
//設(shè)置粘貼TabLayout的選中Tab
if (!mStickyTabLayout.getTabAt(position).isSelected()) {
mStickyTabLayout.getTabAt(position).select();
mViewModel.changeHomeData(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
mStickyTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
int position = tab.getPosition();
if (!mTabLayout.getTabAt(position).isSelected()) {
mTabLayout.getTabAt(position).select();
mHomeList.stopScroll();
//mAdapter.setEnableLoadMore(false);
mViewModel.changeHomeData(position);
......
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
這段的邏輯比較簡單新娜,就是實現(xiàn)了保持TabLayout切換狀態(tài)的統(tǒng)一,當(dāng)TabLayout切換的時候既绩,需要將StickyTabLayout所選中的Tab也設(shè)置一下概龄,mViewModel.changeHomeData(position)這句話是為了切換數(shù)據(jù),下面會分析到
接下來是StickyHead重要的代碼
mHomeList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int[] location = new int[2];
mTabLayout.getLocationInWindow(location);
int count = mViewContainer.getChildCount();
if (location[1] <= mStickyPositionY) {
if (count == 1) {
mViewContainer.addView(mStickyTabLayout);
mBinding.layoutRefresh.setEnabled(false);
}
} else {
if (count > 1) {
mViewContainer.removeView(mStickyTabLayout);
//mOffsetY = DisplayUtil.dip2px(mContainer.getContext(), 46);
//mRefreshPosition = mAdapter.getHeaderLayoutCount();
//mNearPosition = mAdapter.getHeaderLayoutCount();
mBinding.layoutRefresh.setEnabled(true);
}
}
//if (mInitPositionY == -1) {
//mInitPositionY = location[1];
//}
//mHomeListPositionY = location[1];
}
});
主要邏輯就是先獲取TabLayout在窗口的位置饲握,如果Y坐標(biāo)小于粘貼頭部的Y坐標(biāo)私杜,則將粘貼頭部加入到布局中來并顯示吸重,否則,將粘貼頭部布局從布局中移除歪今。判斷count這個值是為了防止重復(fù)添加和重復(fù)移除粘貼頭布局嚎幸。mBinding.layoutRefresh.setEnabled(true/false)是為了在粘貼頭部固定在頂上的時候消除掉外層SwipeRefreshLayout的下拉刷新錯誤。注釋掉的代碼會在下面再講
只需要上面的這么多代碼一個StickyHead就實現(xiàn)了寄猩,在測試的時候遇到點(diǎn)小問題嫉晶,就是焦點(diǎn)重置導(dǎo)致的RecycleView重新回到初始位置的一個錯誤,下面是暫時的解決方案
LinearLayoutManager manager = new LinearLayoutManager(mContainer.getContext()) {
@Override
public boolean onRequestChildFocus(RecyclerView parent, RecyclerView.State state, View child, View focused) {
//TODO 暫時處理View焦點(diǎn)問題
return true;
}
};
下面簡單說下如何實現(xiàn)首次加載新鮮的或者附近的數(shù)據(jù)的時候出現(xiàn)的一個等待頁面
主要思路是這樣的
這個等待的LoadingView是當(dāng)成RecycleView的頭加在TabLayout后面的田篇,當(dāng)數(shù)據(jù)加載完成這個LoadingView設(shè)置為不可見
-
因為有TabLayout會切換替废,導(dǎo)致RecycleView的數(shù)據(jù)會重新繪制,進(jìn)而導(dǎo)致RecyView會回到初始位置泊柬,所以需要記錄下RecycleView所在的位置椎镣,然后手動滑動到記錄的位置
具體的我們還是來看代碼吧private int mHomeListPositionY;//用來標(biāo)識當(dāng)前RecycleView的位置
private int mInitPositionY = -1;//初始狀態(tài)下RecycleView的Y坐標(biāo)
//這里是RecycleView的滑動監(jiān)聽,用來記錄RecycleView的位置兽赁,這里其實是記錄了mTabLayout的位置
mHomeList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int[] location = new int[2];
mTabLayout.getLocationInWindow(location);
int count = mViewContainer.getChildCount();if (mInitPositionY == -1) { mInitPositionY = location[1]; } mHomeListPositionY = location[1]; } });
//這個函數(shù)就是用來手動將RecycleView滑動到正確的位置
private void setLoadingView(boolean visible, int type) {
int position;
if (type == HomeEntity.LIST_TYPE_FRESH) {
position = mRefreshPosition;
} else {
position = mNearPosition;
}
if (visible) {
mLoadingView.setVisibility(View.VISIBLE);
if (mStickyTabLayout.getVisibility() == View.VISIBLE) {
LinearLayoutManager layoutManager = (LinearLayoutManager) mHomeList.getLayoutManager();
layoutManager.scrollToPositionWithOffset(0, mHomeListPositionY - mInitPositionY);
}
} else {
mLoadingView.setVisibility(View.GONE);
LinearLayoutManager layoutManager = (LinearLayoutManager) mHomeList.getLayoutManager();
if (mViewContainer.getChildCount() > 1) {
layoutManager.scrollToPositionWithOffset(position, mStickyTabLayout.getHeight());
} else {
layoutManager.scrollToPositionWithOffset(0, mHomeListPositionY - mInitPositionY);
}
}
}
最后來看下新鮮的和附近的加載更多時頁面的實現(xiàn)
這里Adapter使用了第三方BRVAH状答,所以相對LoadingMore的狀態(tài)BRVAH幫我封了一下,因為雖然是一個List刀崖,但其實是兩個列表復(fù)用一個List的惊科,所以這里的LoadingMore狀態(tài)我們需要記錄兩個,方便切換的時候列表的LoadingMore狀態(tài)是正確的亮钦,下面看下主要代碼
if (propertyId == BR.refreshLoading) {
if (HomeEntity.LIST_TYPE_FRESH != entity.getListType()) {
return;
}
if (mLoadingView.getVisibility() == View.GONE) {
mAdapter.setEnableLoadMore(true);
}
} else if (propertyId == BR.nearLoading) {
if (HomeEntity.LIST_TYPE_NEAR != entity.getListType()) {
return;
}
if (mLoadingView.getVisibility() == View.GONE) {
mAdapter.setEnableLoadMore(true);
}
} else if (propertyId == BR.loadingMoreStatus) {
int status = entity.getLoadingMoreStatus();
mAdapter.setEnableLoadMore(true);
if (LoadMoreView.STATUS_DEFAULT == status) {
mAdapter.loadMoreComplete();
} else if (LoadMoreView.STATUS_END == status) {
mAdapter.loadMoreEnd();
} else if (LoadMoreView.STATUS_FAIL == status) {
mAdapter.loadMoreFail();
}
}
BR.refreshLoading和BR.nearLoading 都是監(jiān)聽首次加載馆截,這里
if (mLoadingView.getVisibility() == View.GONE) {
mAdapter.setEnableLoadMore(true);
}
這個是為了防止首次加載顯示Loading頁面的時候又顯示了LoadingMore布局
BR.loadingMoreStatus這個就是監(jiān)聽LoadingMore的狀態(tài)來更新List的Adapter
其他的主要ViewModel代碼在HomeViewModel中。主要的幾個點(diǎn)
- 粘貼頭布局的邏輯
- TabLayout切換導(dǎo)致數(shù)據(jù)集變化以及位置的變化
- 加載更多的時候需要考慮TabLayout切換的問題