Android下拉刷新代码完全解析,完全解读大神代码,解决两个小bug

本文转载自郭霖的博客转载地址:http://blog.csdn.net/guolin_blog/article/details/9255575,感谢大神的分享!

本文旨在解读代码,让喜欢IT,不断拼搏的程序员同学们能更快的学会大神分享的代码,若有不足之处,请多指教,大神勿喷啊!

不知道从什么时候开始,下拉和侧滑是做App必备的两项装B利器,在没有使用官方原生的侧滑和下拉之前,那都是无数大神用代码构造的啊,看见别人写的第三方,自己也拿来用,用久了,感觉不给力,出bug了不知道如何下手修复,后来就打算去网上找源码看看别人怎么写的,Google搜索排名第一的郭大神的下拉代码就被找出来了,写的原理确实不错啊,逻辑思维也很清晰啊,自己使用的同时也推荐给身边的同事,不过好多同事觉得注释太少看不懂,而且看的也是半懂,小弟怀着为天下苍生谋福利的想法,就把大神的代码全部注释翻译了一遍.

中间发现两个小问题,一个就是下拉刷新后,箭头图片无法隐藏,不过也解决了,就是一句代码的事,另外一个就是listview方法哦fragment时,下拉箭头初始化不会自动隐藏,不过还是一句代码的事,废话不多说,直接上代码,全面解读郭大神的下拉刷新:

差不多每一行都有注释,对于刚进入自绘控件的同学很有帮助.

首先是下拉头的xml文件:其中的箭头自己找一个吧,图片就不展示了

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="60dp">
    <!--线性布局装载-->
<LinearLayout
    android:layout_width="200dip"
    android:layout_height="60dip"
    android:layout_centerInParent="true"
    android:orientation="horizontal" >

    <RelativeLayout
        android:layout_width="0dip"
        android:layout_height="60dip"
        android:layout_weight="3"
        >
        <!--下拉箭头,图片自己找吧-->
        <ImageView
            android:id="@+id/imagejiantou"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:background="@mipmap/pulltorefresh_down_arrow"
            />
        <!--正在更新时使用的ProgressBar-->
        <ProgressBar
            android:id="@+id/progress_bar"
            android:layout_width="30dip"
            android:layout_height="30dip"
            android:layout_centerInParent="true"
            android:visibility="gone"
            />
    </RelativeLayout>

    <LinearLayout
        android:layout_width="0dip"
        android:layout_height="60dip"
        android:layout_weight="12"
        android:orientation="vertical" >
        //下面这些不用解释咯
        <TextView
            android:id="@+id/description"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:gravity="center_horizontal|bottom"
            android:text="下拉可以刷新" />

        <TextView
            android:id="@+id/updated_at"
            android:layout_width="fill_parent"
            android:layout_height="0dip"
            android:layout_weight="1"
            android:gravity="center_horizontal|top"
            android:text="最后更新:暂未更新" />
    </LinearLayout>
</LinearLayout>
</RelativeLayout>

然后开始正题,下拉刷新的代码:

新建一个类继承LinearLayout:

public class PulltoRefresh extends LinearLayout implements View.OnTouchListener {
    private SharedPreferences preferences;
    //状态:正在刷新
    private int STATE_FRESHING = 0;
    //状态:下拉刷新
    private int STATE_PULL = 1;
    //状态:刷新完成
    private int STATE_FINISH = 2;
    //状态:释放刷新
    private int STATE_RELEASE = 3;
    //初始状态
    private int STATE_CURRENT = STATE_FINISH;
    //记录上次状态
    private int STATE_LAST = STATE_CURRENT;
    private View header;
    private ProgressBar progressBar;
    private ImageView imagejiantou;
    private TextView description;
    private TextView updateAt;
    //在被判定为滚动之前用户手指可以移动的最大值。下面有解释
    private int touchSlop;
    //只能画一次头,下面有解释
    private boolean loadOnce = false;
    //高度
    private int hideHeaderHeight;
    private ViewGroup.MarginLayoutParams headerLayoutParams;
    private ListView listview;
    //是否允许下拉
    private boolean ableToPull;
    //listview滚动到顶部的时候,手指点击的坐标
    private float yDown;
    //刷新监听
    private OnPullToRefreshListener onPullToRefreshListener;
    //区分监听的ID
    private int listenerId;

