Android模仿實(shí)現(xiàn)閑魚(yú)首頁(yè)的思路與方法
首先我們來(lái)看看效果圖
Demo是基于MVVM模式來(lái)編寫(xiě)的,歡迎大家給予批評(píng)和指正。
其中Banner的無(wú)限輪播用了PageSnapHelper,后續(xù)RecycleView也可以實(shí)現(xiàn)更多類似ViewPage的效果了
可以看出頁(yè)面大概可以分為這幾個(gè)部分
1.最上面是一個(gè)輪播的Banner
2.中間可能有些其他的功能列表
3.最后是Tab頁(yè)(這里是新鮮的和附近的兩個(gè)列表)
OK,看到這樣的布局需求的時(shí)候可能有兩種思路
1、整體是一個(gè)RefreshLayout布局,內(nèi)嵌RecycleView,而B(niǎo)anner頁(yè),其他功能列表以及TabLayout都當(dāng)成RecycleView的頭加入到RecycleView中,TabLayout下面是真正的列表項(xiàng)
2、整體還是一個(gè)RefreshLayout布局,內(nèi)部是一個(gè)NestScrollView,Banner頁(yè),其他功能列表,TabLayout依次布局在NestScrollView中,然后最下面布局一個(gè)FrameLayout,TabLayout切換的時(shí)候切換不同的Fragment
Demo中使用的是第一種方式,第二種方式考慮到SwipeRefreshLayout和內(nèi)部FrameLayout的滑動(dòng)會(huì)有沖突,后續(xù)再嘗試編寫(xiě)
接下來(lái)考慮需要考慮的問(wèn)題
- TabLayout需要固定到頂部
- 第一次加載數(shù)據(jù)的時(shí)候需要有個(gè)Loading提示,Demo中就是一個(gè)小魚(yú)的空白等待頁(yè)
- 因?yàn)槭褂靡粋€(gè)數(shù)據(jù)集,在TabLayout來(lái)回切換的時(shí)候需要保證數(shù)據(jù)集合所在的位置是正確的(比如新鮮的這個(gè)列表當(dāng)前在Position1的位置,切換到附近的列表我滑到了Position2的位置,當(dāng)我再切回新鮮的時(shí)候需要回到Position1的位置)
下面就一些核心的代碼和思路講解一下
首先是布局,布局很簡(jiǎn)單,SwipeRefreshLayout中包了一個(gè)FrameLayout,然后在FrameLayout中包含了一個(gè)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>
接下來(lái)看下StickyHead如何實(shí)現(xiàn)
//正常的TabLayout布局 private TabLayout mTabLayout; //粘性 TabLayout布局(用于固定在頂部) private TabLayout mStickyTabLayout; //粘性布局的Y坐標(biāo)(用戶判斷粘性布局是否顯示) private int mStickyPositionY; //主列表布局 private RecyclerView mHomeList;
這是變量的定義,下面的這個(gè)類是我將一些頁(yè)面邏輯涉及的變量抽離出來(lái)
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; //首頁(yè)是否正在下拉刷新 private boolean refreshing; //新鮮的和附近的 獲取更多的View的狀態(tài)值(用戶記錄TabLayout切換的時(shí)候,LoadingMore的狀態(tài)) private int refreshMoreStatus; private int nearMoreStatus; //首頁(yè)的活動(dòng)更多的狀態(tài) private int loadingMoreStatus; }
這是變量的定義,然后初始化兩個(gè)TabLayout,主要在于需要監(jiān)聽(tīng)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) { } });
這段的邏輯比較簡(jiǎn)單,就是實(shí)現(xiàn)了保持TabLayout切換狀態(tài)的統(tǒng)一,當(dāng)TabLayout切換的時(shí)候,需要將StickyTabLayout所選中的Tab也設(shè)置一下,mViewModel.changeHomeData(position)
這句話是為了切換數(shù)據(jù),下面會(huì)分析到
接下來(lái)是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),則將粘貼頭部加入到布局中來(lái)并顯示,否則,將粘貼頭部布局從布局中移除。判斷count這個(gè)值是為了防止重復(fù)添加和重復(fù)移除粘貼頭布局。
mBinding.layoutRefresh.setEnabled(true/false)
是為了在粘貼頭部固定在頂上的時(shí)候消除掉外層SwipeRefreshLayout的下拉刷新錯(cuò)誤。注釋掉的代碼會(huì)在下面再講
只需要上面的這么多代碼一個(gè)StickyHead就實(shí)現(xiàn)了,在測(cè)試的時(shí)候遇到點(diǎn)小問(wèn)題,就是焦點(diǎn)重置導(dǎo)致的RecycleView重新回到初始位置的一個(gè)錯(cuò)誤,下面是暫時(shí)的解決方案
LinearLayoutManager manager = new LinearLayoutManager(mContainer.getContext()) { @Override public boolean onRequestChildFocus(RecyclerView parent, RecyclerView.State state, View child, View focused) { //TODO 暫時(shí)處理View焦點(diǎn)問(wèn)題 return true; } };
下面簡(jiǎn)單說(shuō)下如何實(shí)現(xiàn)首次加載新鮮的或者附近的數(shù)據(jù)的時(shí)候出現(xiàn)的一個(gè)等待頁(yè)面
主要思路是這樣的
1、這個(gè)等待的LoadingView是當(dāng)成RecycleView的頭加在TabLayout后面的,當(dāng)數(shù)據(jù)加載完成這個(gè)LoadingView設(shè)置為不可見(jiàn)
2、因?yàn)橛蠺abLayout會(huì)切換,導(dǎo)致RecycleView的數(shù)據(jù)會(huì)重新繪制,進(jìn)而導(dǎo)致RecyView會(huì)回到初始位置,所以需要記錄下RecycleView所在的位置,然后手動(dòng)滑動(dòng)到記錄的位置
具體的我們還是來(lái)看代碼吧
private int mHomeListPositionY;//用來(lái)標(biāo)識(shí)當(dāng)前RecycleView的位置 private int mInitPositionY = -1;//初始狀態(tài)下RecycleView的Y坐標(biāo) //這里是RecycleView的滑動(dòng)監(jiān)聽(tīng),用來(lái)記錄RecycleView的位置,這里其實(shí)是記錄了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]; } }); //這個(gè)函數(shù)就是用來(lái)手動(dòng)將RecycleView滑動(dòng)到正確的位置 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); } } }
最后來(lái)看下新鮮的和附近的加載更多時(shí)頁(yè)面的實(shí)現(xiàn)
這里Adapter使用了第三方BRVAH,所以相對(duì)LoadingMore的狀態(tài)BRVAH幫我封了一下,因?yàn)殡m然是一個(gè)List,但其實(shí)是兩個(gè)列表復(fù)用一個(gè)List的,所以這里的LoadingMore狀態(tài)我們需要記錄兩個(gè),方便切換的時(shí)候列表的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)聽(tīng)首次加載,這里
if (mLoadingView.getVisibility() == View.GONE) { mAdapter.setEnableLoadMore(true); }
這個(gè)是為了防止首次加載顯示Loading頁(yè)面的時(shí)候又顯示了LoadingMore布局
BR.loadingMoreStatus
這個(gè)就是監(jiān)聽(tīng)LoadingMore的狀態(tài)來(lái)更新List的Adapter
其他的主要ViewModel代碼在HomeViewModel中。
主要的幾個(gè)點(diǎn)
- 粘貼頭布局的邏輯
- TabLayout切換導(dǎo)致數(shù)據(jù)集變化以及位置的變化
- 加載更多的時(shí)候需要考慮TabLayout切換的問(wèn)題
項(xiàng)目鏈接:https://github.com/ly85206559/demo4Fish
本地下載:點(diǎn)擊這里
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Android自定義dialog 自下往上彈出的實(shí)例代碼
本文通過(guò)實(shí)例代碼給大家介紹了Android自定義dialog 自下往上彈出效果,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2018-08-08android利用handler實(shí)現(xiàn)打地鼠游戲
這篇文章主要為大家詳細(xì)介紹了android利用handler實(shí)現(xiàn)打地鼠游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11Android創(chuàng)建一個(gè)Activity的方法分析
這篇文章主要介紹了Android創(chuàng)建一個(gè)Activity的方法,結(jié)合實(shí)例形式分析了Android創(chuàng)建Activity的具體步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-04-04Windows下搭建Android開(kāi)發(fā)環(huán)境
這篇文章主要介紹了Windows下搭建Android開(kāi)發(fā)環(huán)境,需要的朋友可以參考下2015-09-09Android開(kāi)發(fā)之DialogFragment用法實(shí)例總結(jié)
這篇文章主要介紹了Android開(kāi)發(fā)之DialogFragment用法,結(jié)合實(shí)例形式總結(jié)分析了Android使用DialogFragment代替Dialog功能的相關(guān)使用技巧與注意事項(xiàng),需要的朋友可以參考下2017-11-11Android 使用PopupWindow實(shí)現(xiàn)彈出更多的菜單實(shí)例詳解
最近想要做一個(gè)彈出更多的菜單,而原生的彈出菜單卻不是我們想要的效果,所以必然要自定義菜單。接下來(lái)通過(guò)本文給大家介紹android 使用popupwindow實(shí)現(xiàn)彈出更多的菜單實(shí)例詳解,需要的朋友可以參考下2017-04-04