首先我們來看看效果圖

Demo是基于MVVM模式來編寫的,歡迎大家給予批評和指正。
其中Banner的無限輪播用了PageSnapHelper,后續RecycleView也可以實現更多類似ViewPage的效果了
可以看出頁面大概可以分為這幾個部分
1.最上面是一個輪播的Banner
2.中間可能有些其他的功能列表
3.最后是Tab頁(這里是新鮮的和附近的兩個列表)
OK,看到這樣的布局需求的時候可能有兩種思路
1、整體是一個RefreshLayout布局,內嵌RecycleView,而Banner頁,其他功能列表以及TabLayout都當成RecycleView的頭加入到RecycleView中,TabLayout下面是真正的列表項
2、整體還是一個RefreshLayout布局,內部是一個NestScrollView,Banner頁,其他功能列表,TabLayout依次布局在NestScrollView中,然后最下面布局一個FrameLayout,TabLayout切換的時候切換不同的Fragment
Demo中使用的是第一種方式,第二種方式考慮到SwipeRefreshLayout和內部FrameLayout的滑動會有沖突,后續再嘗試編寫
接下來考慮需要考慮的問題
下面就一些核心的代碼和思路講解一下
首先是布局,布局很簡單,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如何實現
//正常的TabLayout布局private TabLayout mTabLayout;//粘性 TabLayout布局(用于固定在頂部)private TabLayout mStickyTabLayout;//粘性布局的Y坐標(用戶判斷粘性布局是否顯示)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狀態private boolean refreshLoading;private boolean nearLoading;//首頁是否正在下拉刷新private boolean refreshing;//新鮮的和附近的 獲取更多的View的狀態值(用戶記錄TabLayout切換的時候,LoadingMore的狀態)private int refreshMoreStatus;private int nearMoreStatus;//首頁的活動更多的狀態private int loadingMoreStatus;}這是變量的定義,然后初始化兩個TabLayout,主要在于需要監聽TabLayout的切換
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { int position = tab.getPosition(); //設置粘貼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) { } });這段的邏輯比較簡單,就是實現了保持TabLayout切換狀態的統一,當TabLayout切換的時候,需要將StickyTabLayout所選中的Tab也設置一下,mViewModel.changeHomeData(position)這句話是為了切換數據,下面會分析到
接下來是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坐標小于粘貼頭部的Y坐標,則將粘貼頭部加入到布局中來并顯示,否則,將粘貼頭部布局從布局中移除。判斷count這個值是為了防止重復添加和重復移除粘貼頭布局。
mBinding.layoutRefresh.setEnabled(true/false)是為了在粘貼頭部固定在頂上的時候消除掉外層SwipeRefreshLayout的下拉刷新錯誤。注釋掉的代碼會在下面再講
只需要上面的這么多代碼一個StickyHead就實現了,在測試的時候遇到點小問題,就是焦點重置導致的RecycleView重新回到初始位置的一個錯誤,下面是暫時的解決方案
LinearLayoutManager manager = new LinearLayoutManager(mContainer.getContext()) { @Override public boolean onRequestChildFocus(RecyclerView parent, RecyclerView.State state, View child, View focused) { //TODO 暫時處理View焦點問題 return true; } };下面簡單說下如何實現首次加載新鮮的或者附近的數據的時候出現的一個等待頁面
主要思路是這樣的
1、這個等待的LoadingView是當成RecycleView的頭加在TabLayout后面的,當數據加載完成這個LoadingView設置為不可見
2、因為有TabLayout會切換,導致RecycleView的數據會重新繪制,進而導致RecyView會回到初始位置,所以需要記錄下RecycleView所在的位置,然后手動滑動到記錄的位置
具體的我們還是來看代碼吧
private int mHomeListPositionY;//用來標識當前RecycleView的位置private int mInitPositionY = -1;//初始狀態下RecycleView的Y坐標//這里是RecycleView的滑動監聽,用來記錄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]; } });//這個函數就是用來手動將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); } }}最后來看下新鮮的和附近的加載更多時頁面的實現
這里Adapter使用了第三方BRVAH,所以相對LoadingMore的狀態BRVAH幫我封了一下,因為雖然是一個List,但其實是兩個列表復用一個List的,所以這里的LoadingMore狀態我們需要記錄兩個,方便切換的時候列表的LoadingMore狀態是正確的,下面看下主要代碼
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 都是監聽首次加載,這里
if (mLoadingView.getVisibility() == View.GONE) {mAdapter.setEnableLoadMore(true);}這個是為了防止首次加載顯示Loading頁面的時候又顯示了LoadingMore布局
BR.loadingMoreStatus這個就是監聽LoadingMore的狀態來更新List的Adapter
其他的主要ViewModel代碼在HomeViewModel中。
主要的幾個點
項目鏈接:https://github.com/ly85206559/demo4Fish
本地下載:點擊這里
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。
新聞熱點
疑難解答