前言
現(xiàn)在這個(gè)功能的框架也挺多的了谷饿。之所以要寫是因?yàn)檫@個(gè)框架是自己親手實(shí)現(xiàn)的惶我。說起來有點(diǎn)小激動,這是我正經(jīng)寫出來的第一個(gè)框架博投。對于"不要重復(fù)造輪子"這句話绸贡,我一直不是太認(rèn)同,得從不同的維度看毅哗。如果從使用上來看听怕,當(dāng)然沒必要重復(fù)造輪子,白白費(fèi)時(shí)費(fèi)力不劃算黎做。但是如果從個(gè)人學(xué)習(xí)的角度來看的話叉跛,重復(fù)造輪子不但應(yīng)該去做,而且很有必要蒸殿。只會使用輪子對個(gè)人的成長幫助不大。你得知道它是怎么工作的,它為什么能夠這樣工作宏所,然后更進(jìn)一步的話酥艳,看看我還能不能改進(jìn)它?而學(xué)習(xí)輪子效果最好的方法爬骤,我認(rèn)為就是自己再造一個(gè)輪子充石。說白了你來山寨一個(gè),如果可以霞玄,就改進(jìn)它骤铃!
正文
項(xiàng)目地址
demo和library源碼地址:https://github.com/zhangyuChen1991/PtrSwipeMenuRecyclerView
效果
和常見的側(cè)滑以及下拉刷新效果一樣,見下圖:
側(cè)滑菜單效果:
下拉刷新效果:
上拉加載效果:
效果圖就是這樣坷剧,基本使用在上面源碼地址中都有惰爬,步驟非常簡便。下面主要想說的惫企,是它實(shí)現(xiàn)的基本原理撕瞧。
1.側(cè)滑原理
側(cè)滑的主要實(shí)現(xiàn),靠的是一個(gè)自定義的布局容器狞尔。項(xiàng)目中類名為:SwipeMenuLayout丛版,繼承FrameLayout.
public class SwipeMenuLayout extends FrameLayout
它有兩個(gè)成員:
private View contentView;
private LinearLayout menuView;
一目了然,一個(gè)是內(nèi)容偏序,一個(gè)是菜單页畦。內(nèi)容自然就是RecyclerView條目中的布局內(nèi)容,菜單則是自定義的菜單布局研儒。它作為一個(gè)容器豫缨,包含了這兩個(gè)子布局。
重點(diǎn)是殉摔,怎么讓兩個(gè)子布局歸位到自己的初始位置呢州胳?內(nèi)容布局鋪滿整個(gè)寬度,菜單布局放在屏幕外邊逸月。簡單看看下面的代碼:
//init()方法中執(zhí)行下面三句
LayoutParams contentParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
contentView.setLayoutParams(contentParams);
menuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
//重寫onLayout()方法
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.d(TAG, "contentView.getWidth() = " + contentView.getWidth() + "contentView.getHeight() = " + contentView.getHeight());
super.onLayout(changed, left, top, right, bottom);
int contentViewWidth = contentView.getWidth();
int contentViewHeight = contentView.getHeight();
int menuViewWidth = menuView.getWidth();
if (contentView != null && contentViewWidth != 0 && contentViewHeight != 0) {
contentView.layout(0, 0, contentView.getWidth(), contentView.getHeight());
if (menuView != null) {
menuView.layout(contentViewWidth, 0, contentViewWidth + menuViewWidth, contentViewHeight);
}
}
}
首先對設(shè)置進(jìn)來的內(nèi)容布局和菜單布局設(shè)定寬高栓撞,內(nèi)容布局寬度鋪滿整個(gè)屏幕。菜單布局的寬度為包裹內(nèi)容碗硬。然后瓤湘,決定子控件的位置就是在onLayout()方法中進(jìn)行的,所以重寫onLayout()方法恩尾,根據(jù)內(nèi)容布局和菜單布局的寬和高來執(zhí)行它們的layout()方法弛说。可以看到翰意,內(nèi)容布局的寬是鋪滿整個(gè)屏幕的木人,菜單布局的寬度范圍是contentViewWidth到contentViewWidth+menuViewWidth信柿,也就是從內(nèi)容布局的寬度終點(diǎn)位置到這個(gè)位置加上自己寬度的位置,就剛好在屏幕外面了醒第。
初始化位置搞定之后渔嚷,就要開始處理它的滑動事件了,要把菜單側(cè)滑出來稠曼,重寫onTouchEvent()方法形病。這里需要自己來實(shí)現(xiàn)smoothScroll()等功能,細(xì)節(jié)上要處理控制具體可滑動方向霞幅,菜單自動打開漠吻、自動關(guān)閉等問題,具體實(shí)現(xiàn)請參考代碼司恳。整個(gè)SwipeMenuLayout也就兩三百行代碼途乃,并不復(fù)雜。
處理完側(cè)滑菜單的具體實(shí)現(xiàn)之后抵赢,就要考慮把它放到RecyclerView里面去欺劳,作為默認(rèn)的ItemView。當(dāng)使用者設(shè)置他自己的ItemView時(shí)铅鲤,將其作為SwipeMenuLayout的內(nèi)容布局划提,然后加上構(gòu)造的菜單布局(使用者自定義的),返回SwipeMenuLayout作為新的ItemView邢享,這樣鹏往,每一個(gè)Item就都具備側(cè)滑菜單的效果了。
要做以上的事情骇塘,不可避免的伊履,需要重寫Adapter,這里承擔(dān)這個(gè)角色的是SwipeMenuAdapter款违,繼承RecyclerView.Adapter唐瀑。
public abstract class SwipeMenuAdapter<V extends PtrSwipeMenuRecyclerView.ViewHolder> extends RecyclerView.Adapter
細(xì)心的同學(xué)會發(fā)現(xiàn)這里ViewHolder和默認(rèn)的不一樣,確實(shí)插爹,ViewHolder也重寫了哄辣,主要是為了設(shè)置菜單的點(diǎn)擊事件監(jiān)聽,這里先不討論它赠尾。
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
menuView = createMenuView(parent, viewType);
contentView = createContentView(parent, viewType);
SwipeMenuLayout swipeMenuLayout = new SwipeMenuLayout(parent.getContext(), contentView, menuView);
return onCreateThisViewHolder(swipeMenuLayout, viewType);
}
/**
* 創(chuàng)建item內(nèi)容的view布局
*
* @param parent
* @param viewType
* @return
*/
protected abstract View createContentView(ViewGroup parent, int viewType);
/**
* 創(chuàng)建菜單view的布局
*
* @return
*/
protected abstract LinearLayout createMenuView(ViewGroup parent, int viewType);
/**
* 創(chuàng)建ViewHolder
*
* @param contentView 已經(jīng)在createContentView()中創(chuàng)建好力穗,然后經(jīng)過再次包裹了側(cè)滑菜單布局的itemview
* @param viewType
* @return
*/
public abstract RecyclerView.ViewHolder onCreateThisViewHolder(ViewGroup contentView, int viewType);
這里有三個(gè)抽象方法,createMenuView()創(chuàng)建一個(gè)菜單布局气嫁,由使用者自己實(shí)現(xiàn)当窗,createContentView()創(chuàng)建一個(gè)內(nèi)容布局,同樣由使用者來實(shí)現(xiàn)寸宵。onCreateThisViewHolder則是替代了原來的onCreateViewHolder()方法崖面,用來返回一個(gè)ViewHolder元咙,但是在這里返回的ViewHolder,其實(shí)已經(jīng)是item被包裹了SwipeMenuLayout的item了嘶朱,實(shí)現(xiàn)了側(cè)滑菜單的功能蛾坯。
到這里光酣,側(cè)滑菜單的主干實(shí)現(xiàn)原理就大致說完了疏遏。下面看下拉刷新和自動加載。
2.下拉刷新及自動加載原理
下拉刷新效果總體的流程就是:控制touch事件救军,根據(jù)手指滑動動態(tài)的改變頭部HeaderView的高度和其內(nèi)部View的狀態(tài)财异,達(dá)到好像控件被拉下來觸發(fā)刷新的效果。(當(dāng)然也有根據(jù)手指滑動往下滾動View的實(shí)現(xiàn)方法不是這里用的不去多講)
以前ListView做下拉刷新的時(shí)候唱遭,在頂部會增加一個(gè)Header作為下拉刷新頭戳寸,而ListView也已經(jīng)封裝了setHeader()方法,十分方便拷泽。但是RecyclerView沒有疫鹊,所以實(shí)現(xiàn)下拉刷新的第一個(gè)任務(wù)就是給RecyclerView增加一個(gè)HeaderView作為下拉刷新頭。
增加HeaderView司致,其實(shí)就是在RecyclerView的第0個(gè)位置放上自己特定的一個(gè)View拆吆,用來實(shí)現(xiàn)下拉刷新的效果。首先我們封裝一個(gè)HeaderView脂矫,相當(dāng)于一個(gè)自定義布局枣耀,方便下拉刷新效果變化的管理(代碼略,請參考源碼)庭再。
要增加HeaderView捞奕,又得去重寫Adapter了,好在上面做側(cè)滑菜單的時(shí)候已經(jīng)重寫了拄轻,所以把SwipeMenuAdapter拿出來颅围,繼續(xù)添加代碼。主要是onCreateViewHolder()方法恨搓,然后牽涉到getItemCount()和getItemViewType()等方法院促。由于自動加載更多所添加的FooterView與HeaderView是同樣的原理,所以就一并說吧奶卓。先看代碼:
public class HeaderFooterViewHolder extends RecyclerView.ViewHolder {
public HeaderFooterViewHolder(View itemView) {
super(itemView);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == HeaderType) {
headerViewHolder = new HeaderFooterViewHolder(new HeaderView(parent.getContext()));
return headerViewHolder;
}
if (viewType == FooterType) {
footerViewHolder = new HeaderFooterViewHolder(new FooterView(parent.getContext()));
if(!footerViewEnable) { //不允許上拉加載更多一疯,隱藏FooterView
FooterView footerView = (FooterView) footerViewHolder.itemView;
footerView.setNowState(FooterView.STATE.HIND);
}
return footerViewHolder;
}
...
...
}
@Override
public int getItemCount() {
//添加Header和Footer的數(shù)目
return getThisItemCount() + 2;
}
/**
* 此方法執(zhí)行RecyclerView.Adapter中g(shù)etItemCount()的邏輯
*
* @return
*/
public abstract int getThisItemCount();
/**
* 重寫此方法時(shí)請注意保留父類方法的邏輯,否則導(dǎo)致header計(jì)數(shù)混亂夺姑,下拉刷新出錯
* 使用position時(shí)注意減1(減去header的位置)
*
* @param position
* @return
*/
@Override
public int getItemViewType(int position) {
if (position == 0)
return HeaderType;
if (position == getThisItemCount() + 1)
return FooterType;
return super.getItemViewType(position - 1);//減1去掉herder的位置
}
首先是getItemCount()方法墩邀,加上HeaderView和FooterView的位置,也就是在原有的數(shù)目上加2盏浙,原有的數(shù)目由getThisItemCount()獲取眉睹,由使用者自己實(shí)現(xiàn)荔茬。然后在特定的位置返回特定的類型,position為0時(shí)竹海,返回HeaderType慕蔚,position在最后時(shí),返回FooterType斋配。然后孔飒,在onCreateViewHolder()中根據(jù)viewType返回特定的ViewHolder類型。這樣艰争,就把HeaderView和FooterView都增加進(jìn)去了坏瞄。
接下來的步驟就是控制touch事件動態(tài)設(shè)置HeaderView的高度及控件來實(shí)現(xiàn)下拉刷新的效果了。當(dāng)RecyclerView滑動到頂部時(shí)甩卓,繼續(xù)往下拉觸發(fā)下拉刷新鸠匀。當(dāng)滑到底部時(shí),自動觸發(fā)加載更多逾柿。然后設(shè)置好相關(guān)的接口回調(diào)缀棍,就基本完成。這里面許多細(xì)節(jié)机错,一篇文章很難講完了爬范,基本可以另開新篇。涉及很多基本知識和細(xì)節(jié)邏輯毡熏。大家真的愿意了解的話坦敌。源碼鏈接在下方,可以作為參考痢法。
結(jié)尾
項(xiàng)目托管在github上狱窘,再貼一次地址:https://github.com/zhangyuChen1991/PtrSwipeMenuRecyclerView
有興趣的童鞋可以前去下載,如發(fā)現(xiàn)問題财搁,請斧正蘸炸!非常感謝!