(转载注明: http://blog.csdn.net/itermeng/article/details/52289929 :)
大多App中的一个必备功能:用listView实现下拉刷新和上拉加载,其实有很多大牛都写了类似的Blog,但我还想记录一下,梳理自己的思路,而且我会想之前写的轮播图博客一样,我的重点是在如何写的思路,不愿直接贴代码,想看代码的直接看文章最下面吧 :)
如上gif动图所示,接下来我们要完成下拉刷新的实现。
一. 完成 xml 文件的编写 :
1.完成listView的头布局
结合了帧布局和线性布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<FrameLayout
android:layout_margin="5dp"
android:layout_width="50dp"
android:layout_height="50dp" >
<ImageView
android:id="@+id/iv_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/common_listview_headview_red_arrow" />
<ProgressBar
android:id="@+id/pb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:indeterminateDrawable="@drawable/shape_progress"
android:visibility="invisible" />
</FrameLayout>
<LinearLayout
android:layout_margin="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical" >
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="下拉刷新"
android:textColor="#F00"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_desc_last_refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:singleLine="true"
android:text="最后刷新时间: 2015-10-11 09:20:35"
android:textColor="#666"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
2. 自定义ProgressBar
这里红色渐变的圆圈为自定义的xml
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="-360" >
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:innerRadiusRatio="2.5"
android:shape="ring"
android:thicknessRatio="10"
android:useLevel="false" >
<gradient
android:centerColor="#44FF0000"
android:endColor="#00000000"
android:startColor="#FF0000"
android:type="sweep" />
</shape>
</rotate>
二. 完成逻辑编写实现下拉刷新 :
1. 自定义RefreshListView继承 Listview
继承3个构造方法,并在init()中写initHeaderView()
方法,初始化头布局
public PullToRefreshListView(Context context) {
super(context);
init()
}
public PullToRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init()
}
public PullToRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init()
}
2. 初始化头布局并隐藏
默认让头布局隐藏,主要就是测得我们刚写的xml文件头布局的高度,用padding将它隐藏起来即可,再设置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);
}
3. 滑动事件处理(重点)
万事俱备,头布局也已经隐藏掉,接下来我们想通过手指的滑动,让头布局显示出来,这时就涉及到 onTouchEvent 事件。
public static final int PULL_TO_REFRESH = 0;// 下拉刷新状态
public static final int RELEASE_REFRESH = 1;// 释放刷新状态
public static final int REFRESHING = 2; // 刷新中状态
@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);
}
首先要了解三个状态:
(1)PULL_TO_REFRESH下拉刷新:拉的过程中会进入到“释放刷新”,要是拉到一半就放手即未进入“释放刷新”,就不会去刷新数据。
(2)RELEASE_REFRESH释放刷新:拉到了一定距离,只要放手就会进入”刷新中”状态。
(3)REFRESHING刷新中:状态为正在刷新,此时就可以去请求数据了。
其次从 Down 、Move、Up这三个动作来分析逻辑:
(1) Down: 即手指刚刚点下去,在这里只需要获取当前的 y坐标值(上下滑动,只在意Y值)
(2) Move: 即手指滑动的过程,在这里需要根据移动的距离 offset 和 当前的状态 来 显示出头布局 并 改变状态。
1. 当前状态为“正在刷新”,即不处理(也不看第二点了,return出去)。
2. 只有 偏移量 offset>0, 并且当前第一个可见条目索引是0, 才显示头布局。
paddingTop : 需要隐藏的距离= - 自身高度 + 偏移量。
2.1 (paddingTop >= 0 且 状态不等于“释放刷新”):头布局完全显示,此时状态改为“释放刷新”
2.2 (paddingTop < 0 且 状态不等于“释放刷新”): 头布局不完全显示,此时状态改为“下拉刷新”
2.3 其余情况不管:
情况2.1:
情况2.2:
(3) Up: 即手指抬起,根据Move过程后改变的状态来判断是否进行刷新的逻辑 和 状态修改。
1.状态为”下拉刷新“ :即收起头布局,不需要进行刷新的逻辑。
2.状态为”释放刷新“ :显示完全头布局,修改状态为”正在刷新“,进行刷新的逻辑。
(以上代码中的refreshState方法留到下下一点讲,其实就是一个UI的变化)
(监听对象mListener 采取回调,第六点讲解)
4.下拉刷新箭头旋转动画
以上完成后,接下来完善一下头布局的动画,需要在init()
中初始化initAnimation()
/**
* 初始化头布局的动画
*/
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); // 动画停留在结束位置
}
5. refreshState()修改头布局中UI的变化
第五点同第四点都是有关于UI的变化,比较简单,虽然在onTouchevent中根据不同情况修改了 头布局的显示多少 ,但是头布局中的小控件都有相应的动画或改变,需要抽取一个方法更细致的展示。
/**
* 根据当前状态刷新界面
*/
private void refreshState() {
switch (mCurrentState) {
case STATE_PULL_TO_REFRESH:
tvTitle.setText("下拉刷新");
pbProgress.setVisibility(View.INVISIBLE);
ivArrow.setVisibility(View.VISIBLE);
ivArrow.startAnimation(animDown);
break;
case STATE_RELEASE_TO_REFRESH:
tvTitle.setText("松开刷新");
pbProgress.setVisibility(View.INVISIBLE);
ivArrow.setVisibility(View.VISIBLE);
ivArrow.startAnimation(animUp);
break;
case STATE_REFRESHING:
tvTitle.setText("正在刷新...");
ivArrow.clearAnimation();// 清除箭头动画,否则无法隐藏
pbProgress.setVisibility(View.VISIBLE);
ivArrow.setVisibility(View.INVISIBLE);
break;
default:
break;
}
}
6.下拉刷新监听(重点!!!!!!回调!!!!)
运用回调,使在手指抬起Up后,能够调用方法,进行刷新的逻辑。
(1)下拉刷新的回调接口
public interface OnRefreshListener {
void onRefresh();
}
(2)暴露接口,设置监听
public void setOnRefreshListener(OnRefreshListener listener) {
mListener = listener;
}
(3)定义成员变量,接收监听对象
private OnRefreshListener mListener;
(4) 在合适的地方进行回调
(之前第三点中onTouchevent事件中进行回调)
if (mListener != null) {
mListener.onRefresh();
}
(5). 前端界面设置回调(并非RefreshListView类,是使用了该listView类中写!!!)
lvList.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
// 刷新数据
}
});
7. 刷新结束,收起控件
最后一步,刷新完成,恢复控件原始位置,状态恢复,标题恢复,刷新完成的条件下更新时间。
public void onRefreshComplete(boolean success) {
mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
mCurrentState = STATE_PULL_TO_REFRESH;
tvTitle.setText("下拉刷新");
pbProgress.setVisibility(View.INVISIBLE);
ivArrow.setVisibility(View.VISIBLE);
if (success) {// 只有刷新成功之后才更新时间
setCurrentTime();
}
}
以上就是所有的逻辑,有关于”下拉刷新“的重点,其实各个细节都会有耦合的部分,尽量抽取出一个个小模块进行讲解,其中最重要的就是 第三点.滑动事件处理 和 第六点.下拉刷新监听回调。大致这几个步骤,讲的还是蛮细的,适合入门吧,也不知道这样分开讲好不好,总之,希望对你们有帮助 :)
后续在这里 :http://blog.csdn.net/itermeng/article/details/52297286 :)