[view] 09_下拉刷新菜单-实现

Android 4.0

下拉刷新菜单-实现

主要技术点:1、使用SystemClock.sleep(time)代替Thread.sleep(time)睡眠
2、需要睡眠时,需要是要子线程,但界面UI更新需要在主线程中
runOnUiThread,Handler+Message,AsyncTask
3、测量一个组件的高度,使用ListView中private的measureItem,可以黏贴过来,或者反射
4、枚举的使用,限定在指定的几个范围值中
5、
SimpleDateFormat格式化系统时间
步骤:
实现下拉时候,显示头部并刷新
RefreshListView 中 
1、监听ListView的滚动setOnScrollListener  事件,用来记住第一个item的位置
2、监听ListView的触摸事件,实现onTouchEvent()方法
ACTION_DOWN://触摸按下
记住按下dowmY位置,初始化为-1,防止触摸不到,在移动的时候出错
ACTION_MOVE://触摸移动
a) 判断按下Y轴坐标downY是否为-1,如果是,说明触摸按下事件没有响应,将当前y轴坐标赋值给downY
b)记住移动的Y轴坐标位置moveY
通过paddingTop = (-height)+(moveY-downY)/2  //除以2,是为了防止拉的过长,head会拉到最下面来

如果paddingTop(此时为负数)>-height,说明刷新中
  如果paddingTop大于0时,那么该头就全部出来了,iv_arrow执行向上动画,
   paddingTop一小于0,就执行箭头向下动画
通过一个枚举来记录headView的状态
ACTION_UP://触摸松手
通过headView的状态来判断松手后的动作
a)下拉刷新中,那么完全隐藏
b)松手刷新,跳到最顶层,刷新,并修改相应的组件文本和动画,
c)正在刷新,此时,箭头改为progressbar,且不能listview不能滚动,

3、对外提供一个 OnRefreshListener 接口,并提供 setOnRefreshListener方法,用来设置当刷新过程中,需要执行的操作,比如获取数据 

4、并提供一个刷新完成的方法,用来更新界面的UI
清理动画
组件的文本
更新最后刷新时间等

4、在MainActivity利用runOnUithread()、Handler+Message或者AsyncTask后台获取数据,更新到界面
1、布局:
a)activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <cn.zengfansheng.refreshListview.RefreshListView
        android:id="@+id/refresh_listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </cn.zengfansheng.refreshListview.RefreshListView>
</RelativeLayout>  
b)listview_head.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:orientation="horizontal" >
    <!--1、箭头和进度条  -->
    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="5dp" >
        <!--箭头  -->
        <ImageView
            android:id="@+id/iv_arrow"
            android:layout_width="50dp"
            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="wrap_content"
            android:layout_height="wrap_content"
            android:indeterminateDrawable="@drawable/custom_progressbar"
            android:visibility="gone" />
    </FrameLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="5dp"
        android:gravity="center"
        android:orientation="vertical" >
        
        <!-- 刷新状态 -->
        <TextView
            android:id="@+id/tv_refresh_status"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="下拉刷新"
            android:textColor="#ff0000"
            android:textSize="20sp" />
        <!-- 最后刷新时间 -->
        <TextView
            android:id="@+id/tv_last_update_time"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="最后刷新时间:2013-12-02 23:28:02"
            android:textColor="@android:color/darker_gray"
            android:textSize="12sp" />
    </LinearLayout>
</LinearLayout>
2、核心代码:
RefreshListView.java
package cn.zengfansheng.refreshListview;
import java.text.SimpleDateFormat;
import java.util.Date;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AbsListView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
public class RefreshListView extends ListView {

    private ImageView iv_arrow;// 箭头
    private ProgressBar pb;// 进度条
    private TextView tv_refresh;// 刷新状态文本
    private TextView tv_last_update;// 最后刷新时间
    private int viewHeaderMeasuredHeight;// 测量后的高度
    private int myFirstVisibleItem;// listview中第一个可见的条目
    private float downY = -1;// 触摸按下Y轴的坐标,防止其他事件,导致触摸事件没有响应
    private float moveY;// 触摸移动时Y轴的坐标
    private View viewHeader;// 头布局
    
    private HeaderDisplayMode headerDisplayMode = HeaderDisplayMode.PULL_DOWN;//headerview的状态,默认为下拉刷新
    private float paddingTop;// headerview的上边距
    private Animation refresh_down_animation;// 箭头向下动画
    private Animation refresh_up_animation;// 箭头向上动画
    private OnRefreshListener mOnRefreshListener;// 刷新的监听器