    public PulltoRefresh(Context context, AttributeSet attrs) {
        super(context, attrs);
        //这个不用说吧
        preferences = context.getSharedPreferences("VALUE", Context.MODE_PRIVATE);
        //需要添加的头
        header = LayoutInflater.from(context).inflate(R.layout.activity_pullrefresh, null);
        //旋转的bar控件
        progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
        //下拉的图片箭头
        imagejiantou = (ImageView) header.findViewById(R.id.imagejiantou);
        //描述下拉可以刷新的控件
        description = (TextView) header.findViewById(R.id.description);
        //更新时间
        updateAt = (TextView) header.findViewById(R.id.updated_at);
        //getScaledTouchSlop是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。
        //如果小于这个距离就不触发移动控件,如viewpager就是用这个距离来判断用户是否翻页
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        //refreshUpdatedAtValue();
        //设置方向
        setOrientation(VERTICAL);
        //将view添加到0的位置
        addView(header, 0);
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        //这里有很多疑问,查了很多资料,说一说本人的理解,
        //当自绘控件没有发生位置移动时,changed会为false,此时不会绘制
        //当自绘控件发生位置移动时,changed会为true,此时系统会调用该方法绘制
        //但是我们只需要绘制一次,如果每次都绘制,那么下拉就拉不下来
        //所以这里就在引入一个变量loadOnce,使其先为false,然后同时判断,让该方法第一次时开始绘制
        //绘制完的时候将loadOnce改为true,这样后面系统就不能再次绘制该控件
        if (changed && !loadOnce) {
            //控件的隐藏高度
            hideHeaderHeight = -header.getHeight();
            //得到MarginLayoutParams参数
            headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
            //设置顶部的margin
            headerLayoutParams.topMargin = hideHeaderHeight;
            //此处如果listview实在MainActivity里面,可以不用设置
            //如果是在fragment里面必须设置,笔者亲测...之前没有这句,后来加上后搞定
            header.setLayoutParams(headerLayoutParams);
            //得到listview,此处查看xml文件
            listview = (ListView) getChildAt(1);
            //设置listview的监听
            listview.setOnTouchListener(this);
            //改变变量,使系统只绘制一次
            loadOnce = true;
        }
    }


    //listview的监听
    public boolean onTouch(View v, MotionEvent event) {
        //判断能否下拉,首先判断这个,然后在进行其他操作
        setIsAbleToPull(event);
        //如果可以下拉
        if (ableToPull) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //记录点击时的起始位置
                    yDown = event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    //下拉的位置
                    float ymove = event.getRawY();
                    //下拉的距离
                    int absolute = (int) (ymove - yDown);
                    //如果下拉的距离小于等于0,是上拉,上拉的时候肯定不刷新呗
                    //并且header的topmargin没有超过hideHeaderHeight,说明根本就没有下拉出来
                    //如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
                    if (absolute <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {
                        return false;
                    }
                    //距离小于touchSlop,也应该false
                    if (absolute < touchSlop) {
                        return false;
                    }
                    //如果当前状况不是正在刷新中
                    if (STATE_CURRENT != STATE_FRESHING) {
                        //判断topMargin>0,是释放状态
                        if (headerLayoutParams.topMargin > 0) {
                            STATE_CURRENT = STATE_RELEASE;
                        } else {
                            //否则就是下拉状态
                            STATE_CURRENT = STATE_PULL;
                        }
                        // 通过偏移下拉头的topMargin值,来实现下拉效果
                        //因为absolute的距离很大,所以除以2,在来与hideHeaderHeight搭配
                        headerLayoutParams.topMargin = absolute / 2 + hideHeaderHeight;
                        header.setLayoutParams(headerLayoutParams);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                default:
                    //手指松开时,判断状态
                    //手指松开时如果是释放状态,立即调用刷新方法
                    if (STATE_CURRENT == STATE_RELEASE) {
                        //刷新任务
                        new PullRefreshing().execute();
                        //手指松开时如果是下拉状态,就调用下拉隐藏任务方法
                    } else if (STATE_CURRENT == STATE_PULL) {
                        // 隐藏下拉任务
                        new hideHeader().execute();
                    }
                    break;
            }
            //时刻记得更新下拉信息
            if (STATE_CURRENT == STATE_PULL || STATE_CURRENT == STATE_RELEASE) {
                //更新header任务
                updateHeader();
                // 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态
                listview.setPressed(false);
                listview.setFocusable(false);
                listview.setFocusableInTouchMode(false);
                STATE_LAST = STATE_CURRENT;
                //当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件
                return true;
            }

        }
        return false;
    }

