自定义控件:含下拉刷新和上拉加载 ListView 的原理
2016-03-28 13:07
567 查看
前言
下拉刷新的 ListView,是非常常见的组件。一般情况下,我们会用第三方框架,比如:Android-PullToRefresh、xListView 等实现。第三方框架用起来方便,但如果想个性化定制,需要搞懂其原理。今天,我们自己实现一个支持上拉刷新和下拉加载更多的自定义 ListView,了解其中的原理。基本原理
向 ListView 头部和尾部分别添加 HeaderView、FooterView,默认使其隐藏。下拉时,逐渐使 HeaderView 显示,并不断改变 HeaderView 上的文字为“下拉刷新”、“松开刷新”和“正在加载”,当状态改为正在加载时,调接口加载数据,加载完成恢复原状。上拉时,当 ListView 滑动到最底部并且松开手指,马上显示 FooterView ,调接口加载数据,完成加载恢复默认。最终效果
Gif 录屏工具:LICEcap
定义、初始化成员
定义
首先,照例,定义 class 继承 ListViewpublic class RefreshListView extends ListView,然后对用到的全部成员进行定义
/* 监听接口 */ private OnRefreshListener onRefreshListener; public interface OnRefreshListener { void onRefresh(RefreshListView listView); void onLoad(RefreshListView listView); } /* 头部View、高度 */ private View mHeaderView; private int mHeaderHeight; /* 尾部View、高度 */ private View mFooterView; private int mFooterHeight; /* HeaderView中的控件 */ private ImageView mIvArrow; // 箭头 private ProgressBar mPbRotate; // 进度条 private TextView mTvStatus; // 状态 private TextView mTvTime; // 时间 /* 向上动画、向下动画 */ private RotateAnimation upRotateAnimation; private RotateAnimation downRotateAnimation; /* 当前状态 */ private RefreshState currState = RefreshState.PULL; /* 状态 枚举 */ public enum RefreshState { LOADING, // 正在加载 PULL, // 下拉刷新 RELEASE, // 松开加载 }
监听接口:包含 onRefresh 和 onLoad,作用是:当刷新和加载时回调。
头部、尾部 View:分别来自俩布局文件,作用是:用作头部和尾部
向上、下动画:旋转动画,分别作用于 HeaderView 的箭头,使其转动
状态:分为 LOADING(正在加载)、PULL(下拉刷新)、RELEASE(松开加载),作用是:作为依据,切换 HeaderView 的 UI 显示
初始化
有了最基本的成员,需要初始化,我们在 onSizeChanged 对其初始化,至于为什么用 onSizeChanged,请看我另一篇《对 ViewGroup 生命周期执行顺序的理解》protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 初始化头部 initHeaderView(); // 初始化动画 initAnim(); // 设置滚动事件 setOnScrollListener(this); // 初始化尾部 initFooterView(); }
初始化 HeaderView
/* 初始化头部 */ private void initHeaderView() { mHeaderView = View.inflate(getContext(), R.layout.layout_headerview,null); mHeaderView.measure(0, 0); mHeaderHeight = mHeaderView.getMeasuredHeight(); mHeaderView.setPadding(0, -mHeaderHeight, 0, 0); // 这里省略,初始化headerView 的成员 // mIvArrow(箭头)、mPbRotate(进度)、mTvStatus(状态)、mTvTime(最后更新时间) 略 addHeaderView(mHeaderView); }
其他具体的代码和 xml 代码,这里不再贴了,都是非常简单的,全部都在 Demo 里。但是,要特别注意这几点:
尺寸测量
本例中的 headerView 和 footerView,均来自 inflate。需要手动调用 measure(0,0) 通知系统主动测量,第一个参数表示测量尺寸,第二个参数表示测量模式。测量的知识,请参考《自定义控件:onMeasure 方法和测量原理的理解》
隐藏技巧
隐藏 headerView 和 footerView 是通过设置他们的 paddingTop 为负数实现的。
隐藏 headerView:
mHeaderView.setPadding(0, -mHeaderHeight, 0, 0);
隐藏 footerView:
mFooterView.setPadding(0, -mFooterHeight, 0, 0);
mHeaderHeight、mFooterHeight 分别为对应 View 的宽高
动画角度
/* 初始化动画 */ private void initAnim() { upRotateAnimation = new RotateAnimation(0f, -180f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); upRotateAnimation.setDuration(300); // 动画结束,保持状态 upRotateAnimation.setFillAfter(true); downRotateAnimation = new RotateAnimation(-180f, -360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); downRotateAnimation.setDuration(300); // 动画结束,保持状态 downRotateAnimation.setFillAfter(true); }
我们将状态从下拉刷新改为释放刷新,箭头逆时针180度(0 ~ -180);状态从释放刷新改为下拉刷新,箭头再次逆时针旋转180度(-180 ~ -360)回到原点。即:RotateAnimation 执行后,角度值依然在,第二次执行必须从原来的角度开始。
监听触摸
完成了定义和初始化,接下来是整个 RefreshListView 的核心,这里是最关键也是最坑的,需要在做之前充分分析需求、考虑各种情况,并且调试要耐心。以下是监听触摸的全部代码,注释中有标号的,下边对用有说明
@Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: startY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: int dy = (int) (ev.getY() - startY); // (dy / 2)阻尼效果 int newPaddingTop = -mHeaderHeight + dy / 2; // (一)正在刷新时,不允许改变继续往下拉 if (currState == RefreshState.LOADING){ break; } mHeaderView.setPadding(0, newPaddingTop, 0, 0); // (二)状态的切换 if (newPaddingTop >= 0 && currState == RefreshState.PULL) { // 进入松开刷新 currState = RefreshState.RELEASE; // 根据状态,更新UI refreshHeaderView(); } else if (newPaddingTop < 0 && currState == RefreshState.RELEASE) { // 进入下拉刷新 currState = RefreshState.PULL; // 根据状态,更新UI refreshHeaderView(); } // (三)判断事件是否要交给 ListView 处理 if (dy > 0 && getFirstVisiblePosition() == 0) { return true; } break; case MotionEvent.ACTION_UP: int currPaddingTop = mHeaderView.getPaddingTop(); // (四)松开手指,再根据状态,修改 UI if ( currPaddingTop <= 0 && currState == RefreshState.PULL){ mHeaderView.setPadding(0, -mHeaderHeight, 0, 0); } else if (currPaddingTop > 0 && currState == RefreshState.RELEASE){ currState = RefreshState.LOADING; refreshHeaderView(); // 通知外部,现在正在刷新了 if (onRefreshListener != null){ onRefreshListener.onRefresh(this); } } break; } // (五)必须依然将事件还给 ListView,处理默认的滚动 return super.onTouchEvent(ev); }
(一)正在刷新时,不允许改变继续往下拉
当状态切换到正在刷新(LOADING),就不允许 headerView 再往下拉了,直接 break ,将 ACTION_MOVE 事件交给 ListView 的默认滚动,而不再 mHeaderView.setPadding();
(二)状态的切换
由于 ACTION_MOVE 是持续执行(移动过程中,dispatchTouchEvent 不断将事件包 event 丢给 onTouchEvent),为了保证只在状态改变的时候更新 UI,必须判断:当前的状态是不是等于即将改变的状态,如果不相等,才认为是改变了状态(说的够详细吧)。
(三)判断事件是否要交给 ListView 处理
当手指在 ListView 上不断往下拉,ACTION_MOVE 中的代码,headerView 的 paddingTop 值越来越大。这时,可以很明显的发现,ListView 滑动速度比一般的快了。因为 ACTION_MOVE 不仅被用来改变 headerView 的 paddingTop 值,而且还被 ListView 用来处理滑动,两者相加,速度明显快。所以,在改变 headerView 的 padingTop 时,必须拦截。我的判断条件是:当手指往下拉并且当前可见条目为 0 ,就拦截,这样能省很多不必要的判断。
(四)松开手指,再根据状态,修改 UI
当松开手指,如果 headerView 完全显示(松开刷新状态)就直接切换为正在刷新,并调接口显示数据;如果 headerView 未完全显示(下拉刷新状态)则直接隐藏 headerView,使其恢复初始状态。
(五)必须依然将事件还给 ListView,处理默认的滚动
这个必须注意,如果直接 return true、false,意味着事件到你这里结束了,不再交给 ListView,那么 ListView 将无法滚动。ListView 自带的滚动逻辑可全在 super.onTouchEvent(ev) 中。
根据状态,更新 headerView
/* 根据状态,更新HeaderView的UI */ private void refreshHeaderView() { switch (currState) { case PULL: mIvArrow.startAnimation(downRotateAnimation); mTvStatus.setText("下拉刷新"); break; case RELEASE: mIvArrow.startAnimation(upRotateAnimation); mTvStatus.setText("松开刷新"); break; case LOADING: // 必须要清除动画,否则无法设置隐藏 mIvArrow.clearAnimation(); mHeaderView.setPadding(0, 0, 0, 0); mIvArrow.setVisibility(View.INVISIBLE); mPbRotate.setVisibility(View.VISIBLE); mTvStatus.setText("正在刷新..."); break; } }
这里就是根据各种状态,操作元素。三种状态 PULL、RELEASE、LOADING 分别对应如下如:
处理 footerView
footerView 在初始化的时候添加在 ListView 的尾部,但是默认隐藏(paddingTop 为 -footerViewHeight),那么在什么情况下显示呢? 本例中,当手指松开屏幕并且滑到最后一个时,才显示,并调接口加载数据到 ListView 尾部。@Override public void onScrollStateChanged(AbsListView view, int scrollState) { // 当手指松开、并且最后一个可见 view 为 List 最后一条数据,才显示 footerView if (scrollState == OnScrollListener.SCROLL_STATE_IDLE && getLastVisiblePosition() == getCount() - 1){ mFooterView.setPadding(0, 0, 0, 0); setSelection(Integer.MAX_VALUE); // 调接口,通知外部加载更多 if (onRefreshListener != null){ onRefreshListener.onLoad(this); } } }
这里需要注意:
setSelection 使 ListView 滑到最底部,以显示 footerView
mFooterView.setPadding(0, 0, 0, 0);虽然能让 footerView 显示出来,但如果不往下滑动 ListView,看不
setSelection(Integer.MAX_VALUE)将 ListView 拉到最底部。
完成刷新,状态恢复
恢复 headerView 的状态/* 完成下拉刷新 */ public void completeRefresh(){ mTvStatus.setText("下拉刷新"); mHeaderView.setPadding(0, -mHeaderHeight, 0, 0); mPbRotate.setVisibility(View.INVISIBLE); mIvArrow.setVisibility(View.VISIBLE); // 注意修改状态 currState = RefreshState.PULL; mTvTime.setText("最后刷新:"+getCurrTime()); }
恢复 footerView 的状态
/* 完成加载更多 */ public void completeLoadMore(){ // 隐藏 footerView mFooterView.setPadding(0, -mFooterHeight, 0, 0); setSelection(Integer.MAX_VALUE); }
注意:completeRefresh() 和 completeLoadMore() 是提供给使用者调用的,RefreshListView 并不知道什么时候加载完成。由使用者,开启子线程执行异步任务,执行完成后在 UI 线程调用
mRefreshListView.completeRefresh()或者
mRefreshListView.completeLoadMore()恢复状态。
至此,完成了下拉刷新和上拉加载。
使用演示
设置监听mListView = new RefreshListView(this); mListView.setOnRefreshListener(new RefreshListView.OnRefreshListener(){ @Override public void onRefresh(RefreshListView listView) { // 刷新 - 从服务器加载数据 sendRequest(true); } @Override public void onLoad(RefreshListView listView) { // 加载更多 - 从服务器加载数据 sendRequest(false); } });
请求数据
/* 从服务器加载数据 */ private void sendRequest(boolean isPullRefresh){ new Thread(new Runnable() { @Override public void run() { SystemClock.sleep(2000); mDatas.add(isPullRefresh ? 0 : mDatas.size(), "这是新加载的数据" + new Date()); Message msg = mHandler.obtainMessae(); msg.obj = isPullRefresh; mHandler.sendMessage(msg); } }).start(); }
handler
Handler mHandler = new Handler(){ public void handleMessage(android.os.Message msg) { myAdapter.notifyDataSetChanged(); boolean isRefresh = (Boolean) msg.obj; if (isRefresh){ // 通知ListView应该完成刷新了 mListView.completeRefresh(); } else { // 通知ListView应该完成加载更多了 mListView.completeLoadMore(); } }; };
遇到的坑
1 . ListView item 根布局的不管设置成什么都是默认的 MATCH_PARENT、WRAP_CONTENT,设置成其他的不生效2 . 在 xml 中定义旋转动画,pivotX 属性只支持百分数,不支持小数
3 . api17 及以下版本,addHeaderView 必须在 setAdapter 之前调用。(《Cannot add header view to list – setAdapter has already been called.》)
附录
示例代码:http://git.oschina.net/Integer/RefreshListView相关文章推荐
- 在一个字符串中查找另外一个字符串的全排列出现位置
- Python搜索路径
- css中的选择器
- 敏捷开发 个人感想
- 复利计算器的单元测试
- cocos2d-x触摸事件优先级的探究与实践
- code monkey的进化之路
- linux kernel阅读(一) 进程的生命周期
- struts中的helloword(1)
- 网易实习笔试真题C/C++
- Ubuntu 14.04 LTS 64位安装Oracle 11g (二)
- TableTools导出的文件csv都是中文乱码解决办法
- codeforces 25 E Test KMP
- 两个APP共享AccountManager管理的账号
- RotateImageView 旋转的ImageView
- 字体变大变小
- 彻底关闭Pycharm拼写检查
- 建议
- 进制转换
- Android的快速入门(66期第一天)