    public RefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initHeader();// 初始化头
    }
    public RefreshListView(Context context) {
        super(context);
        initHeader();// 初始化头
    }
    /**
     * 1、初始化头布局
     */
    public void initHeader() {
        viewHeader = View.inflate(getContext(), R.layout.listview_headnull);
        iv_arrow = (ImageView) viewHeader.findViewById(R.id.iv_arrow);
        pb = (ProgressBar) viewHeader.findViewById(R.id.pb);
        tv_refresh = (TextView) viewHeader.findViewById(R.id.tv_refresh_status);
        tv_last_update = (TextView) viewHeader.findViewById(R.id.tv_last_update_time);
        // 设置箭头的最小宽度,防止播放动画时,宽度不够
        // iv_arrow.setMinimumWidth(40);
        /*System.out.println("测量前,通过getHeight()获取viewHeader的高度:"+viewHeader.getHeight());//0
        // 由于ListView每一个item,都会进行测量高度,但是现在还没有测量,所以需要手动测量
        // 测量viewHeader的高度和宽度
        measureHeader(viewHeader);
        System.out.println("测量后,通过getHeight()获取viewHeader的高度:"+viewHeader.getHeight());//0
        
        viewHeaderMeasuredHeight = viewHeader.getMeasuredHeight();
        System.out.println("测量后,通过getMeasuredHeight()获取viewHeader的高度:"+viewHeaderMeasuredHeight);//59
        
        // 如何实现head开始就隐藏呢,利用paddingTop
        //viewHeader.setPadding(0, -viewHeaderMeasuredHeight / 2, 0, 0);//此时,只显示viewHeader高度的一半
        viewHeader.setPadding(0, -viewHeaderMeasuredHeight, 0, 0);// 这样,就可以隐藏了*/
        tv_last_update.setText("最后更新时间:" + getCurrentTime());// 第一次进来,更新一次
        
        measureHeader(viewHeader);// 测量高度,否则获取不到头布局高度
        viewHeaderMeasuredHeight = viewHeader.getMeasuredHeight();
        
        viewHeader.setPadding(0, -viewHeaderMeasuredHeight, 0, 0);//隐藏头布局
        addHeaderView(viewHeader);
        
        // 注册滚动监听事件
        this.setOnScrollListener(new MyOnScrollListener());
        // 初始化动画
        intiAnimation();
    }
    /**
     * 1-1、初始化动画
     */
    private void intiAnimation() {
        // xml定义动画
        refresh_down_animation = AnimationUtils.loadAnimation(getContext(), R.anim.refresh_down);
        refresh_up_animation = AnimationUtils.loadAnimation(getContext(),R.anim.refresh_up);
        
        // 代码定义动画
        /*refresh_down_animation = new RotateAnimation(-180, -360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        refresh_down_animation.setDuration(500);
        refresh_down_animation.setFillAfter(true);*/
        /*refresh_up_animation = new RotateAnimation(0, -180,Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
        refresh_up_animation.setDuration(500);
        refresh_up_animation.setFillAfter(true);*/
    }
    /**
     * 2、测量并计算head的宽度和高度 测量item,这是在ListView中私有的方法,所以可以将其黏贴过来 
     * Measure a particular list child. unify with setUpChild.
     * @param viewHeaderThe child.
     */
    private void measureHeader(View viewHeader) {
        ViewGroup.LayoutParams lp = viewHeader.getLayoutParams();
        if (lp == null) {
            lp = new ViewGroup.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0, lp.width);// 测量标准
        int lpHeight = lp.height;
        int childHeightSpec;
        if (lpHeight > 0) {
            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
        } else {
            childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        viewHeader.measure(childWidthSpec, childHeightSpec);
    }
    
    /**
     * 3、滚动事件-获取第一个item在ListView中的位置
     * @author hacket
     */
    private class MyOnScrollListener implements OnScrollListener {
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
        }
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                int visibleItemCount, int totalItemCount) {
            myFirstVisibleItem = firstVisibleItem;
        }
    }
    // 4、触摸事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
         
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:// 触摸按下
            downY = event.getY();// downY初始值为-1,防止其他事件,导致触摸事件没有响应
            System.out.println("downY:" + downY);
            break;
        case MotionEvent.ACTION_MOVE:// 触摸移动
            // 如果当前的状态是正在刷新中, 跳出
            if (headerDisplayMode == HeaderDisplayMode.ISREFRESHING) {// 正在刷新时,不可触摸移动
                break;
            }
            if (downY == -1) {
                downY = event.getY();
            }
            // 获得移动中y轴的偏移量
            moveY = event.getY();
            // 头布局的负数 + (移动中y轴的偏移量 - 按下时y轴的偏移量) / 2
            paddingTop = -viewHeaderMeasuredHeight + (moveY - downY) / 2;
            //判断paddingTop是否大于头布局的高度的负数,并且滚动时第一个显示的item为0 进入if语句
            if (myFirstVisibleItem==0 && paddingTop > -viewHeaderMeasuredHeight) {// 开始下拉刷新,如果不判断myFirstVisibleItem,后面的item下拉时也不正常
                // 开始移动下拉布局
                if (paddingTop > 0// 完全显示头布局
                        && headerDisplayMode == HeaderDisplayMode.PULL_DOWN) {// 如果大于0,说明已经完全出来了,变成松手刷新状态
                    // 改变当前的状态为 松开刷新
                    headerDisplayMode = HeaderDisplayMode.REALEASE_REFRESH;
                    refreshHeaderView();
                } else if (paddingTop <= 0 // 还没有完全显示头布局
                        && headerDisplayMode == HeaderDisplayMode.REALEASE_REFRESH) {// 小于0,那么变成下拉刷新
                    // 改变当前的状态为下拉状态
                    headerDisplayMode = HeaderDisplayMode.PULL_DOWN;
                    refreshHeaderView();
                }
                // refreshHeaderView();//不能放在这里,如果在松手状态,会多次执行松手动画,导致,箭头上下在移动,影响用户体验
                viewHeader.setPadding(0, (intpaddingTop, 0, 0);
                return true;// 消费掉该事件
            }
            break;
        case MotionEvent.ACTION_UP:// 触摸松手
            // 如果当前状态是正在刷新中 跳出
            if (headerDisplayMode == HeaderDisplayMode.ISREFRESHING) {// 正在刷新时,不可触摸移动
                break;
            }
            // 如果当前状态是松开刷新, 进入if 做刷新的操作
            if (headerDisplayMode == HeaderDisplayMode.REALEASE_REFRESH) {
                headerDisplayMode = HeaderDisplayMode.ISREFRESHING;
                paddingTop = 0; 
                viewHeader.setPadding(0, (intpaddingTop, 0, 0);
                refreshHeaderView();
                if (mOnRefreshListener != null) {// 如果调用者实现了OnRefreshListener接口
                    // 调用刷新回调方法
                    mOnRefreshListener.onRefresh();// 调用接口中暴露出去的方法
                }
            // 当下拉状态时
            } else if (headerDisplayMode == HeaderDisplayMode.PULL_DOWN) {
                // 把头布局复位为隐藏状态
                paddingTop = -viewHeaderMeasuredHeight;
                viewHeader.setPadding(0, (intpaddingTop, 0, 0);
                refreshHeaderView();
            }
            downY = -1;
            moveY = -1;
            break;
        }
        return super.onTouchEvent(event);// 消费掉该事件,如果true,那么就消费了该事件,那么就无法滑动了,所以不能为true
    }
    /**
     * 5、刷新头布局
     */
    private void refreshHeaderView() {
        if (headerDisplayMode == HeaderDisplayMode.PULL_DOWN) {// 下拉状态
            iv_arrow.startAnimation(refresh_down_animation);
            tv_refresh.setText("下拉刷新");
            System.out.println("下拉刷新动画~~~");
        } else if (headerDisplayMode == HeaderDisplayMode.REALEASE_REFRESH) {// 松手刷新
            iv_arrow.startAnimation(refresh_up_animation);
            tv_refresh.setText("松手刷新");
            System.out.println("松手刷新动画~~~");
        } else if (headerDisplayMode == HeaderDisplayMode.ISREFRESHING) {// 正在刷新
            iv_arrow.clearAnimation();// 清理动画
            iv_arrow.setVisibility(View.GONE);
            pb.setVisibility(View.VISIBLE);
            tv_refresh.setText("正在刷新中");
        }
    }
    /**
     * 6、headerview状态枚举
     * @author hacket
     */
    public enum HeaderDisplayMode {
        PULL_DOWN// 下拉刷新
        REALEASE_REFRESH// 松手刷新
        ISREFRESHING // 正在刷新中
    }
    
    /**
     * 7-1、当刷新时的监听事件实现的接口
     */
    public interface OnRefreshListener {
        public abstract void onRefresh();
    }
    /**
     * 7-2、设置刷新时监听器
     * @param mOnRefreshListener
     */
    public void setOnRefreshListener(OnRefreshListener mOnRefreshListener) {
        this.mOnRefreshListener = mOnRefreshListener;
    }
    /**
     * 8、刷新完成后,将状态初始化为下拉状态
     */
    public void refreshFinish() {
        // 隐藏头布局
        viewHeader.setPadding(0, -viewHeaderMeasuredHeight, 0, 0);
        // 更改头布局状态为下拉
        headerDisplayMode = HeaderDisplayMode.PULL_DOWN;
        // 进度条不可见
        pb.setVisibility(View.GONE);
        // 箭头可见
        iv_arrow.setVisibility(View.VISIBLE);
        // 头布局状态文本
        tv_refresh.setText("下拉刷新");
        // 头布局最后刷新时间文本
        tv_last_update.setText("最后一次刷新时间:" + getCurrentTime());
    }
    /**
     * 9、获取当前系统时间
     * @return
     */
    private String getCurrentTime(){
        SimpleDateFormat sdp = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        String time = sdp.format(new Date());
        return time;
    }
}
3、使用该下拉刷新菜单:
MainActivity.java
package cn.zengfansheng.refreshListview;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;
import cn.zengfansheng.refreshListview.RefreshListView.OnRefreshListener;
public class MainActivity extends Activity {
    private RefreshListView refreshListView;
    private List<Integer> list;
    private RefreshAdapter freshAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        list = new ArrayList<Integer>();
        for (int i = 0; i < 50; i++) {
            list.add(i + 1);
        }
        refreshListView = (RefreshListView) this.findViewById(R.id.refresh_listview);
        if (freshAdapter == null) {
            freshAdapter = new RefreshAdapter();
        }
        refreshListView.setAdapter(freshAdapter);
        // 设置刷新监听器
        refreshListView.setOnRefreshListener(new OnRefreshListener() {
            @Override
            public void onRefresh() {
                // TODO:
                //SystemClock.sleep(2000);// 和Thread.sleep();类似,只是不会抛出异常,两秒钟后模拟刷新完毕操作,要在子线程执行
                // 3、AsyncTask异步任务
                // new AsyncTask<Params, Progress, Result>
                new AsyncTask<String, Object, Integer>() {
                    @Override
                    protected void onPreExecute() {
                        super.onPreExecute();
                        Toast.makeText(getApplicationContext(), "开始任务刷新", 0).show();
                    }
                    @Override
                    protected Integer doInBackground(String... params) {// 参数1,String后台任务中需要的参数,传递到这里,方法Integer的result
                        String p1 = params[0];
                        String p2 = params[1];
                        System.out.println("params:" + p1 + ":" + p2);
                        list.add(0, (int) (Math.random() * 10));
                        SystemClock.sleep(2000);
                        return 202;
                    }
                    @Override
                    protected void onPostExecute(Integer result) {// 参数3,Integer执行完毕后,然后的参数,被这里给接收
                        super.onPostExecute(result);
                        System.out.println("result:" + result);
                        refreshListView.refreshFinish();
                        freshAdapter.notifyDataSetChanged();
                        Toast.makeText(getApplicationContext(), "刷新完毕", 0).show();
                    }
                    @Override
                    protected void onProgressUpdate(Object... values) {// 参数2:Object执行过程中,数据的传递
                        super.onProgressUpdate(values);
                    }
                }.execute("hehe""haha");// 这里传入参数1:String,为后台任务所需要的参数
                // 2、 Handler+Message
                // 由于比较耗时,所以用子线程
                /*
                 final Handler handler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        super.handleMessage(msg);
                        refreshListView.refreshFinish();
                        freshAdapter.notifyDataSetChanged();
                        Toast.makeText(getApplicationContext(), "刷新完毕", 0).show();
                    }
                };
                new Thread() {
                    public void run() {
                        SystemClock.sleep(2000);
                        list.add(0, (int) (Math.random() * 10));
                        handler.sendEmptyMessage(0);// 由于是在子线程中,所以需要发送消息
                    }
                }.start();*/
                
                // 1、runOnUiThread
                /*new Thread() { 
                    public void run() {
                        SystemClock.sleep(2000);//子线程中睡眠
                        list.add(0, new Random().nextInt(100));
                        runOnUiThread(new Runnable() {
                            public void run() {
                                refreshListView.refreshFinish();// 调用刷新完毕方法,更新头布局ui
                                freshAdapter.notifyDataSetChanged();
                                Toast.makeText(getApplicationContext(), "刷新完毕", 0).show();
                            }
                        });
                    }
                }.start();*/
            }
        });
    }
    
    /**
     * 1、下拉刷新数据适配器类
     * @author hacket
     */
    private class RefreshAdapter extends BaseAdapter {
        @Override
        public int getCount() {
            if (list != null) {
                return list.size();
            }
            return 0;
        }
        @Override
        public Object getItem(int position) {
            return getItem(position);
        }
        @Override
        public long getItemId(int position) {
            return position;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            
            TextView tView;
            if (convertView != null && convertView instanceof TextView) {
                tView = (TextView) convertView;
            } else {
                tView = new TextView(getApplicationContext());
            }
            Integer i = list.get(position);
            tView.setTextColor(Color.BLACK);
            tView.setText("这是下拉菜单刷新数据:"+i);
            return tView;
        }
    }
}
4、结果:
a) 默认头布局隐藏,但下拉时,箭头向下,文本显示下拉刷新
b) 当下拉到超出该头文件的高度,那么箭头向上,文本显示松手刷新
c) 当松手后,箭头不可见,progressbaar可见,文本更新为正在刷新
d)当刷新完毕后,还原界面ui,并更新最后刷新时间,然后将头布局给隐藏掉