ListView 在日常工作中使用还是非常频繁的,由于它的缓存机制保证加载显示庞大的数据却不会造成 OOM 或者是卡顿。有的时候我们的数据是随时更新的,抑或者我们的数据十分庞大,需要分批加载,我们就需要给 ListView 来加上上拉或者下拉刷新功能。
Google 为了规范化提供了 SwipeRefreshLayout
来帮助我们完成这个操作,使用效果如下图:
新浪微博国际版安卓客户端使用的就是这个。
这里是该控件的官方介绍:SwipeRefreshLayout
这个控件使用起来也十分简单,在官方文档中也明确说明了,只需要将我们需要刷新的 View 作为该控件的子 View 即可,并且 SwipeRefreshLayout 只允许有一个子 View。
就像这样:
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v4.widget.SwipeRefreshLayout>
然后我们为 SwipeRefreshLayout 控件添加一个监听器,就可以监听我们的下拉刷新了:
SwipeRefreshLayout refreshLayout = (SwipeRefreshLayout) findViewById(R.id.refresh_layout);
refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
//更新数据...
}
});
这样刷新之后,就会显示一个进度条,如果我们刷新完了,则只需要调用 setRefreshing(false)
方法就可以将进度条隐藏。同理,也可以调用 setRefreshing(true)
来显示进度条。
大致使用就是这么简单,还有几个比较常用的方法:
ListView 为我们提供了 Header 和 Footer,并且提供了相应的添加方法 addHeaderView(View view)/addFooterView(View view)。
我们可以在这两个东西上面做做文章,来达到我们的目的。
先看一下最简单的 Header 和 Footer:
View headView = LayoutInflater.from(this).inflate(R.layout.listview_head, null);
View footView = LayoutInflater.from(this).inflate(R.layout.listview_foot, null);
listview.addHeaderView(headView);
listview.addFooterView(footView);
通过上面的代码,给 ListView 添加一个头部和底部,显示效果如下:
需要注意的是,在 XML 文件中无法给 ListView 的头和脚设置宽高(设置了也不起作用),所以我们需要在代码中设置
在代码中设置快高和设置其他 View 的宽高一样,都是使用 setLayoutParams
方法,但是需要注意的是,HeaderView 和 FooterView 都是包含在 ListView 当中的,所以其参数应该是:AbsListView.LayoutParams.MATCH_PARENT
或者 AbsListView.LayoutParams.WRAP_CONTENT
或者固定值
,例如:
View headView = LayoutInflater.from(this).inflate(R.layout.listview_head, null);
headView.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT,
AbsListView.LayoutParams.WRAP_CONTENT));
现在我们知道了 HeaderView 和 FooterView 的存在,就可以利用他们来添加帮助我们做到下拉刷新或者点击加载之类的操作了。
我们需要做的就是自定义一个 ListView,然后再结合手势操作,来完成我们想要的效果。
分析一下,下拉刷新的时候,我们需要确定几点:
OnScrollListener
有一个方法 onScroll
,该方法有一个参数:firstVisibleItem
,表示 ListView 中显示的第一个 Item,该值会随着滑动随时改变,当该值为 0 的时候,不就正好是第一个 Item 了么。
ListView 还有一个 getChildAt(int index)
方法,可以根据坐标来获取 ListView 当中的 ItemView,获取到该 Item 之后,通过调用它的 getTop
方法,就可以获取到该 Item 距离父容器顶部的距离。
两者结合,不就可以判断出来了么?!
重写 ListView 的 onTouchEvent
方法,判断 ACTION_MOVE
在 Y 轴的距离是否大于一定的距离,如果大于,则判定为滑动。
那么究竟滑动多长才算是滑动呢??
Android 提供了一个常量:TouchSlop
来作为滑动的“临界点”,每个手机厂商修改过后的系统,该常量也会不同。
可以通过 ViewConfiguration.get(this).getScaledTouchSlop() 来获取该常量。
现在这两点需求都已经满足了,接下来我们需要做的就是将 ListView 的 Header 隐藏起来,待下拉的时候随着下拉手势逐渐显示出来。
直接使用 setVisibility
方法?答案是否定的,使用这个方法来隐藏 ListView 的 Header,不管参数是 INVISIBLE
还是 GONE
,效果都不好,都仅仅是将 HeaderView 隐藏起来了,但是位置变成空白(ListView 并没有顶上去),如图:
网上搜了一圈,找到两个方法:
方法一:使用 setPadding 方法,将 Top Padding 设置为 -Header高度
这个很好理解吧,Padding 嘛,通俗的理解成内边距嘛,设置为负的 HeaderView 高度,自然不就不显示了么!
需要注意的是,获取 HeaderView 的高度需要在 onMeasure 方法当中或者该方法执行之后去获取
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setPadding(0, -headView.getHeight(), 0, 0);
}
方法二:使用一个布局作为 HeadView 的父布局,然后 addHeaderView 的时候将父布局作为参数传入,然后调用 HeaderView 的 setVisibility 方法,参数设置为 GONE
View headerView = LayoutInflater.from(context).inflate(R.layout.listview_head, null);
RelativeLayout headerViewParentLayout = new RelativeLayout(context);
layout.addView(headerView);
headerView.setVisibility(View.GONE);
至此,HeaderView 已经隐藏起来了,接下来需要做的就是让 HeaderView 随着手指滑动来显示。
我们知道,我们手指在屏幕上操作,可以分为三个动作:ACTION_DOWN
,ACIONT_MOVE
,ACTION_UP
,我们可以在这三个操作当中处理我们的需求:
还有一些细节需要处理,这样可以让我们的 ListView 更加的人性化一些:
对于一些属性,我们还需要定义一下:
自定义属性
<!--自定义 ListView 属性-->
<declare-styleable name="CustomListView">
<!--ListView HeaderView 的高度-->
<attr name="header_height" format="dimension" />
<!--ListView HeaderView 背景色-->
<attr name="header_background_color" format="color" />
<!--ListView HeaderView 隐藏时间-->
<attr name="hide_header_time" format="integer" />
<!--下拉时,ListView HeaderView 的提示文本-->
<attr name="down_hint_text" format="string" />
<!--下拉时,ListView HeaderView 的提示文本颜色-->
<attr name="down_hint_text_color" format="color" />
<!--松手时,ListView HeaderView 的提示文字-->
<attr name="up_hint_text" format="string" />
<!--松手时,ListView HeaderView 的提示文字颜色-->
<attr name="up_hint_text_color" format="color" />
<!--刷新时,进度条图片-->
<attr name="refresh_progress_image" format="integer" />
</declare-styleable>
CustomListView.java
public class CustomListView extends ListView implements AbsListView.OnScrollListener {
private Context mContext;
/**
* 系统设定 ACTION_MOVE 常量
*/
private int slop;
/**
* ListView Header View
*/
private View headerView;
/**
* ListView Footer View
*/
private View footView;
/**
* ListView HeaderView 的高度
*/
private int headerViewHeight;
/**
* ListView HeaderView 的高度的一半(纯粹是因为避免魔法值,蛋疼)
*/
private int halfHeaderViewHeight;
/**
* 手指按下时的 Y 坐标
*/
private float startY = 0;
/**
* 首个显示的 Item
*/
private int firstVisibleItem = 0;
/**
* ListView HeaderView 当中的文本
*/
private TextView headerViewText;
/**
* ListView HeaderView 当中的进度条
*/
private ImageView refreshProgressImage;
/**
* ListView HeaderView 当中的向上向下图片
*/
private ImageView image;
/**
* 边距长度 - 用于隐藏 HeaderView
*/
private int paddingSize = 0;
/**
* ListView 头颜色
*/
private int headerViewBackgroundColor;
/**
* 隐藏 ListView HeaderView 的时间
*/
private int hideHeaderTime;
/**
* ListView HeaderView 下拉时的提示文字
*/
private String downHintText;
/**
* ListView HeaderView 下拉时提示文字的颜色
*/
private int downHintTextColor;
/**
* 松开刷新提示文字
*/
private String upHintText;
/**
* 松开刷新提示文字颜色
*/
private int upHintTextColor;
/**
* 刷新时,进度条图片
*/
private int refreshProgressImageId;
/**
* HeaderView 指示箭头是否向上
*/
private boolean arrowUp = true;
public CustomListView(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
//当手指滑动距离为系统设定 ACTION_MOVE 的三倍时,ListView HeaderView 开始显示
slop = ViewConfiguration.get(mContext).getScaledTouchSlop() * 3;
setOnScrollListener(this);
//获取 HeaderViewHeight 属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomListView);
headerViewHeight = (int) typedArray.getDimension(R.styleable.CustomListView_header_height, 0);
halfHeaderViewHeight = headerViewHeight / 2;
headerViewBackgroundColor = typedArray.getColor(R.styleable.CustomListView_header_background_color, Color.WHITE);
hideHeaderTime = typedArray.getInteger(R.styleable.CustomListView_hide_header_time, 5000);
downHintText = typedArray.getString(R.styleable.CustomListView_down_hint_text);
downHintTextColor = typedArray.getColor(R.styleable.CustomListView_down_hint_text_color, Color.BLACK);
upHintText = typedArray.getString(R.styleable.CustomListView_up_hint_text);
upHintTextColor = typedArray.getColor(R.styleable.CustomListView_up_hint_text_color, Color.BLACK);
refreshProgressImageId = typedArray.getResourceId(R.styleable.CustomListView_refresh_progress_image, R.drawable.refresh_progress);
//解析ListView HeaderView
LayoutInflater inflater = LayoutInflater.from(context);
headerView = inflater.inflate(R.layout.listview_head, null);
headerView.setBackgroundColor(headerViewBackgroundColor);
//HeaderView当中的组件
headerViewText = (TextView) headerView.findViewById(R.id.text);
refreshProgressImage = (ImageView) headerView.findViewById(R.id.refresh_image);
refreshProgressImage.setImageResource(refreshProgressImageId);
image = (ImageView) headerView.findViewById(R.id.image);
//设置 HeaderView 的高度
headerView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, headerViewHeight));
//添加 HeaderView
addHeaderView(headerView);
// 设置 PaddingTop 为 -高度,从而达到初始化时隐藏Header 的目的
setPadding(0, -headerViewHeight, 0, 0);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//手指按下时,获取手指 Y 坐标
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//判断 ListView 处于最顶端
if (firstVisibleItem == 0) {
View view = getChildAt(0);
if (view.getTop() < 0) {
//移动距离大于一定长度,这里长度常量设置为系统 slop*3
float moveY = ev.getY();
//当手指移动距离大于设定常量,并且不大于HeaderView 的高度时,逐渐显示 HeaderView
if ((moveY - startY) > slop && paddingSize <= headerViewHeight) {
//手指超过常量之后,移动的距离,该距离就是HeaderView显示的部分
float moveLength = ev.getY() - startY - slop;
paddingSize = headerView.getHeight() - (int) moveLength;
if (paddingSize >= 0) {
//显示HeaderView,并设置提示文字
setPadding(0, -paddingSize, 0, 0);
headerViewText.setText(downHintText);
headerViewText.setTextColor(downHintTextColor);
} else {
//HeaderView 完全显示之后,HeaderView 的提示文字
headerViewText.setText(upHintText);
headerViewText.setTextColor(upHintTextColor);
//旋转HeaderView 的指示箭头
if (arrowUp) {
RotateAnimation animation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(500);
animation.setRepeatCount(0);
animation.setFillAfter(true);
image.setPivotX(image.getWidth() / 2);
image.setPivotY(image.getHeight() / 2);
image.startAnimation(animation);
arrowUp = false;
}
}
}
}
}
break;
//松开手指
case MotionEvent.ACTION_UP:
//如果HeaderView 没有完全显示,则继续隐藏
float upY = ev.getY() - startY - slop;
if (upY < headerViewHeight) {
setPadding(0, -headerViewHeight, 0, 0);
} else {
//隐藏提示文字及只是箭头,显示加载进度条
headerViewText.setVisibility(View.GONE);
image.clearAnimation();
image.setVisibility(View.GONE);
refreshProgressImage.setVisibility(View.VISIBLE);
arrowUp = true;
//加载进度条动画
RotateAnimation animation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(1000);
animation.setRepeatCount(-1);
animation.setInterpolator(new LinearInterpolator());
refreshProgressImage.startAnimation(animation);
//进度条显示5秒
handler.sendEmptyMessageDelayed(1, hideHeaderTime);
}
break;
default:
}
return super.onTouchEvent(ev);
}
@SuppressLint("HandlerLeak")
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
setPadding(0, -headerViewHeight, 0, 0);
refreshProgressImage.setVisibility(View.GONE);
refreshProgressImage.clearAnimation();
headerViewText.setVisibility(View.VISIBLE);
headerViewText.setText("下拉刷新");
image.setImageResource(R.drawable.move_down);
image.setVisibility(View.VISIBLE);
}
};
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem == 0) {
this.firstVisibleItem = firstVisibleItem;
}
}
}
注释已经写的很清楚了,代码也很简单,就不一一解释了,看代码就好。
至此,下拉刷新就做好了,下面开始做点击加载
这个功能就更加简单了,因为“点击加载更多”一般是位于 ListView 的底部,直接添加 FooterView 即可。
当然,你也可以在 ListView 滑动到最底下的时候再添加,只要判断界面显示的最后一条 Item 就是该 ListView 的最后一条 Item,并且该 item 完全显示,就显示 FooterView 就好啦!
如何判断 ListView 已经滑动到底部呢?还得借助 OnScrollListener
中的 onScroll
方法,看代码:
boolean footerShow = true;
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem == 0) {
this.firstVisibleItem = firstVisibleItem;
}
View lastItemView = getChildAt(getChildCount() - 1);
if (lastItemView != null && lastItemView.getBottom() == getHeight() && footerShow) {
View footerView = LayoutInflater.from(mContext).inflate(R.layout.listview_foot, null);
footerView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100));
addFooterView(footerView);
footerShow = false;
}
}
剩下的就是设置一些 FooterView 的显示文字以及动画之类的了,和 HeaderView 一样,不赘言。
完整代码如下:
属性配置
<!--自定义 ListView 属性-->
<declare-styleable name="CustomListView">
<!--ListView HeaderView 的高度-->
<attr name="header_height" format="dimension" />
<!--ListView HeaderView 背景色-->
<attr name="header_background_color" format="color" />
<!--ListView HeaderView 隐藏时间-->
<attr name="hide_header_time" format="integer" />
<!--下拉时,ListView HeaderView 的提示文本-->
<attr name="down_hint_text" format="string" />
<!--下拉时,ListView HeaderView 的提示文本颜色-->
<attr name="down_hint_text_color" format="color" />
<!--松手时,ListView HeaderView 的提示文字-->
<attr name="up_hint_text" format="string" />
<!--松手时,ListView HeaderView 的提示文字颜色-->
<attr name="up_hint_text_color" format="color" />
<!--刷新时,进度条图片-->
<attr name="refresh_progress_image" format="integer" />
</declare-styleable>
CustomListView
public class CustomListView extends ListView implements AbsListView.OnScrollListener {
private Context mContext;
/**
* 系统设定 ACTION_MOVE 常量
*/
private int slop;
/**
* ListView Header View
*/
private View headerView;
/**
* ListView Footer View
*/
private View footerView;
/**
* ListView HeaderView 的高度
*/
private int headerViewHeight;
/**
* ListView HeaderView 的高度的一半(纯粹是因为避免魔法值,蛋疼)
*/
private int halfHeaderViewHeight;
/**
* 手指按下时的 Y 坐标
*/
private float startY = 0;
/**
* 首个显示的 Item
*/
private int firstVisibleItem = 0;
/**
* ListView HeaderView 当中的文本
*/
private TextView headerViewText;
/**
* ListView HeaderView 当中的进度条
*/
private ImageView refreshProgressImage;
/**
* ListView HeaderView 当中的向上向下图片
*/
private ImageView image;
/**
* 边距长度 - 用于隐藏 HeaderView
*/
private int paddingSize = 0;
/**
* ListView 头颜色
*/
private int headerViewBackgroundColor;
/**
* 隐藏 ListView HeaderView 的时间
*/
private int hideHeaderTime;
/**
* ListView HeaderView 下拉时的提示文字
*/
private String downHintText;
/**
* ListView HeaderView 下拉时提示文字的颜色
*/
private int downHintTextColor;
/**
* 松开刷新提示文字
*/
private String upHintText;
/**
* 松开刷新提示文字颜色
*/
private int upHintTextColor;
/**
* 刷新时,进度条图片
*/
private int refreshProgressImageId;
/**
* HeaderView 指示箭头是否向上
*/
private boolean arrowUp = true;
public CustomListView(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
//当手指滑动距离为系统设定 ACTION_MOVE 的三倍时,ListView HeaderView 开始显示
slop = ViewConfiguration.get(mContext).getScaledTouchSlop() * 3;
setOnScrollListener(this);
//获取 HeaderViewHeight 属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomListView);
headerViewHeight = (int) typedArray.getDimension(R.styleable.CustomListView_header_height, 0);
halfHeaderViewHeight = headerViewHeight / 2;
headerViewBackgroundColor = typedArray.getColor(R.styleable.CustomListView_header_background_color, Color.WHITE);
hideHeaderTime = typedArray.getInteger(R.styleable.CustomListView_hide_header_time, 5000);
downHintText = typedArray.getString(R.styleable.CustomListView_down_hint_text);
downHintTextColor = typedArray.getColor(R.styleable.CustomListView_down_hint_text_color, Color.BLACK);
upHintText = typedArray.getString(R.styleable.CustomListView_up_hint_text);
upHintTextColor = typedArray.getColor(R.styleable.CustomListView_up_hint_text_color, Color.BLACK);
refreshProgressImageId = typedArray.getResourceId(R.styleable.CustomListView_refresh_progress_image, R.drawable.refresh_progress);
//解析ListView HeaderView
LayoutInflater inflater = LayoutInflater.from(context);
headerView = inflater.inflate(R.layout.listview_head, null);
headerView.setBackgroundColor(headerViewBackgroundColor);
//HeaderView当中的组件
headerViewText = (TextView) headerView.findViewById(R.id.text);
refreshProgressImage = (ImageView) headerView.findViewById(R.id.refresh_image);
refreshProgressImage.setImageResource(refreshProgressImageId);
image = (ImageView) headerView.findViewById(R.id.image);
//设置 HeaderView 的高度
headerView.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT, headerViewHeight));
//添加 HeaderView
addHeaderView(headerView);
// 设置 PaddingTop 为 -高度,从而达到初始化时隐藏Header 的目的
setPadding(0, -headerViewHeight, 0, 0);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//手指按下时,获取手指 Y 坐标
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//判断 ListView 处于最顶端
if (firstVisibleItem == 0) {
View view = getChildAt(0);
if (view.getTop() < 0) {
//移动距离大于一定长度,这里长度常量设置为系统 slop*3
float moveY = ev.getY();
//当手指移动距离大于设定常量,并且不大于HeaderView 的高度时,逐渐显示 HeaderView
if ((moveY - startY) > slop && paddingSize <= headerViewHeight) {
//手指超过常量之后,移动的距离,该距离就是HeaderView显示的部分
float moveLength = ev.getY() - startY - slop;
paddingSize = headerView.getHeight() - (int) moveLength;
if (paddingSize >= 0) {
//显示HeaderView,并设置提示文字
setPadding(0, -paddingSize, 0, 0);
headerViewText.setText(downHintText);
headerViewText.setTextColor(downHintTextColor);
} else {
//HeaderView 完全显示之后,HeaderView 的提示文字
headerViewText.setText(upHintText);
headerViewText.setTextColor(upHintTextColor);
//旋转HeaderView 的指示箭头
if (arrowUp) {
RotateAnimation animation = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(500);
animation.setRepeatCount(0);
animation.setFillAfter(true);
image.setPivotX(image.getWidth() / 2);
image.setPivotY(image.getHeight() / 2);
image.startAnimation(animation);
arrowUp = false;
}
}
}
}
}
break;
//松开手指
case MotionEvent.ACTION_UP:
//如果HeaderView 没有完全显示,则继续隐藏
float upY = ev.getY() - startY - slop;
if (upY < headerViewHeight) {
setPadding(0, -headerViewHeight, 0, 0);
} else {
//隐藏提示文字及只是箭头,显示加载进度条
headerViewText.setVisibility(View.GONE);
image.clearAnimation();
image.setVisibility(View.GONE);
refreshProgressImage.setVisibility(View.VISIBLE);
arrowUp = true;
//加载进度条动画
RotateAnimation animation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(1000);
animation.setRepeatCount(-1);
animation.setInterpolator(new LinearInterpolator());
refreshProgressImage.startAnimation(animation);
//进度条显示5秒
handler.sendEmptyMessageDelayed(1, hideHeaderTime);
}
break;
default:
}
return super.onTouchEvent(ev);
}
@SuppressLint("HandlerLeak")
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
setPadding(0, -headerViewHeight, 0, 0);
refreshProgressImage.setVisibility(View.GONE);
refreshProgressImage.clearAnimation();
headerViewText.setVisibility(View.VISIBLE);
headerViewText.setText("下拉刷新");
image.setImageResource(R.drawable.move_down);
image.setVisibility(View.VISIBLE);
}
};
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
boolean footerShow = true;
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem == 0) {
this.firstVisibleItem = firstVisibleItem;
}
View lastItemView = getChildAt(getChildCount() - 1);
if (lastItemView != null && lastItemView.getBottom() == getHeight() && footerShow) {
footerView = LayoutInflater.from(mContext).inflate(R.layout.listview_foot, null);
footerView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 100));
addFooterView(footerView);
footerShow = false;
}
}
}