ListView下拉刷新的实现

来源:http://blog.csdn.net/guolin_blog/article/details/9255575

自定义布局类:RefreshableView

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

/**
 * 带下拉刷新的布局
 */
public class RefreshableView extends LinearLayout implements View.OnTouchListener {

    /**
     * 正在下拉中的状态
     */
    public static final int STATUS_PULL_TO_REFRESH = 0;

    /**
     * 释放立即刷新状态
     */
    public static final int STATUS_RELEASE_TO_REFRESH = 1;

    /**
     * 正在刷新状态
     */
    public static final int STATUS_REFRESHING = 2;

    /**
     * 刷新完成或未刷新状态
     */
    public static final int STATUS_REFRESH_FINISHED = 3;

    /**
     * 下拉头部回滚的速度
     */
    public static final int SCROLL_SPEED = -20;

    /**
     * 一分钟的毫秒值,用于判断上次的更新时间
     */
    public static final long ONE_MINUTE = 60 * 1000;

    /**
     * 一小时的毫秒值,用于判断上次的更新时间
     */
    public static final long ONE_HOUR = 60 * ONE_MINUTE;

    /**
     * 一天的毫秒值,用于判断上次的更新时间
     */
    public static final long ONE_DAY = 24 * ONE_HOUR;

    /**
     * 一月的毫秒值,用于判断上次的更新时间
     */
    public static final long ONE_MONTH = 30 * ONE_DAY;

    /**
     * 一年的毫秒值,用于判断上次的更新时间
     */
    public static final long ONE_YEAR = 12 * ONE_MONTH;

    /**
     * 上次更新时间的字符串常量,用于作为SharedPreferences的键值
     * 由UPDATED_AT + mId组成为key
     */
    private static final String UPDATED_AT = "updated_at";

    /**
     * 避免同一个文件出现一样的key而覆盖掉了其他下拉刷新界面的时间
     * 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突(),使用id来做区分
     */
    private int mId = -1;

    /**
     * 下拉刷新的回调接口
     */
    private PullToRefreshListener mListener;

    /**
     * 用于存储上次更新时间,这里文件名定义的:updateTime,记录上一次时间的key=updateTa
     */
    private SharedPreferences preferences;

    /**
     * 下拉头的View
     */
    private View header;

    /**
     * 需要带下拉刷新的ListView
     */
    private ListView listView;

    /**
     * 刷新时显示的进度条
     */
    private ProgressBar progressBar;

    /**
     * 指示下拉和释放的箭头
     */
    private ImageView arrow;

    /**
     * 指示下拉和释放的文字描述
     */
    private TextView tv_description;

    /**
     * 上次更新时间的文字描述
     */
    private TextView tv_updateAt;

    /**
     * 下拉头的布局参数
     */
    private MarginLayoutParams headerLayoutParams;

    /**
     * 上次更新时间的毫秒值
     */
    private long lastUpdateTime;

    /**
     * 下拉头的高度,负数
     */
    private int hideHeaderHeight;

    /**
     * 当前处理什么状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH,
     * STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
     */
    private int currentStatus = STATUS_REFRESH_FINISHED;

    /**
     * 记录上一次的状态是什么,避免进行重复操作
     */
    private int lastStatus = currentStatus;

    /**
     * 手指按下时的屏幕纵坐标
     */
    private float yDown;

    /**
     * 在被判定为滚动之前用户手指可以移动的最大值。这里为16
     * 表示滑动的时候,手的移动距离要大于这个值才能开始移动控件,否则就不触发该控件移动
     */
    private int touchSlop;

    /**
     * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
     */
    private boolean loadOnce=false;

    /**
     * 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉
     */
    private boolean ableToPull;

    /**
     * 当下拉的距离大于或者等于这个数就更新
     */
    private int scrollY;

