下拉刷新控件,网上有很多版本,有自定义Layout布局的,也有封装控件的,各种实现方式的都有。但是很少有人告诉你具体如何实现的,今天我们就来一步步实现自己封装的 PullToRefreshLayout 完美的解决下拉刷新,上拉加载问题。
首先来分析一下原理,为什么一下拉就可以拉出来一个布局,请看下图,从图中可以看到整个屏幕来说有可见部分,有隐藏部分,当我们手指在屏幕上下拉的时候滑动距离到一定程度了就会拉出 下拉头布局,这样就达到了下拉效果。那么具体代码如何实现待我慢慢像大家解析。
1、想要实现 PullToRefreshLayout 下拉刷新控件那么我们就必须要有个容器,也就是如上图的容器,知道了需要什么那么我们就开始自定义一个容器。
这里如果不会自定义控件的同学可以参考博客 http://blog.csdn.net/cscfas/article/details/51330505
/**
* Created by ZQY on 2016/5/17.
* <p/>
* 这个是上拉加载和下拉刷新的 View
* <p/>
* 注:这里的 android:orientation="vertical" 只能为这个值
*/
public class PullToRefreshLayout extends LinearLayout {
public PullToRefreshLayout(Context context) {
super(context);
initAnim();
}
public PullToRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initAnim();
}
public PullToRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAnim();
}
}
2、有了容器,接下来就拉实现下拉头,上拉脚。LinearLayout 我们都用过线性布局嘛,在这里要注意 android:orientation=“vertical” 只能是垂直布局。这里重写了该控件,目的是在代码中动态添加布局到控件中,实现组合控件,就是PullToRefreshLayout ,这里调用了LinearLayout 的addView() 方法将布局添加到PullToRefreshLayout中。
(1)、添加头部布局,这里也就是下拉头
private void addHeaderView() {
mHeaderView = mInflater.inflate(R.layout.refresh_header, this, false);
mHeaderImageView = (ImageView) mHeaderView
.findViewById(R.id.pull_to_refresh_image);
mHeaderTextView = (TextView) mHeaderView
.findViewById(R.id.pull_to_refresh_text);
mHeaderUpdateTextView = (TextView) mHeaderView
.findViewById(R.id.pull_to_refresh_updated_at);
mHeaderUpdateTextView.setText(DataUtil.getRefreshCompleteTime());
mHeaderProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);
measureView(mHeaderView);
mHeaderViewHeight = mHeaderView.getMeasuredHeight();
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, mHeaderViewHeight);
//设置 topMargin 的值为负的 header View 高度,即将其隐藏在最上方
params.topMargin = -(mHeaderViewHeight);
//添加头部到布局
addView(mHeaderView, params);
}
(2)、添加脚部布局,这里也就是 上拉脚
private void addFooterView() {
mFooterView = mInflater.inflate(R.layout.refresh_footer, this, false);
mFooterImageView = (ImageView) mFooterView
.findViewById(R.id.pull_to_load_image);
mFooterTextView = (TextView) mFooterView
.findViewById(R.id.pull_to_load_text);
mFooterProgressBar = (ProgressBar) mFooterView
.findViewById(R.id.pull_to_load_progress);
// 底部布局
measureView(mFooterView);
mFooterViewHeight = mFooterView.getMeasuredHeight();
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
mFooterViewHeight);
/**
* int top = getHeight();
params.topMargin=getHeight();//在这里getHeight()==0,但在onInterceptTouchEvent()方法里getHeight()已经有值了,不再是0;
getHeight()什么时候会赋值,稍候再研究一下
由于是线性布局可以直接添加,只要AdapterView的高度是MATCH_PARENT,那么footer view就会被添加到最后,并隐藏
*/
addView(mFooterView, params);
}
看完以上代码你肯定会想就这么简单嘛!当然不是,细心的同学会发现两个函数都有调用 measureView()函数,它是干嘛的呢!下面就来看下这个函数,这个函数看起来代码和注释很多,这里的功能无非就是计算子控件在父控件中的大小。
private void measureView(View child) {
/**
* child.getLayoutParams();
*
* 返回 该视图的布局参数
*
* 此视图的父视图指定如何安排它的供应参数
*
*/
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
/**
* 用指定的 宽度和高度 创建一组新的布局参数
*
* @param width 宽度,或者 {@link #WRAP_CONTENT},
* {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in
* API Level 8),或一个固定大小的像素
* @param height 高度,或者 {@link #WRAP_CONTENT},
* {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in
* API Level 8), 或一个固定大小的像素
*/
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
/**
* 是否measureChildren困难的部分:搞清楚MeasureSpec传递给特定的子控件。这种方法计算出正确的MeasureSpec一个子视图中的一维(高度或宽度)。
* 目标是信息从我们MeasureSpec与子控件的的LayoutParams结合,以获得最佳的可能结果。例如,如果这个观点知道它的大小(因为它MeasureSpec有整整模式),
* 子控件在其的LayoutParams已经表示,它想成为的尺寸与父控件一样,父控件应让子控件布置给精确的尺寸。
* @param spec 该视图的要求
* @param padding 该视图为当前维的填充和利润(如果适用)
*
* @param childDimension 希望为子控件设置的尺寸
* @return MeasureSpec 一个MeasureSpec整数为孩子
*
*/
int childWidthSpec=ViewGroup.getChildMeasureSpec(0,0+0,p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
/**
*
创建基于所提供的大小和模式的量度规范。该模式必须是下列之一:
UNSPECIFIED
EXACTLY
AT_MOST
* @param size 该措施说明书的大小
* @param mode 该措施规范的模式
* @return 基于规模和模式的措施规范
*/
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED);
}
/**
* 这就是所谓的大一个视图应该如何。父控件 约束信息的宽度和高度参数。
一个视图的实际测量工作是在onMeasure(int,int),称为该方法。因此,只有onMeasure(int,int)可以而且必须由子类重写。
@param widthMeasureSpec 横向空间的需求添加到的父控件大小
@param heightMeasureSpec 垂直间距需求添加到的父控件大小
*/
child.measure(childWidthSpec, childHeightSpec);
}
3、知道了 下拉头,上拉脚 怎么实现了,接下来就看在哪里加入到 PullToRefreshLayout控件中,又是如何实现动画的。请看下面代码。
(1)、这里动画实现的是刷新箭头的方向旋转,最后一行 addHeaderView() 实现了头部的添加。
/**
* 初始化动画
*/
private void initAnim() {
//加载所有的动画,我们需要的代码,而不是通过 XML
mFlipAnimation = new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF,
0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
//设置动画 均速
mFlipAnimation.setInterpolator(new LinearInterpolator());
/**
* 动画应该持续多久,持续时间不能为负
*
@param durationMillis
* @throws java.lang.IllegalArgumentException 如果 durationMillis < 0
* @attr 参考 R.styleable #Animation_duration
*/
mFlipAnimation.setDuration(250);
/**
* 如果 fillafter 是 true ,这个动画进行改造将坚持当它完成。
* 默认为 false ,如果不设置。
*
*请注意,这适用于个别动画,当使用 {@link android.view.animation.AnimationSet AnimationSet} 链动画
*
* @param fillAfter 如果动画结束后,动画应该应用它的转换
* @attr ref android.R.styleable#Animation_fillAfter
*
* @see #setFillEnabled(boolean)
*/
mFlipAnimation.setFillAfter(true);
/**
*构造函数使用时建立一个rotateanimation 对象
*
*
* @param fromDegrees 在动画开始时应用旋转偏移。
*
* @param toDegrees 在动画结束时应用旋转偏移。
*
* @param pivotXType 指定如何pivotxvalue应解释。什么之中的一个
* Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
* Animation.RELATIVE_TO_PARENT.
*
* @param pivotXValue X坐标的对象被旋转的点,指定一个绝对数量,0是左边缘。这个值可以是绝对数如果pivotxtype是绝对的,或一个百分比(1是100%)否则。
*
*
* @param pivotYType 指定如何pivotyvalue应解释。什么之中的一个
* Animation.ABSOLUTE, Animation.RELATIVE_TO_SELF, or
* Animation.RELATIVE_TO_PARENT.
*
* @param pivotYValue X坐标的对象被旋转的点,指定一个绝对数量,0是左边缘。这个值可以是绝对数如果pivotxtype是绝对的,或一个百分比(1是100%)否则。
*/
mReverseFlipAnimation = new RotateAnimation(-180, 0,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
//设置此动画的加速曲线。默认为线性插值。 这里是匀速
mReverseFlipAnimation.setInterpolator(new LinearInterpolator());
mReverseFlipAnimation.setDuration(250);
mReverseFlipAnimation.setFillAfter(true);
mInflater = LayoutInflater.from(getContext());
// header view 在此添加,保证是第一个添加到linearlayout的最上端
addHeaderView();
}
(2)、知道了头部如何加入PullToRefreshLayout中,那么底部是如何添加的呢!其实底部的加入是有技巧的,接下来请看代码。onFihishInflate() 看到@Override 你就知道这个函数是 LinearLayout 提供,那么它有何作用呢,它的作用就是在所有的XML和头部布局都添加了的情况下加入 脚部布局。
/**
*
完成 填充 XML格式的视图。这就是所谓的 UI填充 的最后阶段,所有子视图已被添加之后。
即使子类覆盖onFinishInflate,他们应始终确保调用超级方法,使我们得到调用。 既必须调用 super.onFinishInflate();
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// footer view 在此添加保证添加到linearlayout中的最后
addFooterView();
initContentAdapterView();
}
(3)、在上面的代码中你会看到 initContentAdapterView() 这个函数,你会想它又是什么鬼,它有什么作用呢?请看代码。
如果你有了解过,我的上一篇博客:http://blog.csdn.net/cscfas/article/details/51330505 ;那么你就知道在自定义控件中,如果XML布局中引入了控件,会加载该自定义控件的第二个构造函数,那么addHeaderView() 会被加载到布局中,PullToRefreshLayout 在xml 中加入的布局也会被添加到控件中。该布局可以包裹 ListView 和 GridView 及 ScrollView 控件。
/**
*
* 初始化 adapterview像ListView,GridView等;或init ScrollView
*/
private void initContentAdapterView(){
int count=getChildCount();
if (count<3)
throw new IllegalArgumentException(
"this layout must contain 3 child views,and AdapterView or ScrollView must in the second position!");
View view=null;
for (int i=0;i<count-1;++i){
view=getChildAt(i);
if (view instanceof AdapterView<?>){
System.out.println("the type is AdapterView");
mAdapterView=(AdapterView<?>)view;
}
if (view instanceof ScrollView){
System.out.println("thie type is ScrollView");
mScrollView= (ScrollView) view;
}
}
if (mAdapterView==null&&mScrollView==null){
throw new IllegalArgumentException(
"must contain a AdapterView or ScrollView in this layout!");
}
}
4、接下来看下项目中用到的常量和变量注释,这对阅读后续代码有帮助。
/**
* 下拉刷新
*/
private static final int PULL_TO_REFRESH = 2;
/**
* 释放刷新
*/
private static final int RELEASE_TO_REFRESH = 3;
/**
* 刷新
*/
private static final int REFRESHING = 4;
/**
* 上拉加载
*/
private static final int PULL_UP_STATE = 10;
/**
* 下拉刷新
*/
private static final int PULL_DOWN_STATE = 11;
/**
* 最后Y轴距离
*/
private int mLastMotionY;
/**
* 锁定
*/
private boolean mLock;
5、了解了布局如何实现,接下来就到了手势如何实现,也就是我们下拉为什么可以拉出 下拉头,这里涉及到手势相关的概念,如果不了解手可以参考博客:http://blog.csdn.net/cscfas/article/details/51372342
这里就不讲事件是如何拦截,如何分发的了,我们重点来看如下代码,这里在 ACTION_DOWN时并没有拦截事件只是记录下了 Y轴坐标,为什么呢?因为PullToRefreshLayout 是属于 ViewGroup 容器型的控件,如果ACTION_DOWN 直接被拦截了那么 ListVeiw 和 GridView 中的 item点击事件及 ScrollView中点击事件和长按事件将无法触发。
/**
* 事件拦截
* @param ev
* @return
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int y = (int) ev.getRawY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: //手指按下时记录 Y轴坐标
// 首先拦截down事件,记录y坐标
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE: //滑动时 拿到移动距离 判断是否拦截手势
// deltaY > 0 是向下运动,< 0是向上运动
int deltaY = y - mLastMotionY;
if (isRefreshViewScroll(deltaY)) {
// System.out.println("正在移动:返回true");
return true;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
break;
}
return false;
}
细心的同学会发现在 ACTION_MOVE 中有调用 isRefreshViewScroll() 函数,那么它又有什么功能呢!仔细看代码会发现它返回了一个 boolean 类型的值,是它控制这事件是否拦截,看到这里你是不是觉得它至关重要,那么就来分析一下它的结构吧!
mAdapterView 这个控件从何而来,有认真看过上面代码你就应该知道了。那么它是何方神圣呢?它就是 适配器填充控件后得到的结果,AdapterView 是适配器和控件的组合,这里主要是拿到AdapterView中的子控件,也就是ListView或 GridView中的Item,通过获取子控件的状态来动态设置 是否要拦截手势,以及设置 mPullState 状态。
mScrollView 控件也是同理,拿到子控件的状态来判断是否要拦截事件。具体代码都有注释请看代码,这里就不详解了。
/**
* 是否应该到了父View,即PullToRefreshView滑动
*
* @param deltaY
* , deltaY > 0 是向下运动,< 0是向上运动
* @return
*/
private boolean isRefreshViewScroll(int deltaY) {
// 当头部状态是 刷新 或 底部状态是刷新时 返回 false 不拦截
if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
return false;
}
//对于ListView和GridView
if (mAdapterView != null) {
// 子view(ListView or GridView)滑动到最顶端
if (deltaY > 0) {
View child = mAdapterView.getChildAt(0);
if (child == null) {
//设置状态为下拉刷新
mPullState = PULL_DOWN_STATE;
//设置状态为拦截
return true;
}
// 适配中 第一个控件高度为 0 且 第一个控件可见
if (mAdapterView.getFirstVisiblePosition() == 0
&& child.getTop() == 0) {
//设置状态为下拉刷新
mPullState = PULL_DOWN_STATE;
return true;
}
int top = child.getTop();
int padding = mAdapterView.getPaddingTop();
if (mAdapterView.getFirstVisiblePosition() == 0
&& Math.abs(top - padding) <= 8) {//这里之前用3可以判断,但现在不行,还没找到原因
mPullState = PULL_DOWN_STATE;
return true;
}
} else if (deltaY < 0) { //如果移动的距离为 负值
//获取适配中最后一个控件
View lastChild = mAdapterView.getChildAt(mAdapterView
.getChildCount() - 1);
if (lastChild == null) {
mPullState = PULL_UP_STATE;
// 如果mAdapterView中没有数据,不拦截
return true;
}
// 最后一个子view的Bottom小于父View的高度说明mAdapterView的数据没有填满父view,
// 等于父View的高度说明mAdapterView已经滑动到最后
if (lastChild.getBottom() <= getHeight()
&& mAdapterView.getLastVisiblePosition() == mAdapterView
.getCount() - 1) {
mPullState = PULL_UP_STATE;
return true;
}
}
}
// 对于ScrollView
if (mScrollView != null) {
// 子scroll view滑动到最顶端
View child = mScrollView.getChildAt(0);
//当移动距离为 正值 且滚动条没有滚动
if (deltaY > 0 && mScrollView.getScrollY() == 0) {
mPullState = PULL_DOWN_STATE; //设置状态为下拉刷新
return true;
} else if (deltaY < 0
&& child.getMeasuredHeight() <= getHeight()
+ mScrollView.getScrollY()) {
mPullState = PULL_UP_STATE; //设置为上拉加载
return true;
}
}
return false;
}
6、接下来就要见证奇迹了,就是具体如何实现 下拉刷新上拉加载更多效果的业务了,还记得上面我们有讲到手势拦截吧!如果你了解手势就知道被拦截后会执行什么函数,那就是 onTouchEvent() 函数了。
(1)、首先看下 ACTION_MOVE 这里我们来计算用户手指在屏幕上的滑动距离,还记得在 onInterceptTounchEvent()中已经对 mPullState 状态做过改变,这里开始就通过判断当前状态是下拉还是上拉来处理 HeaderView 和 FootView的显示及动画效果。
/*
* 如果在onInterceptTouchEvent()方法中没有拦截(即onInterceptTouchEvent()方法中 return false)
*
* 则由PullToRefreshView 的子View来处理;否则由下面的方法来处理(即由PullToRefreshView自己来处理)
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mLock) { //当处于锁定状态时
return true;
}
//拿到Y轴坐标
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: //手指按下时触发 ACTION_DOWN
// onInterceptTouchEvent已经记录
// mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE: //手指在屏幕上滑动时触发 ACTION_MOVE
//拿到用户滑动的距离
int deltaY = y - mLastMotionY;
if (mPullState == PULL_DOWN_STATE) { //如果当前状态处于下拉刷新 PULL_DOWN_STATE 那么执行 headerPrepareToRefresh() 函数实现刷新效果
// PullToRefreshView执行下拉
Log.i(TAG, " pull down!parent view move!");
headerPrepareToRefresh(deltaY);
// setHeaderPadding(-mHeaderViewHeight);
} else if (mPullState == PULL_UP_STATE) { //如果当前状态处于上拉加载 PULL_UP_STATE
if (pullUpLoad) { //判断用户是否启用上拉加载
// PullToRefreshView执行上拉
Log.i(TAG, "pull up!parent view move!");
footerPrepareToRefresh(deltaY);
}
}
mLastMotionY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: //当事件被取消时
//获取当前header view 的topMargin 值
int topMargin = getHeaderTopMargin();
if (mPullState == PULL_DOWN_STATE) { //如果当前状态是下拉刷新
if (topMargin >= 0) {
// 开始刷新
headerRefreshing();
} else {
// 还没有执行刷新,重新隐藏
setHeaderTopMargin(-mHeaderViewHeight);
}
} else if (mPullState == PULL_UP_STATE) { //如果当前状态处于上拉加载
if (pullUpLoad) {
if (Math.abs(topMargin) >= mHeaderViewHeight
+ mFooterViewHeight) {
// 开始执行footer 刷新
footerRefreshing();
} else {
// 还没有执行刷新,重新隐藏
setHeaderTopMargin(-mHeaderViewHeight);
}
}
}
break;
}
return super.onTouchEvent(event);
}
(2)、处理下拉或上拉布局被拉出效果,接下来看 headPrepareToRefresh() 和 footerPrepareToRefresh() 这两个函数实现了上拉及下拉效果 ,这里要注意 mHeaderState、mFooterState 的状态改变,它决定这是否释放刷新
/**
* header 准备刷新,手指移动过程,还没有释放
*
* @param deltaY
* ,手指滑动的距离
*/
private void headerPrepareToRefresh(int deltaY) {
int newTopMargin = changingHeaderViewTopMargin(deltaY);
// 当header view的topMargin>=0时,说明已经完全显示出来了,修改header view 的提示状态
if (newTopMargin >= 0 && mHeaderState != RELEASE_TO_REFRESH) {
mHeaderTextView.setText(R.string.pull_to_refresh_release_label);
mHeaderUpdateTextView.setVisibility(View.VISIBLE);
mHeaderImageView.clearAnimation();
mHeaderImageView.startAnimation(mFlipAnimation);
//改变状态为释放刷新
mHeaderState = RELEASE_TO_REFRESH;
} else if (newTopMargin < 0 && newTopMargin > -mHeaderViewHeight) {// 拖动时没有释放
mHeaderImageView.clearAnimation();
mHeaderImageView.startAnimation(mFlipAnimation);
mHeaderTextView.setText(R.string.pull_to_refresh_pull_label);
mHeaderState = PULL_TO_REFRESH;
}
}
/**
* footer 准备刷新,手指移动过程,还没有释放 移动footer view高度同样和移动header view
* 高度是一样,都是通过修改header view的topmargin的值来达到
*
* @param deltaY
* ,手指滑动的距离
*/
private void footerPrepareToRefresh(int deltaY) {
int newTopMargin = changingHeaderViewTopMargin(deltaY);
// 如果header view topMargin 的绝对值大于或等于header + footer 的高度
// 说明footer view 完全显示出来了,修改footer view 的提示状态
if (Math.abs(newTopMargin) >= (mHeaderViewHeight + mFooterViewHeight)
&& mFooterState != RELEASE_TO_REFRESH) {
mFooterTextView
.setText(R.string.pull_to_refresh_footer_release_label);
mFooterImageView.clearAnimation();
mFooterImageView.startAnimation(mFlipAnimation);
mFooterState = RELEASE_TO_REFRESH;
} else if (Math.abs(newTopMargin) < (mHeaderViewHeight + mFooterViewHeight)) {
mFooterImageView.clearAnimation();
mFooterImageView.startAnimation(mFlipAnimation);
mFooterTextView.setText(R.string.pull_to_refresh_footer_pull_label);
mFooterState = PULL_TO_REFRESH;
}
}
(3)、仔细阅读上面代码,会发现所有的判断跟随着这个 headerPrepareToRefresh() 函数的返回值决定,接下来看下这个函数。判断当前 mPullState 状态 及 拉动距离是否大于设置距离,动态返回 TopMargin 及拉出的距离
/**
* 修改Header view top margin的值
*
* @description
* @param deltaY
*/
private int changingHeaderViewTopMargin(int deltaY) {
LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();
float newTopMargin = params.topMargin + deltaY * 0.4f;
//这里对上拉做一下限制,因为当前上拉后然后不释放手指直接下拉,会把下拉刷新给触发了
//表示如果是在上拉后一段距离,然后直接下拉
if(deltaY>0&&mPullState == PULL_UP_STATE&&Math.abs(params.topMargin) <= mHeaderViewHeight){
return params.topMargin;
}
//同样地,对下拉做一下限制,避免出现跟上拉操作时一样的bug
if(deltaY<0&&mPullState == PULL_DOWN_STATE&&Math.abs(params.topMargin)>=mHeaderViewHeight){
return params.topMargin;
}
params.topMargin = (int) newTopMargin;
mHeaderView.setLayoutParams(params);
/**
* 无效整个视图。如果视图是可见的,
*
* {@link #onDraw(android.graphics.Canvas)} 将在某个时候被调用
*
* 这必须从UI线程调用。从非UI线程,致电致电
*
* {@link #postInvalidate()}.
*/
invalidate();
return params.topMargin;
}
(4)、ACTION_UP、ACTION_CANCEL 处理释放刷新和取消执行刷新,首先拿到 topMargin 既拉动的距离,通过判断拉动距离和 mPullState 状态来决定是释放刷新还是取消执行刷新。
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: //当事件被取消时
//获取当前header view 的topMargin 值
int topMargin = getHeaderTopMargin();
if (mPullState == PULL_DOWN_STATE) { //如果当前状态是下拉刷新
if (topMargin >= 0) {
// 开始刷新
headerRefreshing();
} else {
// 还没有执行刷新,重新隐藏
setHeaderTopMargin(-mHeaderViewHeight);
}
} else if (mPullState == PULL_UP_STATE) { //如果当前状态处于上拉加载
if (pullUpLoad) {
if (Math.abs(topMargin) >= mHeaderViewHeight
+ mFooterViewHeight) {
// 开始执行footer 刷新
footerRefreshing();
} else {
// 还没有执行刷新,重新隐藏
setHeaderTopMargin(-mHeaderViewHeight);
}
}
}
break;
(5)、headerRefreshing() 、footerRefreshing() 释放刷新,这里将 Runnable 添加到UI线程中,延迟1500 毫秒达到,下拉头或上拉脚停顿效果,这里主要回调监听接口,该接口是调用 PullToRefreshLayout 控件的 Activity或FrangMent 中实现。
/**
* 下拉头释放刷新
*
*/
private void headerRefreshing() {
mHeaderState = REFRESHING;
setHeaderTopMargin(0);
mHeaderImageView.setVisibility(View.GONE);
mHeaderImageView.clearAnimation();
mHeaderImageView.setImageDrawable(null);
mHeaderProgressBar.setVisibility(View.VISIBLE);
mHeaderTextView.setText(R.string.pull_to_refresh_refreshing_label);
if (mOnHeaderRefreshListener != null) {
/**
* 使Runnable被添加到消息队列,经过规定的时间之后运行。
*
* 运行将运行在用户界面线程。既UI线程中
*/
this.postDelayed(new Runnable() {
@Override
public void run() {
mOnHeaderRefreshListener.onHeaderRefresh(PullToRefreshView.this);
}
}, 1500);
}
}
/**
* 底部释放刷新
*/
private void footerRefreshing() {
mFooterState = REFRESHING;
int top = mHeaderViewHeight + mFooterViewHeight;
setHeaderTopMargin(-top);
mFooterImageView.setVisibility(View.GONE);
mFooterImageView.clearAnimation();
mFooterImageView.setImageDrawable(null);
mFooterProgressBar.setVisibility(View.VISIBLE);
mFooterTextView
.setText(R.string.pull_to_refresh_footer_refreshing_label);
if (mOnFooterRefreshListener != null) {
this.postDelayed(new Runnable() {
@Override
public void run() {
mOnFooterRefreshListener.onFooterRefresh(PullToRefreshView.this);
}
}, 1500);
}
}
(5)、注意在刷新失败的时候会执行 setHeaderMargin() 该函数作用主要是实现布局的隐藏
/**
* 设置header view 的topMargin的值
*
* @description
* @param topMargin
* ,为0时,说明header view 刚好完全显示出来; 为-mHeaderViewHeight时,说明完全隐藏了
*/
private void setHeaderTopMargin(int topMargin) {
LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();
params.topMargin = topMargin;
mHeaderView.setLayoutParams(params);
invalidate();
}
7、以上步骤基本实现了整个下拉刷新,上拉加载的功能,但是美中不足,刷新完成后我们还需要隐藏我们的布局,下面的代码是更新完后恢复初始化状态
/**
* header view 完成更新后恢复初始状态
*
* @description hylin 2012-7-31上午11:54:23
*/
public void onHeaderRefreshComplete() {
setHeaderTopMargin(-mHeaderViewHeight);
mHeaderImageView.setVisibility(View.VISIBLE);
mHeaderImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow);
mHeaderTextView.setText(R.string.pull_to_refresh_pull_label);
mHeaderProgressBar.setVisibility(View.GONE);
mHeaderState = PULL_TO_REFRESH;
}
/**
* footer view 完成更新后恢复初始状态
*/
public void onFooterRefreshComplete() {
setHeaderTopMargin(-mHeaderViewHeight);
mFooterImageView.setVisibility(View.VISIBLE);
mFooterImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow_up);
mFooterTextView.setText(R.string.pull_to_refresh_footer_pull_label);
mFooterProgressBar.setVisibility(View.GONE);
mFooterState = PULL_TO_REFRESH;
}
8、以上基本实现了下拉刷新上拉加载,博客也写累了,剩余的功能我就不贴代码了,可以参看Demo
下载地址:http://download.csdn.net/detail/cscfas/9524306