项目Demo:https://github.com/liaozhoubei/CustomViewDemo
下拉刷新和加载更多
下拉刷新和加载更多这两个功能在资讯类的app最容易找到,当你想查看有没有最新的资讯的时候只需要把手指在屏幕上往下一拉就能够更新数据,而加载更多则是拉到当前页面底部的时候想找到往期的内容,这时手指往上一拉,往期内容就加载出来。现在就让我们实现这个功能吧,效果如下:
实现原理
从效果图中分析,我们可以得出以下结论:
1、整体布局是使用ListView实现的
2、上拉加载是在顶部增加数据,加载更多这是在底部增加数据。
然后我们在查看ListVew的详细资料,我们可以看到它拥有addHeaderView()方法,将数据添加到顶部,同时也拥有addFooterView()方法,将数据添加到底部。
现在我们已经明白怎样在顶部和底部添加数据了,但是下拉的时候有下拉属性的布局,上拉加载更多也有一个布局,这又是怎么回事呢?
确实,这是一个很令人迷惑的地方,但是如果我们换一个思路,假设它原本就是在哪里,但是却被隐藏起来呢?
答案却是是如此,ListView先设置头布局到顶部,但是却不在屏幕中显示,只有在下拉的时候才显示出来,同时在下拉的时候链接网络加载更多数据,这就是实现加载更多的原理!
一切都分析完成了,现在我们就来看看代码是如何实现的吧!
代码实现下拉刷新
虽然实现加载更多的功能是依赖于Listview,但是ListVew并没有办法实现我们想要的所有功能,所以我们可以通过继承ListView来实现我们需要的功能。代码如下:
public class RefreshListView extends ListView implements OnScrollListener {
private View mHeaderView; // 头布局
private float downY; // 按下的y坐标
private float moveY; // 移动后的y坐标
private int mHeaderViewHeight; // 头布局高度
public static final int PULL_TO_REFRESH = 0;// 下拉刷新
public static final int RELEASE_REFRESH = 1;// 释放刷新
public static final int REFRESHING = 2; // 刷新中
private int currentState = PULL_TO_REFRESH; // 当前刷新模式
private RotateAnimation rotateUpAnim; // 箭头向上动画
private RotateAnimation rotateDownAnim; // 箭头向下动画
private View mArrowView; // 箭头布局
private TextView mTitleText; // 头布局标题
private ProgressBar pb; // 进度指示器
private TextView mLastRefreshTime; // 最后刷新时间
private OnRefreshListener mListener; // 刷新监听
private View mFooterView; // 脚布局
private int mFooterViewHeight; // 脚布局高度
private boolean isLoadingMore; // 是否正在加载更多
public RefreshListView(Context context) {
super(context);
init();
}
public RefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
/**
* 初始化头布局, 脚布局 滚动监听
*/
private void init() {
initHeaderView();
initAnimation();
initFooterView();
setOnScrollListener(this);
}
/**
* 初始化脚布局
*/
private void initFooterView() {
mFooterView = View.inflate(getContext(), R.layout.layout_footer_list, null);
mFooterView.measure(0, 0);
mFooterViewHeight = mFooterView.getMeasuredHeight();
// 隐藏脚布局
mFooterView.setPadding(0, -mFooterViewHeight, 0, 0);
addFooterView(mFooterView);
}
/**
* 初始化头布局的动画
*/
private void initAnimation() {
// 向上转, 围绕着自己的中心, 逆时针旋转0 -> -180.
rotateUpAnim = new RotateAnimation(0f, -180f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
rotateUpAnim.setDuration(300);
rotateUpAnim.setFillAfter(true); // 动画停留在结束位置
// 向下转, 围绕着自己的中心, 逆时针旋转 -180 -> -360
rotateDownAnim = new RotateAnimation(-180f, -360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
rotateDownAnim.setDuration(300);
rotateDownAnim.setFillAfter(true); // 动画停留在结束位置
}
/**
* 初始化头布局
*/
private void initHeaderView() {
mHeaderView = View.inflate(getContext(), R.layout.layout_header_list, null);
mArrowView = mHeaderView.findViewById(R.id.iv_arrow);
pb = (ProgressBar) mHeaderView.findViewById(R.id.pb);
mTitleText = (TextView) mHeaderView.findViewById(R.id.tv_title);
mLastRefreshTime = (TextView) mHeaderView.findViewById(R.id.tv_desc_last_refresh);
// 提前手动测量宽高
mHeaderView.measure(0, 0);// 按照设置的规则测量
mHeaderViewHeight = mHeaderView.getMeasuredHeight();
System.out.println(" measuredHeight: " + mHeaderViewHeight);
// 设置内边距, 可以隐藏当前控件 , -自身高度
mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
// 在设置数据适配器之前执行添加 头布局/脚布局 的方法.
addHeaderView(mHeaderView);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// 判断滑动距离, 给Header设置paddingTop
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = ev.getY();
System.out.println("downY: " + downY);
break;
case MotionEvent.ACTION_MOVE:
moveY = ev.getY();
System.out.println("moveY: " + moveY);
// 如果是正在刷新中, 就执行父类的处理
if (currentState == REFRESHING) {
return super.onTouchEvent(ev);
}
float offset = moveY - downY; // 移动的偏移量
// 只有 偏移量>0, 并且当前第一个可见条目索引是0, 才放大头部
if (offset > 0 && getFirstVisiblePosition() == 0) {
// int paddingTop = -自身高度 + 偏移量
int paddingTop = (int) (-mHeaderViewHeight + offset);
mHeaderView.setPadding(0, paddingTop, 0, 0);
if (paddingTop >= 0 && currentState != RELEASE_REFRESH) {// 头布局完全显示
System.out.println("切换成释放刷新模式: " + paddingTop);
// 切换成释放刷新模式
currentState = RELEASE_REFRESH;
updateHeader(); // 根据最新的状态值更新头布局内容
} else if (paddingTop < 0 && currentState != PULL_TO_REFRESH) { // 头布局不完全显示
System.out.println("切换成下拉刷新模式: " + paddingTop);
// 切换成下拉刷新模式
currentState = PULL_TO_REFRESH;
updateHeader(); // 根据最新的状态值更新头布局内容
}
return true; // 当前事件被我们处理并消费
}
break;
case MotionEvent.ACTION_UP:
// 根据刚刚设置状态
if (currentState == PULL_TO_REFRESH) {
// - paddingTop < 0 不完全显示, 恢复
mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
} else if (currentState == RELEASE_REFRESH) {
// - paddingTop >= 0 完全显示, 执行正在刷新...
mHeaderView.setPadding(0, 0, 0, 0);
currentState = REFRESHING;
updateHeader();
}
break;
default:
break;
}
return super.onTouchEvent(ev);
}
/**
* 根据状态更新头布局内容
*/
private void updateHeader() {
switch (currentState) {
case PULL_TO_REFRESH: // 切换回下拉刷新
// 做动画, 改标题
mArrowView.startAnimation(rotateDownAnim);
mTitleText.setText("下拉刷新");
break;
case RELEASE_REFRESH: // 切换成释放刷新
// 做动画, 改标题
mArrowView.startAnimation(rotateUpAnim);
mTitleText.setText("释放刷新");
break;
case REFRESHING: // 刷新中...
mArrowView.clearAnimation();
mArrowView.setVisibility(View.INVISIBLE);
pb.setVisibility(View.VISIBLE);
mTitleText.setText("正在刷新中...");
if (mListener != null) {
mListener.onRefresh(); // 通知调用者, 让其到网络加载更多数据.
}
break;
default:
break;
}
}
/**
* 刷新结束, 恢复界面效果
*/
public void onRefreshComplete() {
if (isLoadingMore) {
// 加载更多
mFooterView.setPadding(0, -mFooterViewHeight, 0, 0);
isLoadingMore = false;
} else {
// 下拉刷新
currentState = PULL_TO_REFRESH;
mTitleText.setText("下拉刷新"); // 切换文本
mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);// 隐藏头布局
pb.setVisibility(View.INVISIBLE);
mArrowView.setVisibility(View.VISIBLE);
String time = getTime();
mLastRefreshTime.setText("最后刷新时间: " + time);
}
}
// 将获取的时间格式化
private String getTime() {
long currentTimeMillis = System.currentTimeMillis();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.format(currentTimeMillis);
}
public interface OnRefreshListener {
void onRefresh(); // 下拉刷新
void onLoadMore();// 加载更多
}
public void setRefreshListener(OnRefreshListener mListener) {
this.mListener = mListener;
}
// public static int SCROLL_STATE_IDLE = 0; // 空闲
// public static int SCROLL_STATE_TOUCH_SCROLL = 1; // 触摸滑动
// public static int SCROLL_STATE_FLING = 2; // 滑翔
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// 状态更新的时候
System.out.println("scrollState: " + scrollState);
if (isLoadingMore) {
return; // 已经在加载更多.返回
}
// 最新状态是空闲状态, 并且当前界面显示了所有数据的最后一条. 加载更多
if (scrollState == SCROLL_STATE_IDLE && getLastVisiblePosition() >= (getCount() - 1)) {
isLoadingMore = true;
System.out.println("scrollState: 开始加载更多");
mFooterView.setPadding(0, 0, 0, 0);
setSelection(getCount()); // 跳转到最后一条, 使其显示出加载更多.
if (mListener != null) {
mListener.onLoadMore();
}
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
// 滑动过程
}
}
代码分析
一样看上去,这里面的代码是非常多的,但是也只是分为这几部分:
1、隐藏头布局(下拉刷新)和脚布局(加载更多)
2、设置触摸事件,在下拉的时候显示头布局,上拉的时候显示脚布局
3、在下拉和上拉的执行的时候,我们执行动画,更换头布局和脚步局里面的空间(箭头、文字等)
4、设置回掉方法,在下拉或上拉的时候将数据传回来,同时更新ListView中的数据
那么我们就按照这几部分一个一个的讲解吧!
打开界面的时候隐藏头布局(下拉刷新)
隐藏头布局的代码在以下方法中:
private void initHeaderView() {
mHeaderView = View.inflate(getContext(), R.layout.layout_header_list, null);
mArrowView = mHeaderView.findViewById(R.id.iv_arrow);
pb = (ProgressBar) mHeaderView.findViewById(R.id.pb);
mTitleText = (TextView) mHeaderView.findViewById(R.id.tv_title);
mLastRefreshTime = (TextView) mHeaderView.findViewById(R.id.tv_desc_last_refresh);
// 提前手动测量宽高
mHeaderView.measure(0, 0);// 按照设置的规则测量
mHeaderViewHeight = mHeaderView.getMeasuredHeight();
System.out.println(" measuredHeight: " + mHeaderViewHeight);
// 设置内边距, 可以隐藏当前控件 , -自身高度
mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
// 在设置数据适配器之前执行添加 头布局/脚布局 的方法.
addHeaderView(mHeaderView);
}
由于隐藏脚步局的方法和隐藏头布局的方法相差不大,这里就主要讲解隐藏头布局的方法。
在之前我们了解到ListView中有addHeaderView()方法,将视图或者数据添加到ListVew的顶部,这里就是初始化头布局,然后将其加载到顶部。重要的是这三行代码:
mHeaderView.measure(0, 0);// 按照设置的规则测量
mHeaderViewHeight = mHeaderView.getMeasuredHeight();
mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
为什么要使用measure()方法,这是因为ListView是在Activity执行setContentView()加载完布局之后才后高度和宽度。但是我们在自定义控件中要提前获取控件里面的子布局的高度和宽度,而在measure()传入0这是代表按照原有的,在XML中设定好的大小获取宽高。
然后就是setPadding()方法,这个方法是不是很熟悉,Padding是设置布局的内边距。ListVew的顶部的距离为0,也就是刚好在屏幕的顶部,而将mHeaderView头布局的Top顶部的举例设置为负数,那么mHeaderView将不会在屏幕中显示出来,大家可以自己新建一个项目试试Pading的效果。
以上就是隐藏头布局的代码,然后将隐藏好的头布局直接添加到LisView之中就可以了。
设置触摸事件,在下拉的时候显示头布局
onTouchEvent()触摸事件中的代码:
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
moveY = ev.getY();
// 如果当前已经在下拉刷新状态,那么就不刷新
if (currentState == REFRESHING) {
return super.onTouchEvent(ev);
}
float offset = moveY - downY; // 移动的偏移量
if (offset > 0 && getFirstVisiblePosition() == 0) {
int paddingTop = (int) (-mHeaderViewHeight + offset);
mHeaderView.setPadding(0, paddingTop, 0, 0);
if (paddingTop >= 0 && currentState != RELEASE_REFRESH) {// 头布局完全显示
currentState = RELEASE_REFRESH;
updateHeader(); // 根据最新的状态值更新头布局内容
} else if (paddingTop < 0 && currentState != PULL_TO_REFRESH) { // 头布局不完全显示
currentState = PULL_TO_REFRESH;
updateHeader(); // 根据最新的状态值更新头布局内容
}
return true; // 当前事件被我们处理并消费
}
break;
case MotionEvent.ACTION_UP:
if (currentState == PULL_TO_REFRESH) {
mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
} else if (currentState == RELEASE_REFRESH) {
mHeaderView.setPadding(0, 0, 0, 0);
currentState = REFRESHING;
updateHeader();
}
break;
default:
break;
}
在这段代码中,主要是设置mHeaderView的setPadding()方法,在当前ListView条目为顶部第一条时就将其PaddingTop内边距顶部的距离不断的减小,依据手指移动的距离从负数转为正数,将mHeaderView显示出来。
在下拉的时候显示动画
在触摸事件中有updateHeader()方法,这是用来执行选择执行动画和更换文字的一个方法,当我们下来头布局,头布局完全显示的时候其下箭头变为上箭头,同时更换文字;在释放下拉,手指离开屏幕的时候,箭头消失,变成循环进度条
设置回调方法,更新ListView中的数据
最后便是回调方法了,在这里创建了一个Interface接口类OnRefreshListener,这里面有两个方法,一个是下拉刷新时增加数据,一个是加载更多时增加数据。
这个类在setRefreshListener中被使用,但我们的RefreshListView自定义控件被使用时,调用setRefreshListener方法,这是就要重写OnRefreshListener接口的两个方法,在里面传入要增加的数据,并且更新适配器,这时RefreshListView就能够实时更新数据了。
总结
下拉刷新的自定义视图就说到这里就完结了,我们的精通自定义视图也到此就结束了。
虽然说是精通自定义视图,但说实话这只是应用层的自定义视图的使用,仍然是基于Android提供的框架中修修改改,如果真的想创建出属于自己的自定义视图,恐怕需要进入Android底层或者框架层写代码吧!
但是不管怎么样,学习完这几个自定义视图,相信在应用层这方面的自定义视图都不会有太大的困难了。
在这里的几篇文章中,提纲挈领的说了每个项目之中比较重要的方法,关于具体的逻辑并没有详解,所以这方面需要大家多看代码,又或者直接敲代码,这样才能够理解这里面说蕴含的知识。
项目Demo:https://github.com/liaozhoubei/CustomViewDemo
扩展阅读:
Android 精通自定义视图(1) http://www.jianshu.com/p/c2195269ce44
Android 精通自定义视图(2) http://www.jianshu.com/p/092e126b623f
Android 精通自定义视图(3) http://www.jianshu.com/p/1660479e76ef
Android 精通自定义视图(4) http://www.jianshu.com/p/850e387fc9d8