    //刷新完成方法
    public void finishRefresh() {
        //记录状态
        STATE_CURRENT = STATE_FINISH;
        //将当前时间的毫秒数写入xml文件,方便下次更新是调用计算更新时间
        //将传入监听的ListenerId也一起写进去,方便后续判断是哪个listview的更新时间
        preferences.edit().putLong("TIME" + listenerId, System.currentTimeMillis()).commit();
        //隐藏下拉头
        new hideHeader().execute();
    }


    //判断能否下拉
    private void setIsAbleToPull(MotionEvent event) {
        //得到listview的第一个item
        View firstchild = listview.getChildAt(0);
        if (firstchild != null) {
            //得到listview当前屏幕最上方的索引
            int firstVisiblePosition = listview.getFirstVisiblePosition();
            if (firstVisiblePosition == 0 && firstchild.getTop() == 0) {

                if (!ableToPull) {
                    //记录坐标
                    yDown = event.getRawY();
                }
                // 如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新
                ableToPull = true;
            } else {
                //如果listview没有滚动到顶部,判断header的topmargin,超出自身高度后
                //设置回自身高度的margin
                if (headerLayoutParams.topMargin != hideHeaderHeight) {
                    headerLayoutParams.topMargin = hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);
                }
                //当然此时还是不能下拉
                ableToPull = false;
            }
        } else {
            //如果listview中没有内容,应该允许下拉
            ableToPull = true;
        }
    }

    //为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id
    public void setOnPullToRefreshListener(OnPullToRefreshListener onPullToRefreshListener, int id) {
        this.onPullToRefreshListener = onPullToRefreshListener;
        listenerId = id;
    }

    public interface OnPullToRefreshListener {
        void onPullToRefreshListener();
    }

    //隐藏header任务
    //隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏。
    class hideHeader extends AsyncTask<Void, Integer, Integer> {
        @Override
        protected Integer doInBackground(Void... params) {
            int topMargin = headerLayoutParams.topMargin;
            int speed = -20;
            while (true) {
                topMargin = topMargin + speed;
                if (topMargin <= hideHeaderHeight) {
                    topMargin = hideHeaderHeight;
                    break;
                }
                publishProgress(topMargin);
                SystemClock.sleep(10);
            }


            return topMargin;
        }


        @Override
        protected void onPostExecute(Integer integer) {
            super.onPostExecute(integer);
            headerLayoutParams.topMargin = integer;
            header.setLayoutParams(headerLayoutParams);
            STATE_CURRENT = STATE_FINISH;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            headerLayoutParams.topMargin = values[0];
            header.setLayoutParams(headerLayoutParams);
        }
    }

    //设置旋转箭头方法
    public void rotateImage() {
        //根据状态分别定义旋转的起始角度
        float fromDegrees = 0f;
        float toDegrees = 0f;
        if (STATE_CURRENT == STATE_PULL) {
            fromDegrees = 180f;
            toDegrees = 360f;
        } else if (STATE_CURRENT == STATE_RELEASE) {
            fromDegrees = 0f;
            toDegrees = 180f;
        }
        //此处有改动
        //旋转动画需要设置旋转的中心点,
        //Animation.RELATIVE_TO_SELF意思是相对于自己
        //两个0.5f,意思是相对于自己的x和y各一半,也就是控件自身的中心
        RotateAnimation rotateAnimation = new RotateAnimation(fromDegrees, toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        rotateAnimation.setDuration(100);
        rotateAnimation.setFillAfter(true);
        imagejiantou.startAnimation(rotateAnimation);
    }

    //设置更新头方法
    public void updateHeader() {
        if (STATE_LAST != STATE_CURRENT) {
            if (STATE_CURRENT == STATE_PULL) {
                description.setText("下拉刷新");
                imagejiantou.setVisibility(View.VISIBLE);
                progressBar.setVisibility(View.GONE);
                rotateImage();
            } else if (STATE_CURRENT == STATE_RELEASE) {
                description.setText("释放刷新");
                imagejiantou.setVisibility(View.VISIBLE);
                progressBar.setVisibility(View.GONE);
                rotateImage();
            } else if (STATE_CURRENT == STATE_FRESHING) {
                description.setText("正在刷新");
                //此处代码必须加上,之前使用时,发现箭头图片不会隐藏
                //后来才知道,图片设置过动画,而且设置了rotateAnimation.setFillAfter(true)方法后
                //该图片就不会被隐藏掉,
                //所以隐藏前必须先清除动画
                imagejiantou.clearAnimation();
                imagejiantou.setVisibility(View.GONE);
                progressBar.setVisibility(View.VISIBLE);
            }
            //更新时间
            //此处时间有争议,可以放到点击事件MotionEvent.ACTION_DOWN:
            //总之看个人喜好,建议放到构造方法或者onLayout方法里面去
            updateTime();
        }
    }

    //更新时间的方法
    public void updateTime() {
        //获得上次的更新时间
        long lastTime = preferences.getLong("TIME" + listenerId, -1);
        //得到当前的更新时间
        long currentTime = System.currentTimeMillis();
        //计算更新时间的间隔
        long updateSpace = currentTime - lastTime;
        //新建一个文本变量
        String timeText = null;
        //当上次时间等于默认值时,说明还没更新
        if (lastTime == -1) {
            timeText = "最后更新:暂未更新";
            //当时间间隔小于0时,显示更新时间错误
        } else if (updateSpace < 0) {
            timeText = "更新时间错误";
            //当时间间隔小于2分钟时,显示刚刚更新
        } else if (updateSpace < 60 * 1000 ) {
            timeText = "最后更新:刚刚更新";
            //当时间间隔小于1小时,显示最后更新:xx分钟前
        } else if (updateSpace < 60 * 1000 * 60) {
            long updateCount = updateSpace / (60 * 1000);
            timeText = "最后更新:" + updateCount + "分钟前";
            //当时间间隔小于24小时,显示最后更新:xx小时前
        } else if (updateSpace < 60 * 1000 * 60 * 24) {
            long updateCount = updateSpace / (60 * 1000 * 60);
            timeText = "最后更新:" + updateCount + "小时前";
        } else {
            //最后时间太长了,直接很久以前吧
            timeText = "最后更新:很久以前";
        }
        //最后设置给textView
        updateAt.setText(timeText);

    }

    //设置更新任务
    class PullRefreshing extends AsyncTask<Void, Integer, Void> {
        protected Void doInBackground(Void... params) {
            int topMargin = headerLayoutParams.topMargin;
            int speed = -20;
            while (true) {
                topMargin = topMargin + speed;
                if (topMargin <= 0) {
                    topMargin = 0;
                    break;
                }
                publishProgress(topMargin);
                SystemClock.sleep(10);
            }
            STATE_CURRENT = STATE_FRESHING;
            publishProgress(0);
            if (onPullToRefreshListener != null) {
                onPullToRefreshListener.onPullToRefreshListener();
            }
            return null;
        }
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            updateHeader();
            headerLayoutParams.topMargin = values[0];
            header.setLayoutParams(headerLayoutParams);
        }
    }
}


