更新2016-2-19

代码下载地址已经更新。因为代码很久没更新,已经很落伍了,建议大家使用RecyclerView实现。

参考项目:

https://github.com/bingoogolapple/BGARefreshLayout-Android

https://github.com/baoyongzhang/android-PullRefreshLayout


下拉刷新,Android中非常普遍的功能。为了方便便重写的ListView来实现下拉刷新,同时添加了上拉自动加载更多的功能。设计最初是参考开源中国的Android客户端源码。先看示例图。

        

                                                  图1                                                                                                             图2


        

                                                  图3                                                                                                            图4


         

                                                  图5                                                                                                       图6

下拉刷新的时动画效果: 图1 ==》 图2 ==》 图3 ==》 图4 ==》 图1。

上拉自动加载更多的效果:图5

图6是示例demo的截图

重写后的listview动画效果来源于添加的头部(header)和尾部(footer),listview提供了addHeaderView和addFooterView方法来添加header和footer。大家也可以通过修改头部、尾部的xml文件来定义自己的动画效果。


实现原理

1.下拉刷新

通过onTouchEvent判断手势,来改变listview的header。header的状态共4种,自己定义为:

NONE(对应图1):初始状态

PULL(对应图2):下拉状态,此时松开会还原到状态NONE,并不进行刷新

RELEASE(对应图3):同样是下拉状态,但此刻松开会执行刷新,进入状态REFRESHING

REFRESHING(对应图4):正在执行刷新,刷新结束后进入状态NONE。

header在四种状态切换时不仅改变内部组件,同时改变自身的大小。改变内部组件的体现比如,箭头的朝上或者朝下,文字提示的变化,等待圆圈的显示与否。大小的改变其实就是高度的改变,NONE时header高度为0,RELEASE时header的高度由你下拉的程度决定。

2.加载更多

在listview滑动停止后,判断listview的最后一个item是否已经显示,如果显示说明listview已经滑动到了最底部,这时便触发加载更多的方法,方法结束根据结果改变footer。

3.回调方法

在类中定义了两个接口OnRefreshListener和OnLoadListener,用来定义和提供加载数据的方法,具体实现则交给它们的实现类去做。


示例代码

AutoListView.java

