帶你一步一步實現(xiàn)listview上拉加載下拉刷新
思路:
- 要是實現(xiàn)這個效果我們需要尉尾,有一個view能在頭部和底部存在(通過查找的只listview有addheard,和addfoot的方法可以將一個view添加到頭部的底部)
- 如何進來的時候不顯示頭部的view呢隅居?(我門可以設(shè)置View的初始高度為0梧宫,或者設(shè)置setPadding設(shè)置他距離上面的距離為-的高度就可以隱藏了莺琳,我們使用setPadding方法)
- 如何隱藏底部的view蛉幸?(同上)
- 如何能讓view跟著我們的手勢移動呢赏酥?(第一種我們可以使用OverScroller這個類,第二種我們可以通過動態(tài)的改變頭部view和底部view的高度來實現(xiàn)移動的的效果医增,我們使用后者)
- 如何實現(xiàn)view的自動回彈呢?(我們可以使用OverScroller這個類的startScroll方法老虫,然后實現(xiàn)computeScroll方法在這個里面去動態(tài)的改變view的高度)
第一步我們創(chuàng)建HeaderView這個類當(dāng)頂部的刷新的view
public class HeaderView extends LinearLayout {
/** 刷新狀態(tài) */
private LoadState mState = LoadState.NORMAL;
private View mHeader = null;
private ImageView mArrow = null;
private ProgressBar mProgressBar = null;
private TextView mRefreshTips = null;
private TextView mRefreshLastTime = null;
private RotateAnimation mRotateUp = null;
private RotateAnimation mRotateDown = null;
private final static int ROTATE_DURATION = 250;
/** 一分鐘的毫秒值叶骨,用于判斷上次的更新時間. */
private final long ONE_MINUTE = 60 * 1000;
/** 一小時的毫秒值,用于判斷上次的更新時間. */
private final long ONE_HOUR = 60 * ONE_MINUTE;
/** 一天的毫秒值祈匙,用于判斷上次的更新時間. */
private final long ONE_DAY = 24 * ONE_HOUR;
/** 一月的毫秒值忽刽,用于判斷上次的更新時間. */
private final long ONE_MONTH = 30 * ONE_DAY;
/** 一年的毫秒值,用于判斷上次的更新時間. */
private final long ONE_YEAR = 12 * ONE_MONTH;
public HeaderView(Context context) {
this(context, null);
}
public HeaderView(Context context, AttributeSet attrs) {
super(context, attrs);
initHeaderView(context);
}
private void initHeaderView(Context context) {
LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
mHeader = LayoutInflater.from(context).inflate(R.layout.g_refresh_header, null);
addView(mHeader, lp);
setGravity(Gravity.BOTTOM);
mArrow = (ImageView) mHeader.findViewById(R.id.ivArrow);
mProgressBar = (ProgressBar) mHeader.findViewById(R.id.pbWaiting);
mRefreshTips = (TextView) mHeader.findViewById(R.id.refresh_tips);
mRefreshLastTime = (TextView) mHeader.findViewById(R.id.refresh_last_time);
//初始化旋轉(zhuǎn)的動畫需要反轉(zhuǎn)箭頭
mRotateUp = new RotateAnimation(0.0f, -180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateUp.setDuration(ROTATE_DURATION);
mRotateUp.setFillAfter(true);
mRotateDown = new RotateAnimation(-180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateDown.setDuration(ROTATE_DURATION);
mRotateDown.setFillAfter(true);
Log.e("HeaderView","HeaderView初始化了");
}
public void setHeaderState(LoadState state) {
if (mState == state) {
return;
}
mArrow.clearAnimation();
if (state == LoadState.LOADING) {
mArrow.setVisibility(View.GONE);
mProgressBar.setVisibility(View.VISIBLE);
} else {
mProgressBar.setVisibility(View.GONE);
mArrow.setVisibility(View.VISIBLE);
}
switch (state) {
case NORMAL:
mArrow.startAnimation(mRotateDown);
mRefreshTips.setText(R.string.g_pull_down_for_refresh);
break;
case WILL_RELEASE:
//旋轉(zhuǎn)當(dāng)前的箭頭的狀態(tài)
mArrow.startAnimation(mRotateUp);
mRefreshTips.setText(R.string.g_release_for_refresh);
break;
case LOADING:
mRefreshTips.setText(R.string.g_refreshing);
break;
default:
break;
}
mState = state;
}
public LoadState getCurrentState() {
return mState;
}
public void setHeaderHeight(int height) {
if (height <= 0) {
height = 0;
}
LayoutParams lp = (LayoutParams) mHeader.getLayoutParams();
lp.height = height;
mHeader.setLayoutParams(lp);
}
public int getHeaderHeight() {
return mHeader.getHeight();
}
我們可以看到HeaderView這個類很簡單夺欲,就是初始化一個view,然后提供了設(shè)置高度跪帝,和不同狀態(tài)的判斷顯示的不同字體的邏輯
我們創(chuàng)建FooterView這個類其實和heardview一樣
/**
* 底部
*/
public class FooterView extends LinearLayout {
/** 加載更多. */
private LoadState mState = LoadState.NORMAL;
private View mFooter = null;
private ImageView mArrow = null;
private ProgressBar mProgressBar = null;
private TextView mLoaderTips = null;
private RotateAnimation mRotateUp = null;
private RotateAnimation mRotateDown = null;
private final static int ROTATE_DURATION = 250;
public FooterView(Context context) {
this(context, null);
}
public FooterView(Context context, AttributeSet attrs) {
super(context, attrs);
initFooterView(context);
}
private void initFooterView(Context context) {
LinearLayout.LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0);
mFooter = LayoutInflater.from(context).inflate(R.layout.g_loader_footer, null);
addView(mFooter, lp);
mArrow = (ImageView) mFooter.findViewById(R.id.ivLoaderArrow);
mProgressBar = (ProgressBar) mFooter.findViewById(R.id.pbLoaderWaiting);
mLoaderTips = (TextView) mFooter.findViewById(R.id.loader_tips);
mRotateDown = new RotateAnimation(0.0f, 180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateDown.setDuration(ROTATE_DURATION);
mRotateDown.setFillAfter(true);
mRotateUp = new RotateAnimation(180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mRotateUp.setDuration(ROTATE_DURATION);
mRotateUp.setFillAfter(true);
show();
}
public void setFooterState(LoadState state) {
if (mState == state) {
return;
}
mArrow.clearAnimation();
if (state == LoadState.LOADING) {
mProgressBar.setVisibility(View.VISIBLE);
mArrow.setVisibility(View.GONE);
} else if (state == LoadState.NODATA) {
mProgressBar.setVisibility(View.GONE);
mArrow.setVisibility(View.GONE);
} else {
mProgressBar.setVisibility(View.GONE);
mArrow.setVisibility(View.VISIBLE);
}
switch (state) {
case NORMAL:
mArrow.startAnimation(mRotateUp);
mLoaderTips.setText(R.string.g_pull_up_for_more);
break;
case WILL_RELEASE:
mArrow.startAnimation(mRotateDown);
mLoaderTips.setText(R.string.g_release_for_more);
break;
case LOADING:
mLoaderTips.setText(R.string.g_loading);
break;
case NODATA:
mLoaderTips.setText(R.string.g_nodata);
break;
default:
break;
}
mState = state;
}
public LoadState getCurrentState() {
return mState;
}
public void setFooterHeight(int height) {
if (height <= 0) {
height = 0;
}
LayoutParams lp = (LayoutParams) mFooter.getLayoutParams();
lp.height = height;
mFooter.setLayoutParams(lp);
}
public int getFooterHeight() {
return mFooter.getHeight();
}
}
邏輯也是一樣,設(shè)置高度些阅,提供不同狀態(tài)的判斷伞剑,顯示不同的內(nèi)容
接下來上我們的主要類XrefershListview
/**
* 作者:liujingyuan on 2015/12/21 13:14
* 郵箱:906514731@qq.com
* 自定義的下拉刷新上拉加載的Listview
*/
public class XrefershListview extends ListView implements AbsListView.OnScrollListener {
private final static String TAG = "[XrefershListview]";
private final static float OFFSET_Y = 0.7f;
private OverScroller mScroller;
private final static int SCROLL_HEADER = 0;
private final static int SCROLL_FOOTER = 1;
//刷新的監(jiān)聽
public XrefershListviewListener mListViewListener;
/** 滑動項. */
private int iScrollWhich = SCROLL_HEADER;
//是否顯示footview
public boolean isShowLoadeFooterView=false;
HeaderView mHeaderView;
FooterView mFooterView;
int iHeaderHeight;
int iFooterHeight;
float EndRawy;
float mStartY;
float dy;
private float mLastY;
public XrefershListview(Context context) {
super(context);
initHeardView(context);
}
public XrefershListview(Context context, AttributeSet attrs) {
super(context, attrs);
initHeardView(context);
}
public XrefershListview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initHeardView(context);
}
//初始化頭部的view
private void initHeardView(Context context) {
//初始化scmScroller
mScroller = new OverScroller(context,new DecelerateInterpolator());
mHeaderView = new HeaderView(context);
mFooterView = new FooterView(context);
//初始化hearview的高度
mHeaderView.setHeaderHeight(200);
//初始化footview
initFooterView(context);
final RelativeLayout header_content = (RelativeLayout) mHeaderView.findViewById(R.id.header_content);
addHeaderView(mHeaderView);
this.setOnScrollListener(this);
//監(jiān)聽到view加載完畢獲取view的高度
mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
iHeaderHeight =mHeaderView.getMeasuredHeight();
Log.d(TAG, "iHeaderHeight = " + iHeaderHeight);
//剛進來的時候我們隱藏heardview
mHeaderView.setPadding(0,-iHeaderHeight,0,0);
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下的時候我們需要記住起始點的Y軸的坐標(biāo)
mLastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//移動的時候獲取當(dāng)前的y軸的坐標(biāo)
mStartY = ev.getY();
//計算滑動的距離
dy=mStartY-mLastY;
//再次獲取一次,因為我們是增量增加寬度的市埋,每次獲取的是相對于上一點的位移
mLastY = ev.getY();
if (getFirstVisiblePosition()==0&& (mHeaderView.getMeasuredHeight() > 0 || dy > 0)){
//通過改變heardview的高度
upDataHeardView(dy*0.7f);
}else if (getLastVisiblePosition() == getCount() - 1 && (mFooterView.getFooterHeight() > 0 || dy < 0)){
//通過改變footview的高度
updateFooterState(-dy);
}
Log.e(TAG,"dy============"+dy);
break;
case MotionEvent.ACTION_UP:
EndRawy = ev.getRawY();
//如果當(dāng)前的可見的第一個條目是0黎泣,并且當(dāng)前的hreadview的高度不為0,滑動的距離大于0就證明是在下拉刷新
if (getFirstVisiblePosition()==0&& (mHeaderView.getMeasuredHeight() > 0 || dy > 0)){
//判斷當(dāng)前的狀態(tài)不是正在刷新的狀態(tài)就設(shè)置為刷新的狀態(tài)
if (mHeaderView.getCurrentState() != LoadState.LOADING) {
if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
mHeaderView.setHeaderState(LoadState.LOADING);
//調(diào)用是界面的刷新,延時1秒
mListViewListener.onRefresh();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
resetHeaderHeight();
}
},1000);
}
}
//如果當(dāng)前已經(jīng)顯示到最后一個條目了就是上拉加載缤谎,并且當(dāng)前footview的高度大于0聘裁,移動的距離也大于0
}else if(getLastVisiblePosition() == getCount() - 1 && (mFooterView.getFooterHeight() > 0 || dy < 0)){
//判斷當(dāng)前的狀態(tài)不是正在刷新的狀態(tài)就設(shè)置為刷新的狀態(tài)
if (mFooterView.getCurrentState() != LoadState.LOADING) {
if (mFooterView.getFooterHeight() > iFooterHeight) {
mFooterView.setFooterState(LoadState.LOADING);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//調(diào)用是界面的刷新,延時1秒
mListViewListener.onLoadMore();
resetFooter();
}
},3000);
}
}
}
break;
default:
break;
}
return super.onTouchEvent(ev);
}
/**
* 更新底部footview的狀態(tài)
*
* @param delta
*/
private void updateFooterState(float delta) {
if (null == mFooterView) {
return;
}
//設(shè)置footview的高度移動的時候,這個高度是增加每次移動的點相當(dāng)于上一個點的位置的距離
mFooterView.setFooterHeight((int) (delta + mFooterView.getFooterHeight()));
//當(dāng)前的狀態(tài)不是在刷新的狀態(tài)弓千!
if (mFooterView.getCurrentState() != LoadState.LOADING) {
if (mFooterView.getFooterHeight() > iFooterHeight) {
//設(shè)置當(dāng)前的顯示的內(nèi)容為松開加載更多
mFooterView.setFooterState(LoadState.WILL_RELEASE);
} else {
mFooterView.setFooterState(LoadState.NORMAL);
}
}
}
/**
* 初始化底部footview
*/
private void initFooterView(Context context) {
mFooterView = new FooterView(context);
mFooterView.setFooterHeight(200);
//隱藏footview
mFooterView.setPadding(0,0,0,-200);
addFooterView(mFooterView);
mFooterView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//獲取測量的footview的高度
iFooterHeight = mFooterView.getMeasuredHeight();
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
/**
* 重置footview
*/
private void resetFooter() {
//重置刷新狀態(tài)
mFooterView.setFooterState(LoadState.NORMAL);
if (null == mFooterView) {
return;
}
int height = mFooterView.getMeasuredHeight();
if (height == 0) {
return;
}
int finalHeight = 0;
if (height > iFooterHeight && mFooterView.getCurrentState() != LoadState.NORMAL) {
finalHeight = iFooterHeight;
} else if (mFooterView.getCurrentState() == LoadState.LOADING) {
return;
}
iScrollWhich = SCROLL_FOOTER;
mScroller.startScroll(0, height, 0, finalHeight - height, 300);
invalidate();
}
/**
* 重置heardview的狀態(tài)
*/
private void resetHeaderHeight() {
//重置刷新的狀態(tài)
mHeaderView.setHeaderState(LoadState.NORMAL);
//獲取測量得到的heardview的高度
int height = mHeaderView.getMeasuredHeight();
//如果獲取當(dāng)前的View的高度等于0代表是沒有移動就不做處理
if (height == 0) // not visible.
return;
int finalHeight = 0;
//如果超過HeaderView高度,則回滾到HeaderView高度即可
if (height > iHeaderHeight && mHeaderView.getCurrentState() != LoadState.NORMAL) {
finalHeight = iHeaderHeight;
}
//標(biāo)記當(dāng)前是heardview在移動
iScrollWhich = SCROLL_HEADER;
//inalHeight - height献起, finalHeight代表heardview抬起的時候高度-heardview初始化的高度=heardview在y軸上移動的距離洋访,280毫秒內(nèi)移動完畢
//int startX,開始的位置
// int mLastY,y位置開始的位置
// int dx, x滑動的距離
// int dy, y滑動的距離
// int duration執(zhí)行完畢需要的時間
mScroller.startScroll(0, height, 0, finalHeight - height, 300);
//手動調(diào)用刷新移動
invalidate();
}
/**
* 設(shè)置頭部的view的高度,來達(dá)到下移的效果
*/
private void upDataHeardView(float dy) {
//如果當(dāng)前的狀態(tài)不是正在加載中谴餐,就改變狀態(tài)
if (mHeaderView.getCurrentState() != LoadState.LOADING) {
//如果當(dāng)前的heardview的高度大于原始的高度就代表用戶下拉刷新了要該表狀態(tài)姻政,變成下拉刷新的狀態(tài)
if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
mHeaderView.setHeaderState(LoadState.WILL_RELEASE);
} else {
mHeaderView.setHeaderState(LoadState.NORMAL);
}
}
//移動距離等于move的距離加上heardview的高度
mHeaderView.setHeaderHeight((int) (dy + mHeaderView.getHeaderHeight()));
}
/**
* 設(shè)置刷新的監(jiān)聽
*/
public void setXrefershListviewListener(XrefershListviewListener l) {
mListViewListener = l;
}
//每次移動都會調(diào)用computeScroll
@Override
public void computeScroll() {
//判斷是否滑動結(jié)束
if (mScroller.computeScrollOffset()) {
//代表是下拉刷新
if (iScrollWhich == SCROLL_HEADER) {
//獲取到當(dāng)前滾動的位置,來動態(tài)的設(shè)置mHeaderView的高度
mHeaderView.setHeaderHeight(mScroller.getCurrY());
//代表是上拉加載改變footview
} else if (iScrollWhich == SCROLL_FOOTER) {
//不斷去改變footview的高度
mFooterView.setFooterHeight(mScroller.getCurrY());
}
invalidate();
}
}
//監(jiān)聽listview的滑動監(jiān)聽
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
//監(jiān)聽listview的滑動監(jiān)聽
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
}
上面的注釋也非常的清楚岂嗓,大體思路:
- 首先我們直接初始化HeaderView和FooterView然后設(shè)置她們的默認(rèn)高度為200dp
- addHeaderView然后我們添加頭部的view
- 設(shè)置addOnGlobalLayoutListener的監(jiān)聽mHeaderView.getMeasuredHeight();在回調(diào)的方法中獲取測量的heardview的高度
- mHeaderView.setPadding(0,-iHeaderHeight,0,0);這步我們就隱藏了heardview,設(shè)置他距離上面的距離為-iHeaderHeight
- 然后我們重寫onTouchEvent來實現(xiàn)具體的移動的邏輯汁展,首先我們在ACTION_DOWN里面獲取當(dāng)前的坐標(biāo),然后在ACTION_MOVE中我們?nèi)ビ嬎慊瑒拥木嚯x厌殉,詳情看代碼的注釋
- 通過 upDataHeardView(dy*0.7f);這個方法我們實現(xiàn)頭部view下拉的效果
/**
* 設(shè)置頭部的view的高度食绿,來達(dá)到下移的效果
*/
private void upDataHeardView(float dy) {
//如果當(dāng)前的狀態(tài)不是正在加載中,就改變狀態(tài)
if (mHeaderView.getCurrentState() != LoadState.LOADING) {
//如果當(dāng)前的heardview的高度大于原始的高度就代表用戶下拉刷新了要該表狀態(tài)公罕,變成下拉刷新的狀態(tài)
if (mHeaderView.getHeaderHeight() > iHeaderHeight) {
mHeaderView.setHeaderState(LoadState.WILL_RELEASE);
} else {
mHeaderView.setHeaderState(LoadState.NORMAL);
}
}
//移動距離等于move的距離加上heardview的高度
mHeaderView.setHeaderHeight((int) (dy + mHeaderView.getHeaderHeight()));
}
- 然后我們在看ACTION_UP器紧,抬起的時候我們?nèi)ヅ袛喈?dāng)前的是否是下拉刷新,如果是就直接調(diào)用onRefresh是刷新界面的數(shù)據(jù)在resetHeaderHeight我們重置頭部view的狀態(tài)
/**
* 重置heardview的狀態(tài)
*/
private void resetHeaderHeight() {
//重置刷新的狀態(tài)
mHeaderView.setHeaderState(LoadState.NORMAL);
//獲取測量得到的heardview的高度
int height = mHeaderView.getMeasuredHeight();
//如果獲取當(dāng)前的View的高度等于0代表是沒有移動就不做處理
if (height == 0) // not visible.
return;
int finalHeight = 0;
//如果超過HeaderView高度楼眷,則回滾到HeaderView高度即可
if (height > iHeaderHeight && mHeaderView.getCurrentState() != LoadState.NORMAL) {
finalHeight = iHeaderHeight;
}
//標(biāo)記當(dāng)前是heardview在移動
iScrollWhich = SCROLL_HEADER;
//inalHeight - height铲汪, finalHeight代表heardview抬起的時候高度-heardview初始化的高度=heardview在y軸上移動的距離熊尉,280毫秒內(nèi)移動完畢
//int startX,開始的位置
// int mLastY,y位置開始的位置
// int dx, x滑動的距離
// int dy, y滑動的距離
// int duration執(zhí)行完畢需要的時間
mScroller.startScroll(0, height, 0, finalHeight - height, 300);
//手動調(diào)用刷新移動
invalidate();
}
- 我們需要重寫computeScroll這個方法來實現(xiàn)阻尼的效果
@Override
public void computeScroll() {
//判斷是否滑動結(jié)束
if (mScroller.computeScrollOffset()) {
//代表是下拉刷新
if (iScrollWhich == SCROLL_HEADER) {
//獲取到當(dāng)前滾動的位置,來動態(tài)的設(shè)置mHeaderView的高度
mHeaderView.setHeaderHeight(mScroller.getCurrY());
//代表是上拉加載改變footview
} else if (iScrollWhich == SCROLL_FOOTER) {
//不斷去改變footview的高度
mFooterView.setFooterHeight(mScroller.getCurrY());
}
invalidate();
}
}
底部的刷新邏輯和頭部的都一樣掌腰,這樣我們就實現(xiàn)了上拉刷新下拉加載的效果了狰住!
代碼地址點擊這