最近應(yīng)公司項(xiàng)目需要文虏,動(dòng)手寫了一個(gè)下拉刷新和上拉加載的自定義控件。之所以沒有直接用網(wǎng)上的依賴庫坷备,一來是感覺會(huì)讓項(xiàng)目變得更臃腫挪拟,而大多數(shù)酷炫的效果,根本就用不到击你;二來,也在網(wǎng)上找過類似的demo谎柄,但是看完之后又都感覺太復(fù)雜丁侄,最終決定還是自己動(dòng)手寫。從最開始構(gòu)思朝巫、查閱資料到徹底完成鸿摇,差不多用了2天時(shí)間,代碼量總共才300來行劈猿,整體看著比較簡單拙吉。
包含以下功能:
1、數(shù)據(jù)不滿一屏揪荣,自動(dòng)屏蔽上拉加載
2筷黔、數(shù)據(jù)加載完畢不再執(zhí)行加載動(dòng)畫,而是提示end
3仗颈、支持RecyclerView
和ListView
已知bug:
當(dāng)設(shè)置layout_height=warp_content
時(shí)佛舱,未滿一屏,仍然會(huì)執(zhí)行上拉加載操作
由于我這邊沒法錄屏,所以只好借用劉小帥的動(dòng)圖请祖,部分代碼也參考了他的思路订歪,如有疑問,請電郵2647759254@qq.com肆捕。
根據(jù)效果刷晋,我們知道這里包含3部分:頭部,播放刷新動(dòng)畫慎陵;底部眼虱,播放加載動(dòng)畫;中間荆姆,展示數(shù)據(jù)蒙幻。正常情況,我們只需要展示中間的部分胆筒,當(dāng)手指下拉邮破,到列表的頭部時(shí),開始顯示頭部仆救,手指松開抒和,播放刷新動(dòng)畫,并加載數(shù)據(jù)彤蔽。數(shù)據(jù)加載完畢摧莽,動(dòng)畫停止,頭部隱藏顿痪。底部也是同樣的道理镊辕。
通過上面的分析,需要處理下面幾個(gè)問題:
1蚁袭、列表控件征懈,如:RecyclerView
,是充滿父布局的揩悄,并且處于中間的位置卖哎,其頭部和底部分別有一個(gè)View
2、如何判斷RecyclerView
是否滑動(dòng)到邊界
3删性、滑動(dòng)到邊界之后亏娜,開始展示頭部或者底部界面,而且它們的界面也會(huì)跟著手指滑動(dòng)蹬挺,進(jìn)行變化
4维贺、刷新或者加載完畢之后,如何隱藏頭部或底部界面
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if(headerView == null) {
headerView = LayoutInflater.from(getContext()).inflate(R.layout.layout_header, null);
headerIV = (LevelImageView) headerView.findViewById(R.id.iv_refresh);
headerTV = (TextView) headerView.findViewById(R.id.tv_header);
RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.addRule(ALIGN_PARENT_TOP);
headerView.setLayoutParams(params);
headerView.setVisibility(GONE);
addView(headerView);
}
if(footerView == null) {
footerView = LayoutInflater.from(getContext()).inflate(R.layout.layout_footer, null);
footerIV = (LevelImageView) footerView.findViewById(R.id.iv_load);
footerTV = (TextView) footerView.findViewById(R.id.tv_footer);
RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.addRule(ALIGN_PARENT_BOTTOM);
footerView.setLayoutParams(params);
footerView.setVisibility(GONE);
addView(footerView);
}
childView = getChildAt(0);
if(childView != null) {
RelativeLayout.LayoutParams params = (LayoutParams) childView.getLayoutParams();
params.addRule(RelativeLayout.ABOVE, R.id.footer_ll);
params.addRule(RelativeLayout.BELOW, R.id.header_ll);
childView.setLayoutParams(params);
childView.requestLayout();
}
}
通過第一點(diǎn)的分析巴帮,我決定采用組合控件的方式來寫幸缕。整個(gè)布局繼承RelativeLayout
群发,在onAttachedToWindow()
方法中,分別在頭部和底部添加一個(gè)View
,RecyclerView
則通過調(diào)用getChildAt(0)
獲取发乔。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
moveY = (int) ev.getY();
if (moveY - downY > 0 && !ViewCompat.canScrollVertically(childView, -1)) { //下拉
slideStatus = PULL_DOWN_REFRESH;
return true;
} else if (moveY - downY < 0 && !ViewCompat.canScrollVertically(childView, 1)) {//上拉
int hei = countDataHeight();
if(mParentHei - countDataHeight() > 50) {
Log.e("mParentHei", "mParentHei" + mParentHei + "---childView" + countDataHeight());
return false;
} else {
slideStatus = PULL_UP_LOAD;
return true;
}
}
break;
}
return super.onInterceptTouchEvent(ev);
}
在這個(gè)方法中熟妓,通過滑動(dòng)的距離差,來判斷是上拉還是下拉栏尚,而ViewCompat.canScrollVertically(view, -1)
則是用來判斷RecyclerView
是否滑動(dòng)到邊界了起愈。countDataHeight()
這個(gè)方法,是用來計(jì)算RecyclerView
中內(nèi)容的高度译仗,if(mParentHei - countDataHeight() > 50)
這一句用于判斷抬虽,RecyclerView
中的內(nèi)容,是否滿一屏纵菌,如果不滿一屏阐污,則會(huì)屏蔽上拉加載。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isLoading || isRefreshing) return super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
mY = (int) (event.getY() - downY); //計(jì)算滑動(dòng)的距離
if (slideStatus == PULL_DOWN_REFRESH) {
headerView.setVisibility(VISIBLE);
mY = mY <= 150 ? mY : 150;
headerView.getLayoutParams().height = mY;
headerTV.setText("釋放刷新");
headerView.requestLayout();
} else if (mY < 0 && slideStatus == PULL_UP_LOAD) {
footerView.setVisibility(VISIBLE);
mY = Math.abs(mY);
mY = mY <= 150 ? mY : 150;
if (childView instanceof RecyclerView) {
RecyclerView mRecyclerView = (RecyclerView) childView;
mRecyclerView.smoothScrollToPosition(mRecyclerView.getAdapter().getItemCount() - 1);
} else if (childView instanceof ListView) {
ListView mListView = (ListView) childView;
mListView.smoothScrollToPosition(mListView.getAdapter().getCount() - 1);
}
footerView.getLayoutParams().height = mY;
footerView.requestLayout();
if(mOnLoadListener != null) {
if(!mOnLoadListener.canLoad()) {
footerTV.setText("--end--");
footerIV.setVisibility(GONE);
}
}
}
break;
case MotionEvent.ACTION_UP:
if (slideStatus == PULL_DOWN_REFRESH) {
if (mY > 40) {
headerView.getLayoutParams().height = 120;
headerView.requestLayout();
isRefreshing = true;
headerTV.setText("刷新中咱圆、笛辟、、");
startProgress(headerIV, PULL_DOWN_REFRESH);
if (mOnRefreshListener != null) {
mOnRefreshListener.onRefresh(animator);
}
} else {
headerView.setVisibility(GONE);
}
} else if (slideStatus == PULL_UP_LOAD) {
if (mY > 40) {
if (mOnLoadListener != null) {
if(mOnLoadListener.canLoad()) {
footerView.getLayoutParams().height = 120;
footerView.requestLayout();
isLoading = true;
footerTV.setText("加載中序苏、手幢、、");
startProgress(footerIV, PULL_UP_LOAD);
mOnLoadListener.onLoad(animator);
} else {
footerView.setVisibility(GONE);
}
}
} else {
footerView.setVisibility(GONE);
}
}
break;
}
return super.onTouchEvent(event);
}
這里有四個(gè)作用:
第一個(gè)if (slideStatus == PULL_DOWN_REFRESH)
用于顯示頭部界面忱详,提示‘釋放刷新’围来,并重新設(shè)置頭部高度
第一個(gè)else if (mY < 0 && slideStatus == PULL_UP_LOAD)
除了顯示底部界面之外,還需要通過if(!mOnLoadListener.canLoad())
判斷是否已經(jīng)加載到底匈睁,沒有更多數(shù)據(jù)了监透。
第二個(gè)if (slideStatus == PULL_DOWN_REFRESH)
是在釋放的情況下開始執(zhí)行刷新動(dòng)畫,并且執(zhí)行刷新數(shù)據(jù)的操作
第二個(gè)else if (slideStatus == PULL_UP_LOAD)
晚一些會(huì)抽空上傳代碼航唆,有需要的朋友胀蛮,也可以直接聯(lián)系我2647759254@qq.com。