package com.example.autolistview.widget;import com.example.autolistview.R;import com.example.autolistview.utils.Utils;import android.content.Context;import android.util.AttributeSet;import android.view.LayoutInflater;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.view.animation.LinearInterpolator;import android.view.animation.RotateAnimation;import android.widget.AbsListView;import android.widget.ImageView;import android.widget.ProgressBar;import android.widget.TextView;import android.widget.AbsListView.OnScrollListener;import android.widget.ListView;/** * @author SunnyCoffee * @create 2013-10-24 * @version 1.0 * @desc 自定义Listview 下拉刷新,上拉加载更多 */public class AutoListView extends ListView implements OnScrollListener {// 区分当前操作是刷新还是加载public static final int REFRESH = 0;public static final int LOAD = 1;// 区分PULL和RELEASE的距离的大小private static final int SPACE = 20;// 定义header的四种状态和当前状态private static final int NONE = 0;private static final int PULL = 1;private static final int RELEASE = 2;private static final int REFRESHING = 3;private int state;private LayoutInflater inflater;private View header;private View footer;private TextView tip;private TextView lastUpdate;private ImageView arrow;private ProgressBar refreshing;private TextView noData;private TextView loadFull;private TextView more;private ProgressBar loading;private RotateAnimation animation;private RotateAnimation reverseAnimation;private int startY;private int firstVisibleItem;private int scrollState;private int headerContentInitialHeight;private int headerContentHeight;// 只有在listview第一个item显示的时候(listview滑到了顶部)才进行下拉刷新, 否则此时的下拉只是滑动listviewprivate boolean isRecorded;private boolean isLoading;// 判断是否正在加载private boolean loadEnable = true;// 开启或者关闭加载更多功能private boolean isLoadFull;private int pageSize = 10;private OnRefreshListener onRefreshListener;private OnLoadListener onLoadListener;public AutoListView(Context context) {super(context);initView(context);}public AutoListView(Context context, AttributeSet attrs) {super(context, attrs);initView(context);}public AutoListView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);initView(context);}// 下拉刷新监听public void setOnRefreshListener(OnRefreshListener onRefreshListener) {this.onRefreshListener = onRefreshListener;}// 加载更多监听public void setOnLoadListener(OnLoadListener onLoadListener) {this.loadEnable = true;this.onLoadListener = onLoadListener;}public boolean isLoadEnable() {return loadEnable;}// 这里的开启或者关闭加载更多,并不支持动态调整public void setLoadEnable(boolean loadEnable) {this.loadEnable = loadEnable;this.removeFooterView(footer);}public int getPageSize() {return pageSize;}public void setPageSize(int pageSize) {this.pageSize = pageSize;}// 初始化组件private void initView(Context context) {// 设置箭头特效animation = new RotateAnimation(0, -180,RotateAnimation.RELATIVE_TO_SELF, 0.5f,RotateAnimation.RELATIVE_TO_SELF, 0.5f);animation.setInterpolator(new LinearInterpolator());animation.setDuration(100);animation.setFillAfter(true);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);inflater = LayoutInflater.from(context);footer = inflater.inflate(R.layout.listview_footer, null);loadFull = (TextView) footer.findViewById(R.id.loadFull);noData = (TextView) footer.findViewById(R.id.noData);more = (TextView) footer.findViewById(R.id.more);loading = (ProgressBar) footer.findViewById(R.id.loading);header = inflater.inflate(R.layout.pull_to_refresh_header, null);arrow = (ImageView) header.findViewById(R.id.arrow);tip = (TextView) header.findViewById(R.id.tip);lastUpdate = (TextView) header.findViewById(R.id.lastUpdate);refreshing = (ProgressBar) header.findViewById(R.id.refreshing);// 为listview添加头部和尾部,并进行初始化headerContentInitialHeight = header.getPaddingTop();measureView(header);headerContentHeight = header.getMeasuredHeight();topPadding(-headerContentHeight);this.addHeaderView(header);this.addFooterView(footer);this.setOnScrollListener(this);}public void onRefresh() {if (onRefreshListener != null) {onRefreshListener.onRefresh();}}public void onLoad() {if (onLoadListener != null) {onLoadListener.onLoad();}}public void onRefreshComplete(String updateTime) {lastUpdate.setText(this.getContext().getString(R.string.lastUpdateTime,lastUpdate));state = NONE;refreshHeaderViewByState();}// 用于下拉刷新结束后的回调public void onRefreshComplete() {String currentTime = Utils.getCurrentTime();onRefreshComplete(currentTime);}// 用于加载更多结束后的回调public void onLoadComplete() {isLoading = false;}@Overridepublic void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) {this.firstVisibleItem = firstVisibleItem;}@Overridepublic void onScrollStateChanged(AbsListView view, int scrollState) {this.scrollState = scrollState;ifNeedLoad(view, scrollState);}// 根据listview滑动的状态判断是否需要加载更多private void ifNeedLoad(AbsListView view, int scrollState) {if (!loadEnable) {return;}try {if (scrollState == OnScrollListener.SCROLL_STATE_IDLE&& !isLoading&& view.getLastVisiblePosition() == view.getPositionForView(footer) && !isLoadFull) {onLoad();isLoading = true;}} catch (Exception e) {}}/** * 监听触摸事件,解读手势 */@Overridepublic boolean onTouchEvent(MotionEvent ev) {switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:if (firstVisibleItem == 0) {isRecorded = true;startY = (int) ev.getY();}break;case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:if (state == PULL) {state = NONE;refreshHeaderViewByState();} else if (state == RELEASE) {state = REFRESHING;refreshHeaderViewByState();onRefresh();}isRecorded = false;break;case MotionEvent.ACTION_MOVE:whenMove(ev);break;}return super.onTouchEvent(ev);}// 解读手势,刷新header状态private void whenMove(MotionEvent ev) {if (!isRecorded) {return;}int tmpY = (int) ev.getY();int space = tmpY - startY;int topPadding = space - headerContentHeight;switch (state) {case NONE:if (space > 0) {state = PULL;refreshHeaderViewByState();}break;case PULL:topPadding(topPadding);if (scrollState == SCROLL_STATE_TOUCH_SCROLL&& space > headerContentHeight + SPACE) {state = RELEASE;refreshHeaderViewByState();}break;case RELEASE:topPadding(topPadding);if (space > 0 && space < headerContentHeight + SPACE) {state = PULL;refreshHeaderViewByState();} else if (space <= 0) {state = NONE;refreshHeaderViewByState();}break;}}// 调整header的大小。其实调整的只是距离顶部的高度。private void topPadding(int topPadding) {header.setPadding(header.getPaddingLeft(), topPadding,header.getPaddingRight(), header.getPaddingBottom());header.invalidate();}/** * 这个方法是根据结果的大小来决定footer显示的。 * 

* 这里假定每次请求的条数为10。如果请求到了10条。则认为还有数据。如过结果不足10条,则认为数据已经全部加载,这时footer显示已经全部加载 *

* * @param resultSize */public void setResultSize(int resultSize) {if (resultSize == 0) {isLoadFull = true;loadFull.setVisibility(View.GONE);loading.setVisibility(View.GONE);more.setVisibility(View.GONE);noData.setVisibility(View.VISIBLE);} else if (resultSize > 0 && resultSize < pageSize) {isLoadFull = true;loadFull.setVisibility(View.VISIBLE);loading.setVisibility(View.GONE);more.setVisibility(View.GONE);noData.setVisibility(View.GONE);} else if (resultSize == pageSize) {isLoadFull = false;loadFull.setVisibility(View.GONE);loading.setVisibility(View.VISIBLE);more.setVisibility(View.VISIBLE);noData.setVisibility(View.GONE);}}// 根据当前状态,调整headerprivate void refreshHeaderViewByState() {switch (state) {case NONE:topPadding(-headerContentHeight);tip.setText(R.string.pull_to_refresh);refreshing.setVisibility(View.GONE);arrow.clearAnimation();arrow.setImageResource(R.drawable.pull_to_refresh_arrow);break;case PULL:arrow.setVisibility(View.VISIBLE);tip.setVisibility(View.VISIBLE);lastUpdate.setVisibility(View.VISIBLE);refreshing.setVisibility(View.GONE);tip.setText(R.string.pull_to_refresh);arrow.clearAnimation();arrow.setAnimation(reverseAnimation);break;case RELEASE:arrow.setVisibility(View.VISIBLE);tip.setVisibility(View.VISIBLE);lastUpdate.setVisibility(View.VISIBLE);refreshing.setVisibility(View.GONE);tip.setText(R.string.pull_to_refresh);tip.setText(R.string.release_to_refresh);arrow.clearAnimation();arrow.setAnimation(animation);break;case REFRESHING:topPadding(headerContentInitialHeight);refreshing.setVisibility(View.VISIBLE);arrow.clearAnimation();arrow.setVisibility(View.GONE);tip.setVisibility(View.GONE);lastUpdate.setVisibility(View.GONE);break;}}// 用来计算header大小的。比较隐晦。private void measureView(View child) {ViewGroup.LayoutParams p = child.getLayoutParams();if (p == null) {p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT);}int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);int lpHeight = p.height;int childHeightSpec;if (lpHeight > 0) {childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,MeasureSpec.EXACTLY);} else {childHeightSpec = MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED);}child.measure(childWidthSpec, childHeightSpec);}/* * 定义下拉刷新接口 */public interface OnRefreshListener {public void onRefresh();}/* * 定义加载更多接口 */public interface OnLoadListener {public void onLoad();}}

这个类的逻辑复杂的地方就是header四种状态的判断和切换。

以下是几种状态的切换情况

NONE   ==》 PULL

NONE 《==   PULL   ==》 RELEASE

PULL 《==   RELEASE   ==》 REFRESHING

REFRESHING   ==》 NONE

代码中对于 RELEASE  ==》PULL 状态的切换处理的不太理想,好像是纵坐标的记录方式有问题,如果有谁解决,希望能够留言告知。


为了减少篇幅,其他代码就不在贴了。

注意:定义header的xml最外层必须使用线性布局,不然的话会出错。

 

测试代码:点击下载(eclipse工程项目,编码UTF-8)。由于代码涉及公司项目,所以提供的demo是自己新写的。如图6所示。

本篇会持续更新。

更多相关文章

  1. android 解决RecyclerView notifyDataSetChanged刷新闪屏问题(图
  2. Android(安卓)系统原生dialog使用
  3. Android获取手机当前连接的WiFi信息(SSID,IP,连接状态)
  4. 【Android(安卓)开发教程】保存状态等信息
  5. Android(安卓)APP 引导页实现-第一次应用进入时加载
  6. Android(安卓)Webview加载www.youtube.com的问题
  7. Android检查网络状态步骤
  8. 添加并客制化Statusbar中图标显示顺序
  9. Android(安卓)禁止状态栏下拉

随机推荐

  1. android之写文件到sd卡
  2. 安卓布局
  3. Android 的Platform version 和 API Leve
  4. Android xml ListView 的divider属性
  5. [Android各版本特性]Android 7.0 Nougat
  6. Android自学笔记(番外篇):全面搭建Linux环境
  7. android布局属性预览
  8. Android实现文件夹目录选择器
  9. android Java 笔试考题
  10. android appos 笔记