代码注释很详细吧,这里就不啰嗦咯!看下面使用!

先是Xml的引用,直接把listview放进去:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.pulltorefesh.MainActivity">
    <!--此处就是自绘的下拉刷新-->
    <com.example.pulltorefesh.PulltoRefresh
        android:id="@+id/pulltorefresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!--此处listview-->
        <ListView
            android:id="@+id/listview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:divider="#ff0000"
            android:dividerHeight="1px"
            android:entries="@array/city"></ListView>
    </com.example.pulltorefesh.PulltoRefresh>

</RelativeLayout>
然后是我们的MainActivity:

public class MainActivity extends Activity {

    private PulltoRefresh pulltorefresh;
    private ListView listView;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        listView = (ListView) findViewById(R.id.listview);
        pulltorefresh = (PulltoRefresh) findViewById(R.id.pulltorefresh);
        //别忘记设置监听
        pulltorefresh.setOnPullToRefreshListener(new PulltoRefresh.OnPullToRefreshListener() {
            @Override
            public void onPullToRefreshListener() {
                //在这里执行刷新任务
                //我这里就不执行刷新任务了,让系统睡两秒
                SystemClock.sleep(2000);
                //执行完后调用finish,如果是是异步或者子线程中刷新,记得传回主线程在调用finish
                pulltorefresh.finishRefresh();
        //监听ID别忘记了        
            }
        }, 0);
    }
}

代码解读完毕,注释很多吧,慢慢看吧!中间的下拉箭头图片不能隐藏的解决代码,可别忘记哦!

最后向大神致敬!!!!!



  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值