一、测试效果截图
二、原理和思路见源码(注释很详细)
package com.widget.pull_tofresh_listview;
import com.example.nxtravelclient.R;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
/**
* @name 下拉刷新、上拉加载分页ListView控件
* @Descripation
* 1、需要捕获的用户手势:down按下、up抬起、move移动。主要捕获move状态下手指(纵坐标)在屏幕上滑动的距离,
* 通过计算下滑或上滑的距离decline/increase来改变头部(底部)的状态。
* 2、列表的5种互斥状态:pull、release、refreshing、down、back。下拉(上拉)、临界、松手、进入刷新()状态、刷新完成。
* 3、头部(底部)布局高度随手势变化:初始状态下,头部上边距和底部下边距均为零。下拉头部布局时,头部上边距逐渐增大,
* 返回时逐渐减小;上拉底部布局时,底部下边距逐渐增大, 返回时逐渐减小。
* 4、头部(底部)动画效果随手势变化:下拉临界时,头部箭逆置,原路返回时不变;上拉临界时,底部箭头逆置,原路返回时不变。
* 5、用户事件接口:下拉刷新OnRefreshListener、上拉加载更多分页数据OnLoadMoreListener,
* 同步更新最近时间。
* @author Freedoman
* @date 2014-9-24
* @version 1.0
*/
public class PullListView extends ListView implements OnScrollListener {
private final static String TAG_HEADER = "HeaderView";
private final static String TAG_FOOTER = "FooterView";
// 头部布局及其组件(指示、进度、指示文本、最近更新时间)
private LinearLayout headerViewLayout;
private ImageView headerArrowImage;
private ImageView headerProgressImage;
private TextView headerTipsText;
private TextView headerLastUpdateText;
// 底部布局及其组件
private LinearLayout footerViewLayout;
private ImageView footerArrowImage;
private ImageView footerProgressImage;
private TextView footerTipsText;
private TextView footerLastUpdateText;
// 箭头旋转和逆置、进度条旋转动画
private RotateAnimation rotateAnimation;
private RotateAnimation reverseAnimation;
private Animation progressAnimation;
// 下拉(上拉)、松手、正在刷新和完成4种状态
private final static int PULL = 0;
private final static int RELEASE = 1;
private final static int REFRESHING = 2;
private final static int DONE = 3;
// 标记是否从最头部下拉,最底部上拉
private boolean isFromFirstItem;
private boolean isFromLastItem;
private int firstItemIndex;
private int lastItemIndex;
private int curTotalItemCount;
// 标记是否原路返回
private boolean isHeaderBack;
private boolean isFooterBack;
// 当前状态
private int headerState;
private int footerState;
// 头部视图的上边距、底部视图的下边距
private int headerTopPadding;
private int footerBottomPadding;
// 当前手势的滑动的纵向坐标
private float headerGestureStartY;
private float footerGestureStartY;
// 布局实际高度和指定拉动临界值
private int viewLayoutHeight;
private int pullHeighSpec = 100;
// 头部刷新、底部加载更多用户接口
public OnRefreshListener onRefreshListener;
public OnLoadMoreListener onLoadMoreListener;
public PullListView(Context context, AttributeSet attrs) {
super(context, attrs);
initAnimation(context);
initHeaderView(context);
initFooterViews(context);
setOnScrollListener(this);
}
public PullListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initAnimation(context);
initHeaderView(context);
initFooterViews(context);
setOnScrollListener(this);
}
/**
* 初始化头部和底部使用的动画效果
*/
private void initAnimation(Context context) {
// 中心旋转180(0,-180)
rotateAnimation = new RotateAnimation(0, -180,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setInterpolator(new LinearInterpolator());
rotateAnimation.setDuration(100);
rotateAnimation.setFillAfter(true);
// 逆置动画(-180,0)
reverseAnimation = new RotateAnimation(-180, 0,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
reverseAnimation.setInterpolator(new LinearInterpolator());
reverseAnimation.setDuration(100);
reverseAnimation.setFillAfter(true);
// 进度条旋转动画
progressAnimation = AnimationUtils.loadAnimation(context,
R.anim.loading_rotate_anim);
}
/**
* 加载头部所有组件(箭头图标、圆圈进度条、提示文本、最后一次更新时间),设置初始大小和样式
*/
private void initHeaderView(Context context) {
headerViewLayout = (LinearLayout) LayoutInflater.from(context).inflate(
R.layout.header_footer_list_view, null);
headerArrowImage = (ImageView) headerViewLayout
.findViewById(R.id.arrow_image);
headerArrowImage.setMinimumWidth(50);
headerArrowImage.setMinimumHeight(50);
headerProgressImage = (ImageView) headerViewLayout
.findViewById(R.id.progress_image);
headerTipsText = (TextView) headerViewLayout
.findViewById(R.id.tips_text);
headerLastUpdateText = (TextView) headerViewLayout
.findViewById(R.id.last_updated_text);
headerViewLayout.measure(0, 0);
headerViewLayout.measure(0, 0);
viewLayoutHeight = headerViewLayout.getMeasuredHeight();
// 初始边距为负值,即为隐藏状态
headerTopPadding = headerViewLayout.getPaddingTop();
headerViewLayout.setPadding(headerViewLayout.getPaddingLeft(), -1
* viewLayoutHeight, headerViewLayout.getPaddingRight(),
headerViewLayout.getPaddingBottom());
headerViewLayout.invalidate();
addHeaderView(headerViewLayout);
}
/**
* 加载底部所有组件(箭头图标、圆圈进度条、提示文本、最后一次更新时间),设置初始大小和样式
*/
private void initFooterViews(Context context) {
footerViewLayout = (LinearLayout) LayoutInflater.from(context).inflate(
R.layout.header_footer_list_view, null);
footerArrowImage = (ImageView) footerViewLayout
.findViewById(R.id.arrow_image);
footerArrowImage.setMinimumWidth(50);
footerArrowImage.setMinimumHeight(50);
footerProgressImage = (ImageView) footerViewLayout
.findViewById(R.id.progress_image);
footerTipsText = (TextView) footerViewLayout
.findViewById(R.id.tips_text);
footerLastUpdateText = (TextView) footerViewLayout
.findViewById(R.id.last_updated_text);
footerViewLayout.measure(0, 0);
// 初始边距为负值,即为隐藏状态
footerBottomPadding = footerViewLayout.getPaddingBottom();
footerViewLayout.setPadding(footerViewLayout.getPaddingLeft(),
footerViewLayout.getPaddingTop(),
footerViewLayout.getPaddingRight(), -1 * viewLayoutHeight);
footerViewLayout.invalidate();
addFooterView(footerViewLayout);
}
/**
* 监听列表滚动,记录列表的第一项和最后一项(为头部刷新和底部刷新提供依据)
*/
public void onScroll(AbsListView view, int firstVisiableItem,
int visibleItemCount, int totalItemCount) {
firstItemIndex = firstVisiableItem;
lastItemIndex = firstVisiableItem + visibleItemCount;
curTotalItemCount = totalItemCount;
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
/**
* 捕获手势,计算滑动距离来改变列表状态
*/
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
// 从头开始向下滑动并记录下滑起始位置
if (!isFromFirstItem && firstItemIndex == 0) {
isFromFirstItem = true;
headerGestureStartY = event.getY();
}
// 从底开始上滑并记录上滑起始位置
if (!isFromLastItem && (lastItemIndex == curTotalItemCount)) {
isFromLastItem = true;
footerGestureStartY = event.getY();
}
// 从头部开始滑动且当前状态处于‘非’正在刷新REFRESHING状态(pull,release,done三种状态互斥)时
if (headerState != REFRESHING && isFromFirstItem) {
// 手势下滑程度
float decline = event.getY() - headerGestureStartY;
listenHeaderStateChanged(decline);
}
// 从底部开始上滑且当前状态处于‘非’正在刷新REFRESHING状态(pull,release,done三种状态互斥)时
if (footerState != REFRESHING && isFromLastItem) {
// 手势上滑程度
float increase = footerGestureStartY - event.getY();
listenFooterStateChanged(increase);
}
break;
// 手势抬起时会出现三种不同的状态(release,done,pull)
case MotionEvent.ACTION_UP:
// 头部
if (headerState != REFRESHING) {
if (headerState == DONE) {
} else if (headerState == PULL) {
headerState = DONE;
changeHeaderViewByState();
} else if (headerState == RELEASE) {
headerState = REFRESHING;
changeHeaderViewByState();
onRefresh();
}
}
isFromFirstItem = false;
isHeaderBack = false;
// 底部
if (footerState != REFRESHING) {
if (footerState == DONE) {
} else if (footerState == PULL) {
footerState = DONE;
changeFooterViewByState();
} else if (footerState == RELEASE) {
footerState = REFRESHING;
changeFooterViewByState();
onLoadMore();
}
}
isFromLastItem = false;
isFooterBack = false;
break;
}
return super.onTouchEvent(event);
}
/**
* 监听头部状态随手势下滑程度的变化
*
* @param decline
*/
private void listenHeaderStateChanged(float decline) {
// 当前状态:下拉刷新(0 < decline < 指定高度)
if (headerState == PULL) {
Log.i(TAG_HEADER, ">>>state=" + headerState + ",decline=" + decline);
if (decline >= viewLayoutHeight + pullHeighSpec) {
headerState = RELEASE;
isHeaderBack = true;
changeHeaderViewByState();
} else if (decline <= 0) {
headerState = DONE;
changeHeaderViewByState();
}
}
// 当前状态:松手刷新(decline >= 指定高度时)
else if (headerState == RELEASE) {
Log.i(TAG_HEADER, ">>>state=" + headerState + ",decline=" + decline);
if (decline > 0 && decline < (viewLayoutHeight + pullHeighSpec)) {
headerState = PULL;
changeHeaderViewByState();
} else if (decline <= 0) {
headerState = DONE;
changeHeaderViewByState();
}
}
// 当前状态:完成(decline = 0)
else if (headerState == DONE) {
Log.i(TAG_HEADER, ">>>state=" + headerState + ",decline=" + decline);
if (decline > 0) {
headerState = PULL;
changeHeaderViewByState();
}
}
// 根据不同的状态来改变头部布局的上边距,达到头部跟随手势一起下滑的效果
// 下拉刷新状态下:上边距逐渐增大
if (headerState == PULL) {
int topPadding = (int) ((-1 * viewLayoutHeight + (decline)));
headerViewLayout.setPadding(headerViewLayout.getPaddingLeft(),
topPadding, headerViewLayout.getPaddingRight(),
headerViewLayout.getPaddingBottom());
headerViewLayout.invalidate();
}
// 松手刷新状态下:上边距逐渐减小
if (headerState == RELEASE) {
int topPadding = (int) ((decline - viewLayoutHeight));
headerViewLayout.setPadding(headerViewLayout.getPaddingLeft(),
topPadding, headerViewLayout.getPaddingRight(),
headerViewLayout.getPaddingBottom());
headerViewLayout.invalidate();
}
}
/**
* 监听底部状态随手势下滑程度的变化
*/
private void listenFooterStateChanged(float increase) {
// 当前状态:上拉刷新(0 < increase < 指定高度)
if (footerState == PULL) {
Log.i(TAG_FOOTER, ">>>state=" + footerState + ",increase="
+ increase);
if (increase >= viewLayoutHeight + pullHeighSpec) {
footerState = RELEASE;
isFooterBack = true;
changeFooterViewByState();
} else if (increase <= 0) {
footerState = DONE;
changeFooterViewByState();
}
}
// 当前状态:松手刷新(increase >= 指定高度时)
else if (footerState == RELEASE) {
Log.i(TAG_FOOTER, ">>>state=" + footerState + ",increase="
+ increase);
if (increase > 0 && increase < (viewLayoutHeight + pullHeighSpec)) {
footerState = PULL;
changeFooterViewByState();
} else if (increase <= 0) {
footerState = DONE;
changeFooterViewByState();
}
}
// 当前状态:完成(increase <= 0)
else if (footerState == DONE) {
Log.i(TAG_FOOTER, ">>>state=" + footerState + ",increase="
+ increase);
if (increase > 0) {
footerState = PULL;
changeFooterViewByState();
}
}
// 根据不同的状态来改变底部布局的下边距,达到头部跟随手势一起上滑的效果
// 下拉刷新状态下:上边距逐渐增大
if (footerState == PULL) {
int bottomPaddding = (int) ((-1 * viewLayoutHeight + (increase)));
footerViewLayout.setPadding(footerViewLayout.getPaddingLeft(),
footerViewLayout.getPaddingTop(),
footerViewLayout.getPaddingRight(), bottomPaddding);
footerViewLayout.invalidate();
}
// 松手刷新状态下:上边距逐渐减小
if (footerState == RELEASE) {
int bottomPadding = (int) ((increase - viewLayoutHeight));
footerViewLayout.setPadding(footerViewLayout.getPaddingLeft(),
footerViewLayout.getPaddingTop(),
footerViewLayout.getPaddingRight(), bottomPadding);
footerViewLayout.invalidate();
}
}
/**
* 根据当前状态同步刷新头部界面
*/
private void changeHeaderViewByState() {
switch (headerState) {
// 下拉刷新:除进度条外全部显示
case PULL:
headerArrowImage.setVisibility(View.VISIBLE);
headerArrowImage.clearAnimation();
if (isHeaderBack) {
isHeaderBack = false;
headerArrowImage.clearAnimation();
headerArrowImage.startAnimation(reverseAnimation);
}
headerProgressImage.clearAnimation();
headerProgressImage.setVisibility(View.GONE);
headerTipsText.setVisibility(View.VISIBLE);
headerTipsText.setText("下拉刷新");
headerLastUpdateText.setVisibility(View.VISIBLE);
break;
// 松手刷新:除进度条外全部显示
case RELEASE:
headerArrowImage.setVisibility(View.VISIBLE);
headerArrowImage.clearAnimation();
headerArrowImage.startAnimation(rotateAnimation);
headerProgressImage.clearAnimation();
headerProgressImage.setVisibility(View.GONE);
headerTipsText.setVisibility(View.VISIBLE);
headerTipsText.setText("松手刷新");
headerLastUpdateText.setVisibility(View.VISIBLE);
break;
// 正在刷新:隐藏指示图标、显示加载进度条
case REFRESHING:
headerViewLayout.setPadding(headerViewLayout.getPaddingLeft(),
headerTopPadding, headerViewLayout.getPaddingRight(),
headerViewLayout.getPaddingBottom());
headerViewLayout.invalidate();
headerArrowImage.clearAnimation();
headerArrowImage.setVisibility(View.GONE);
headerProgressImage.setVisibility(View.VISIBLE);
headerProgressImage.startAnimation(progressAnimation);
headerTipsText.setText("正在拼命加载...");
headerLastUpdateText.setVisibility(View.GONE);
break;
// 完成:恢复原样
case DONE:
headerViewLayout.setPadding(headerViewLayout.getPaddingLeft(), -1
* viewLayoutHeight, headerViewLayout.getPaddingRight(),
headerViewLayout.getPaddingBottom());
headerViewLayout.invalidate();
headerArrowImage.clearAnimation();
headerArrowImage.setImageResource(R.drawable.ic_pull_arrow_down);
headerProgressImage.clearAnimation();
headerProgressImage.setVisibility(View.GONE);
headerTipsText.setText("下拉刷新");
headerLastUpdateText.setVisibility(View.VISIBLE);
break;
}
}
/**
* 根据当前状态同步刷新底部界面
*/
private void changeFooterViewByState() {
switch (footerState) {
// 上拉刷新、除进度条隐藏、箭头向下
case PULL:
footerArrowImage.setVisibility(View.VISIBLE);
footerArrowImage.startAnimation(rotateAnimation);
if (isFooterBack) {
isFooterBack = false;
footerArrowImage.clearAnimation();
}
footerProgressImage.clearAnimation();
footerProgressImage.setVisibility(View.GONE);
footerTipsText.setVisibility(View.VISIBLE);
footerTipsText.setText("上拉加载更多");
footerLastUpdateText.setVisibility(View.VISIBLE);
break;
// 松手刷新:除进度条外全部显示
case RELEASE:
footerArrowImage.setVisibility(View.VISIBLE);
footerArrowImage.startAnimation(reverseAnimation);
footerProgressImage.clearAnimation();
footerProgressImage.setVisibility(View.GONE);
footerTipsText.setVisibility(View.VISIBLE);
footerTipsText.setText("松手加载");
footerLastUpdateText.setVisibility(View.VISIBLE);
break;
// 正在刷新:隐藏指示图标、显示加载进度条
case REFRESHING:
footerViewLayout.setPadding(footerViewLayout.getPaddingLeft(),
footerViewLayout.getPaddingTop(),
footerViewLayout.getPaddingRight(), footerBottomPadding);
footerViewLayout.invalidate();
footerArrowImage.clearAnimation();
footerArrowImage.setVisibility(View.GONE);
footerProgressImage.setVisibility(View.VISIBLE);
footerProgressImage.startAnimation(progressAnimation);
footerTipsText.setText("正在拼命加载...");
footerLastUpdateText.setVisibility(View.GONE);
break;
// 完成:恢复原样
case DONE:
footerViewLayout.setPadding(footerViewLayout.getPaddingLeft(),
footerViewLayout.getPaddingTop(),
footerViewLayout.getPaddingRight(), -1 * viewLayoutHeight);
footerViewLayout.invalidate();
footerArrowImage.clearAnimation();
footerArrowImage.setImageResource(R.drawable.ic_pull_arrow_down);
footerProgressImage.clearAnimation();
footerProgressImage.setVisibility(View.GONE);
footerTipsText.setText("上拉加载更多");
footerLastUpdateText.setVisibility(View.VISIBLE);
break;
}
}
/**
* 进入头部刷新状态时,执行用户指定操作
*/
private void onRefresh() {
if (onRefreshListener != null) {
onRefreshListener.onRefresh();
}
}
/**
* 进入底部加载更多状态时,执行用户指定的操作
*/
private void onLoadMore() {
if (onLoadMoreListener != null) {
onLoadMoreListener.onLoadMore();
}
}
/**
* 用户调用此方法,注入头部刷新接口的子类对象
*
* @param refreshListener
*/
public void setOnRefreshListener(OnRefreshListener refreshListener) {
this.onRefreshListener = refreshListener;
}
/**
* 用户调用此方法,注入底部加载更多接口的子类对象
*
* @param loadMoreListener
*/
public void setOnLoadMoreListener(OnLoadMoreListener loadMoreListener) {
this.onLoadMoreListener = loadMoreListener;
}
/**
* 用户调用此方法,注入头部刷新操作完成时的时间
*/
public void onRefreshComplete(String update) {
headerLastUpdateText.setText(update);
onRefreshComplete();
}
public void onRefreshComplete() {
headerState = DONE;
changeHeaderViewByState();
}
/**
* 用户调用此方法,注入头部刷新操作完成时的时间
*/
public void onLoadMoreComplete(String update) {
footerLastUpdateText.setText(update);
footerState = DONE;
changeFooterViewByState();
}
public void onLoadMoreComplete() {
footerState = DONE;
changeFooterViewByState();
}
}
三、总结
1、需要捕获的用户手势:down按下、up抬起、move移动。主要捕获move状态下手指(纵坐标)在屏幕上滑动的距离,通过计算下滑或上滑的距离decline/increase来改变头部(底部)的状态。
2、列表的5种互斥状态:pull、release、refreshing、down、back。下拉(上拉)、临界、松手、进入刷新()状态、刷新完成。
3、头部(底部)布局高度随手势变化:初始状态下,头部上边距和底部下边距均为零。下拉头部布局时,头部上边距逐渐增大,返回时逐渐减小;上拉底部布局时,底部下边距逐渐增大, 返回时逐渐减小。
4、头部(底部)动画效果随手势变化:下拉临界时,头部箭逆置,原路返回时不变;上拉临界时,底部箭头逆置,原路返回时不变。
5、用户事件接口:下拉刷新OnRefreshListener、上拉加载更多分页数据OnLoadMoreListener,同步更新最近时间。