前:本文为QiaoJim原创,转载请附原文链接,谢谢合作!
http://blog.csdn.net/qiao_jim/article/details/79076518
-----------------------------------------------------------------------------------------------
本篇主要封装一个自定义view,主要为了满足:1、下拉刷新;2、底部加载更多;3、支持许多自定义属性
本片主要讲解封装思路,想快速使用的请戳:android 下拉刷新+底部加载更多 QJPageReloadView使用
源码GitHub地址:QiaoJim/QJViews
需要读者对于android touch事件分发拦截机制有一定的了解,如果不是很懂,推荐一个博客:
-----------------------------------------------------------------------------------------------
一、整体思路
1、QJPageReloadView包含:顶部下拉刷新view(QJHeaderView)+listview+底部加载更多view(TextView)
2、(简单考虑事件拦截)
当listview已到达顶部,继续下滑,则拦截事件,加载下拉刷新view(QJHeaderView);
当listview已到达底部,继续上滑,则拦截事件,加载底部的加载更多view;
其他,不拦截,任其传递给listview处理
3、在拦截后是下拉刷新动作时,回调onRefresh()方法,自定义刷新逻辑;
4、在拦截后是加载更多动作时,回调onLoadMore()方法,自定义加载更多逻辑(根据已显示的totalCount可支持分页加载);
二、加载3个view
我们的view包含3个view,我们继承线性布局,然后竖向一次添加3个view即可。
我们在构造方法里加载自定义属性后(略),便可在onFinishInflate()方法中,调用加载我们3个view的方法(如下)。
@Override
protected void onFinishInflate() {
super.onFinishInflate();
initChildView();
}
/*
* 实例子view,依次添加
* 1.顶部下拉刷新view
* 2.中间的listview
* 3.底部加载更多view*/
private void initChildView() {
setOrientation(VERTICAL);
addHeaderView();//顶部下拉刷新view
addListView();
addFooterView();//底部加载更多view
}
QJHeaderView这里我包含1个圆形进度条和1个 textview,textview在不同的状态下,显示不同的提示文字,如下拉刷新,释放立即刷新。
中间就是一个listview,底部是1个textview,显示如加载更多,正在加载等提示信息。均调用addView()方法即可。
(由于这里主要将思路,具体内部一些细节就展示了,感兴趣可以参照GitHub源码)
三、touch事件的拦截
首先事件分发默认即可,因为我们将3个view封装成一个大的view(即viewgroup),所以默认就会把事件分发给我们QJPageReloadView的onInterceptTouchEvent()方法。
在onInterceptTouchEvent()中,我们需要根据当前的touch事件,判断当前的动作,进而判断是否拦截该事件。
(具体何时拦截,第一部分整体思路已简单说明,下面给出源码)
/*
* 1.列表到达顶部,且继续下拉,则拦截事件,加载下拉刷新view,返回true
* 2.列表到达底部,且继续上滑,则拦截事件,加载加载更多view,返回true
* 3.其他不拦截,给子listview处理。返回false,交给子 view 的 dispatchTouchEvent()*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
boolean listViewScrolling = false;
float curX = 0, curY = 0;
//滑动事件判定
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// Log.e(TAG, "======== onInterceptTouchEvent ==========\tdown");
curX = preX = ev.getX();
curY = preY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
// Log.e(TAG, "======== onInterceptTouchEvent ==========\tmove");
curX = ev.getX();
curY = ev.getY();
//加载更多view不可见时,全屏幕监听滑动事件
if (!footerViewVisible()) {
if (curY - preY > MIN_DELTA_Y) {
listViewScrolling = true;
moveDown = true;
} else if (preY - curY > MIN_DELTA_Y) {
listViewScrolling = true;
moveUp = true;
}
}
// 加载更多view可见时,监听view区域外的滑动事件。
// 若touch区域在加载更多的view区域,不拦截事件,应该为view的点击事件
else if (!touchInFooterView(ev)) {
if (curY - preY > MIN_DELTA_Y) {
listViewScrolling = true;
moveDown = true;
} else if (preY - curY > MIN_DELTA_Y) {
listViewScrolling = true;
moveUp = true;
}
}
break;
}
// Log.e(TAG, "======== onInterceptTouchEvent ==========\n下滑手势:" + moveDown + " 上滑手势:" + moveUp);
if (listViewArriveTop() && moveDown && !loading && refreshEnable) {
curAction = QJViewAction.ACTION_REFRESH;
intercept = true;
} else if (listViewArriveBottom() && moveUp && !loading && loadMoreEnable) {
curAction = QJViewAction.ACTION_LOAD_MORE;
intercept = true;
} else {
curAction = QJViewAction.ACTION_UNDEFINED;
if (listViewScrolling && !loading)
resetFooterView(QJViewState.CLEAR);
}
// if (intercept) {
// Log.e(TAG, "======== onInterceptTouchEvent ==========\n拦截:" + intercept);
// }
// 重置上滑和下拉标记
resetActionTag();
// 改变pre的 X,Y 坐标
resetPreCoordinate(curX, curY);
return intercept;
}
虽然有不少判断,大体上看主要的:
(1)ACTION_MOVE时,判断是上滑还是下滑,这是moveUp和moveDown标记(有一个滑动最小值,防止点击事件的误判);
(2)51和54行,对listview到达位置的判断和滑动动作结合的判断,设置当前动作,并控制是否拦截intercept标记;
51行:listview到达顶部,且是下滑动作,设置当前动作为下拉刷新;
54行:listview到达底部,且是上滑动作,设置当前动作为加载更多;
(3)当底部加载更多的view已经显示时,touch在FooterView区域时,我们不应该拦截。因为此时期望是点击底部的加载更多view。
四、touch事件的处理
onTouchEvent()中我们主要要处理ACTION_MOVE和ACTION_UP/CANCEL这2类事件。
1、ACTION_MOVE时,主要针对顶部的下拉刷新view,即判断为下拉刷新动作,继续下拉,我们期望我们的HeaderView
高度也随着下拉的距离而增加。并且如果用户又滑动回去,我们期望判断这是一个放弃下拉刷新的动作。所以我们需要进
行相应处理和控制。我们在ACTION_MOVE时,调用handleMoving()方法(如下)。
/*
* 处理滑动事件,调节view的位置*/
private boolean handleMoving(MotionEvent event) {
float curX = event.getX();
float curY = event.getY();
float deltaY = curY - preY;//下滑y轴的增量
//当前动作为下拉刷新时,才操作下拉刷新的view
if (curAction == QJViewAction.ACTION_REFRESH) {
//测量加载更多view的高度
int headerViewHeight = measureHeaderViewHeight(deltaY);
//记录是否放弃下拉刷新操作
cancelRefresh = headerViewHeight < refreshMinHeight;
}
resetPreCoordinate(curX, curY);
return true;
}
其中先记录y轴下滑距离的增量,并传入measureHeaderViewHeight()方法,方法就是改变顶部下拉刷新view的高度,细节略。
然后判断下拉总距离(同时也是HeaderView的高度)是否大于最小的临界值,如果小,这判定为放弃下拉刷新。否则释放手指后,开始刷新方法的回调。
2、ACTION_UP/CANCEL时,即一次动作完成,我们需要根据当前动作,对相应的view做出一些操作。下面是主要的逻辑:
(1)下拉刷新动作时,在没有放弃下拉刷新时,我们回调接口的onRefresh()方法,同时设置HeaderView的状态为Loading。
(2)加载更多时,如果用户设置了“自动加载”为true,则直接回调onLoadMore()方法,同时设置FooterView的状态为Loading。如果用户没有设置自动加载,则显示加载更多view即可。当用户点击该view时,再回调onLoadMore()方法即可
(3)下面的demo有控制,如果已经在加载数据,可忽略新的加载动作,具体参考下面loading标记的控制。
/*
* 处理拦截下来的一次touch事件*/
private boolean handleOnceTouch() {
boolean handled = false;
//回调接口不为null,确定回调函数
if (curAction == QJViewAction.ACTION_REFRESH) {
if (qjPageReloadViewListener != null) {
//没有放弃下拉刷新动作。若手动滑上去,则判定为不刷新
//若已经正在刷新,则屏蔽此次动作
if (!cancelRefresh && !loading) {
qjPageReloadViewListener.onStart();
QJReloadTask.newInstance(this).execute(QJViewAction.ACTION_REFRESH);
loading = true;
resetHeaderView(QJViewState.LOADING);
}
}
handled = true;
} else if (curAction == QJViewAction.ACTION_LOAD_MORE) {
if (qjPageReloadViewListener != null) {
if (autoLoadMore && !loading) {
qjPageReloadViewListener.onStart();
QJReloadTask.newInstance(this).execute(QJViewAction.ACTION_LOAD_MORE);
loading = true;
resetFooterView(QJViewState.LOADING);
} else if (!loading)
resetFooterView(QJViewState.CREATE);
}
handled = true;
}
return handled;
}
五、向外部提供使用方法
可能主要有(基于我的封装):
1、设置回调接口的实现
2、设置listview的adapter
3、设置一些自定义属性,如autoLoad等
4、获取列表已包含的个数
5、更新列表数据