    /**
     * 在xml布局文件中引用该类所调用的构造方法
     */
    public RefreshableView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //初始化控件
        header=LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true);
        arrow= (ImageView) header.findViewById(R.id.arrow);
        progressBar= (ProgressBar) header.findViewById(R.id.progressBar);
        tv_description= (TextView) header.findViewById(R.id.description);
        tv_updateAt= (TextView) header.findViewById(R.id.updated_time);
        preferences=context.getSharedPreferences("updateTime",Context.MODE_PRIVATE);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        Log.i("tag","移动的最大值:"+touchSlop);//16
        //初始化下拉头布局中 距离上次刷新时间的值
        refreshTextViewUpdateAtValue();
        setOrientation(VERTICAL);//设置该布局方向为垂直方向
        addView(header,0);//添加下拉头布局到该布局中,0代表第一个子控件
    }

    /**
     * 确定该布局中子视图的位置,只让该方法执行一次
     * 由于下拉头在构造函数中添加到该布局中了,这里需要修改位置
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if(changed && !loadOnce){
            headerLayoutParams= (MarginLayoutParams) header.getLayoutParams();
            hideHeaderHeight= -header.getHeight();//布局中的高度取反,设置下拉头距离屏幕的高度
            scrollY=hideHeaderHeight-hideHeaderHeight/2;//只有滑动值大于这个高度就认为可以刷新
            Log.i("tag","滑动值:"+scrollY+"本来的:"+hideHeaderHeight);//-110,-220
            //这样就把下拉的头布局放到了屏幕外上面
            headerLayoutParams.topMargin=hideHeaderHeight;
            listView = (ListView) getChildAt(1);
            listView.setOnTouchListener(this);//为listView设置触摸监听
            loadOnce=true;//已经加载了
        }
    }

    /**
     * 给下拉刷新控件注册一个监听器。
     *
     * @param listener
     *            监听器的实现。
     * @param id
     *            为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id。
     */
    public void setOnRefreshListener(PullToRefreshListener listener, int id) {
        mListener = listener;
        mId = id;
    }

    /**
     * listView的触摸监听
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        setIsAbleToPull(event);//设置ableToPull的值,可不可以下拉
        if (ableToPull){//可以显示出下拉头的布局
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN://记录按下的值
                    yDown=event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE://处理当前状态的逻辑
                    float yMove=event.getRawY();
                    int distance=(int) (yMove-yDown);
                    if(distance <= 0 ){
                        //Log.i("tag","当前为向上滑:"+distance);
                        return false;//不需要显示下拉头,所以return,不执行下面的下拉头逻辑操作,如果返回true,ListView会滚动不了
                    }
                    if(distance < touchSlop){
                        return false;//滑动距离太小也不执行下面对下拉头的操作
                    }
                    if(currentStatus != STATUS_REFRESHING){//不是正在刷新的状态
                        if(headerLayoutParams.topMargin >= scrollY){
                            //显示了下拉头,设置当前状态为释放立即刷新
                            currentStatus=STATUS_RELEASE_TO_REFRESH;
                        }else{
                            currentStatus = STATUS_PULL_TO_REFRESH;//下拉开始显示出来的状态
                        }
                        //通过偏移下拉头的topMargin值,来实现下拉效果
                        int margin=(distance / 2) + hideHeaderHeight;
                        if(margin>0){
                            margin=0;//避免下拉的高度过大
                        }
                        headerLayoutParams.topMargin = margin;
                        header.setLayoutParams(headerLayoutParams);

                    }
                    break;
                case MotionEvent.ACTION_UP://松手后根据当前状态执行对应的操作
                    if(currentStatus == STATUS_RELEASE_TO_REFRESH){
                        currentStatus=STATUS_REFRESHING;//正在刷新
                        //松手,开始执行释放立即刷新的任务
                        new UpdateTask().execute();
                    }else if(currentStatus == STATUS_PULL_TO_REFRESH){
                        //正在下拉的状态,松手后需要隐藏下拉头的显示
                        new HideHeaderTask().execute();
                    }
                    break;
            }
            //改变下拉头布局中的控件显示信息
            updateHeaderView();
            if(currentStatus == STATUS_PULL_TO_REFRESH || currentStatus== STATUS_REFRESHING){
                // 当前正处于下拉或释放状态,要让ListView失去焦点,屏蔽掉单击事件,不然松手会触发listView的单击事件
                listView.setPressed(false);//不可按压
                listView.setFocusable(false);
                listView.setFocusableInTouchMode(false);
                lastStatus = currentStatus;
                // 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件
                return true;
            }
        }
        return false;
    }

    /**
     * 更新下拉头中的显示信息。
     */
    private void updateHeaderView() {
        if (lastStatus != currentStatus) {
            if (currentStatus == STATUS_PULL_TO_REFRESH) {
                //正在下拉
                tv_description.setText("下拉可以刷新");
                arrow.setVisibility(View.VISIBLE);
                progressBar.setVisibility(View.GONE);
                rotateArrow();//执行动画
            } else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
                tv_description.setText("释放立即刷新");
                arrow.setVisibility(View.VISIBLE);
                progressBar.setVisibility(View.GONE);
                rotateArrow();
            } else if (currentStatus == STATUS_REFRESHING) {
                tv_description.setText("正在刷新...");
                progressBar.setVisibility(View.VISIBLE);
                arrow.clearAnimation();
                arrow.setVisibility(View.GONE);
            }
            refreshTextViewUpdateAtValue();//更新时间
        }else{
            //Log.i("tag","跟记录的上一次状态相同,"+currentStatus);
        }
    }

    /**
     * 根据当前的状态来旋转箭头。
     */
    private void rotateArrow() {
        float pivotX = arrow.getWidth() / 2f;
        float pivotY = arrow.getHeight() / 2f;
        float fromDegrees = 0f;
        float toDegrees = 0f;
        if (currentStatus == STATUS_PULL_TO_REFRESH) {
            fromDegrees = 180f;
            toDegrees = 360f;//正在下拉.360到180度旋转,逆时针180度
        } else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {
            fromDegrees = 0f;
            toDegrees = 180f;//释放立即刷新,从180到0度,顺时针180度
        }
        RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
        animation.setDuration(100);
        animation.setFillAfter(true);//保持为运行后的状态
        arrow.startAnimation(animation);
    }

    /**
     * 根据当前ListView的滚动状态来设定 {@link #ableToPull}的值,
     * 每次都需要在onTouch方法中第一个执行,这样可以判断出当前下拉头可不可以进行下拉。
     */
    private void setIsAbleToPull(MotionEvent event) {
        View firstChild = listView.getChildAt(0);
        if(firstChild != null){//存在子条目
            int firstVisiblePosition = listView.getFirstVisiblePosition();//当前屏幕显示的第一个item的position值
            if(firstVisiblePosition==0 && firstChild.getTop()==0){
                if(! ableToPull){
                    yDown=event.getRawY();//当前为不可下拉状态才获取按下的纵坐标
                }
                //如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新
                ableToPull=true;
            }else{
                if(headerLayoutParams.topMargin != hideHeaderHeight){
                    //说明下拉头已经被拉出,需要恢复成原样隐藏了
                    headerLayoutParams.topMargin=hideHeaderHeight;
                    header.setLayoutParams(headerLayoutParams);
                }
                ableToPull=false;
            }
        }else{
            //如果ListView中没有元素,也应该允许下拉刷新
            ableToPull = true;
        }
    }

    /**
     * 刷新下拉头布局中距离上次更新时间的值
     */
    private void refreshTextViewUpdateAtValue() {
        lastUpdateTime=preferences.getLong(UPDATED_AT + mId,-1);//获取上次的刷新时间
        long currentTime = System.currentTimeMillis();
        long timePassed = currentTime - lastUpdateTime;//相差的时间
        long timeIntoFormat;//格式化后的时间
        String updateAtValue;
        if(lastUpdateTime==-1){
            updateAtValue="暂未更新过";
        }else if(timePassed < 0){
            updateAtValue="时间有问题";
        }else if(timePassed < ONE_MINUTE){//小于一分钟的毫秒数
            updateAtValue="刚刚刷新";
        }else if(timePassed < ONE_HOUR){//大于一分钟,小于一小时
            timeIntoFormat=timePassed / ONE_MINUTE;//得到分钟数
            updateAtValue="上次更新于"+ timeIntoFormat +"分钟前";
        }else if(timePassed < ONE_DAY){//大于一小时,小于一天
            timeIntoFormat=timePassed / ONE_HOUR;//得到小时数
            updateAtValue="上次更新于"+ timeIntoFormat +"小时前";
        }else if(timePassed < ONE_MONTH){//大于一天,小于一个月
            timeIntoFormat=timePassed / ONE_DAY;//得到天数
            updateAtValue="上次更新于"+ timeIntoFormat +"天前";
        }else if(timePassed < ONE_YEAR){//大于一个月,小于一年
            timeIntoFormat=timePassed / ONE_MONTH;//得到月数
            updateAtValue="上次更新于"+ timeIntoFormat +"个月前";
        }else{//大于一年
            timeIntoFormat=timePassed / ONE_YEAR;//得到年数
            updateAtValue="上次更新于"+ timeIntoFormat +"年前";
        }
        tv_updateAt.setText(updateAtValue);//设置更新时间值
    }

    /**
     * 执行下拉头刷新的任务
     */
    class UpdateTask extends AsyncTask<Void,Integer,Void> {

        @Override
        protected Void doInBackground(Void... params) {
            int topMargin = headerLayoutParams.topMargin;
            if(topMargin>scrollY){
                topMargin=scrollY;
            }
            publishProgress(topMargin);//让下拉头与屏幕重合
            mySleep(2000);//模拟刷新2秒
            if (mListener != null) {
                mListener.onRefresh();//回调接口方法,刷新完成
            }
            currentStatus=STATUS_REFRESH_FINISHED;//刷新完成的状态
            preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();//记录这次的刷新时间
            //开始隐藏
            while(true){
                if(topMargin<= hideHeaderHeight){
                    break;
                }
                topMargin=topMargin + SCROLL_SPEED;//每次减20
                publishProgress(topMargin);//刷新位置
                mySleep(50);
            }
            lastStatus = STATUS_REFRESH_FINISHED;//刷新完成
            publishProgress(hideHeaderHeight);//刚好隐藏
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            headerLayoutParams.topMargin = values[0];
            header.setLayoutParams(headerLayoutParams);//更新下拉头的位置
        }

        private void mySleep(int time){
            try {
                Thread.currentThread().sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 隐藏下拉头的任务
     */
    class HideHeaderTask extends AsyncTask<Void,Integer,Void> {

        @Override
        protected Void doInBackground(Void... params) {
            int topMargin = headerLayoutParams.topMargin;
            while(true){
                if(topMargin<= hideHeaderHeight){
                    break;
                }
                topMargin=topMargin + SCROLL_SPEED;//每次减20
                publishProgress(topMargin);//刷新进度
                mySleep(50);
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            headerLayoutParams.topMargin = values[0];
            header.setLayoutParams(headerLayoutParams);//更新下拉头的位置
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            headerLayoutParams.topMargin = hideHeaderHeight;
            header.setLayoutParams(headerLayoutParams);//设置为刚好隐藏
            lastStatus = STATUS_REFRESH_FINISHED;//设置前一次状态为刷新完成或者未刷新状态
        }

        private void mySleep(int time){
            try {
                Thread.currentThread().sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 下拉刷新的接口回调
     */
    public interface PullToRefreshListener {
        /**
         * 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的,你可以不必另开线程来进行耗时操作。
         */
        void onRefresh();
    }
}

下拉刷新的下拉头布局文件:pull_to_refresh.xml

<?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="wrap_content"
    android:gravity="bottom|center_horizontal"
    android:background="#ffcccc">
<!--左边的图片-->
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:gravity="center|bottom"
        android:layout_marginBottom="10dp"
        android:layout_height="100dp">
        <ImageView
            android:id="@+id/arrow"
            android:src="@drawable/arrow"
            android:layout_width="40dp"
            android:layout_height="40dp" />
        <ProgressBar
            android:id="@+id/progressBar"
            android:visibility="gone"
            style="@android:style/Widget.ProgressBar.Inverse"
            android:layout_width="40dp"
            android:layout_height="40dp" />
    </LinearLayout>
<!--右边的文字-->
    <LinearLayout
        android:orientation="vertical"
        android:gravity="center|bottom"
        android:layout_marginBottom="10dp"
        android:layout_width="wrap_content"
        android:layout_height="100dp">
        <TextView
            android:id="@+id/description"
            android:text="更新描述信息"
            android:textSize="15sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:id="@+id/updated_time"
            android:text="更新时间"
            android:textSize="15sp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>

=============================使用方法:=========================================

布局文件activity_main.xml

<?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"
    tools:context="com.ts.listviewpulltorefresh.MainActivity">

    <com.ts.listviewpulltorefresh.RefreshableView
        android:id="@+id/pull_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </com.ts.listviewpulltorefresh.RefreshableView>
</RelativeLayout>

MainActivity.java

/**
 * LIstView下拉刷新
 */
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final RefreshableView pullRefreshView = (RefreshableView) findViewById(R.id.pull_refresh);
        ListView listView= (ListView) pullRefreshView.findViewById(R.id.listView);
        ArrayList<String> list=new ArrayList<String>();
        for (int i = 0; i < 15; i++) {
            list.add("item"+i);
        }
        ArrayAdapter<String> adapter=new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,list);
        //设置刷新完成的监听
        pullRefreshView.setOnRefreshListener(new RefreshableView.PullToRefreshListener() {
            @Override
            public void onRefresh() {
                Log.i("tag","刷新完成");
            }
        },1);//1为区分不同刷新界面的id
        //单击事件
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Log.i("tag","单击了ListView----"+position);
            }
        });
        listView.setAdapter(adapter);
    }
}

效果图:

 

posted @ 2016-09-04 12:39  ts-android  阅读(273)  评论(0编辑  收藏  举报