自定义控件实现ListView下拉刷新和上拉加载

自定义控件实现ListView下拉刷新和上拉加载

下拉刷新和上拉加载时一个很常用的功能,刚好今天学了,好好的总结一把!

下拉刷新实现思路:

  1. 第一步:创建一个类继承ListView
  2. 第二步:写一个头部,添加到listview中,先将其隐藏
  3. 第三步:设置监听触屏事件,判断是否滑动到顶部
  4. 第四步:当到顶部的时候,通过下拉的位移来设置头部的显示高度,并根据头部显示的高度来设置对应的文字和动画效果
  5. 第五步:当松手时,判断需要进行哪一种更新并进行相应的操作

上拉加载实现思路:

由于两个功能是写在一个类里面的,这里就不需要再创建一个类了。

  1. 第一步:写一个尾部,添加到listview中,先将其隐藏
  2. 第二步:设置监听滚动监听事件,判断是否滑动到底部
  3. 第三部:加载数据

具体代码实现

代码里面思路也很清楚,而且注释很多,相信很容易看明白。
自定义控件类:

public class PullToRefresh extends ListView {

    private int downY;
    private View header;
    private int headerHeight;
    private ImageView ivArrow;
    private TextView tvTitle;
    private TextView tvTime;
    private RotateAnimation downAnin;
    private RotateAnimation upAnin;
    private boolean isLoading = false;
    private OnPullToRefreshListener onPullToRefreshListener;
    private static final int PULL_TO_REFRESH = 1; // 下拉刷新状态
    private static final int RELEASE_TO_REFRESH = 2; // 释放刷新状态
    private static final int REFRESHING = 3; // 正在刷新状态

    public PullToRefresh(Context context, AttributeSet attrs) {
        super(context, attrs);

        addHeader();
        initComponent();
        addFooter();
        // 设置滑动监听事件

        setOnScrollListener(new OnScrollListener() {

            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                int count = view.getAdapter().getCount(); // 获取条目数

                switch (scrollState) {
                case OnScrollListener.SCROLL_STATE_FLING:

                    break;
                case OnScrollListener.SCROLL_STATE_IDLE:
                    // 在闲置状态时判断是否到达了底部
                    if (getLastVisiblePosition() == count - 1 && !isLoading) {
                        footer.setPadding(0, 0, 0, 0);
                        setSelection(count - 1);
                        isLoading = true;
                        if (onPullToRefreshListener != null) {
                            onPullToRefreshListener.loadMore();
                        }
                    }
                    break;
                case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:

                    break;

                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem,
                    int visibleItemCount, int totalItemCount) {

            }
        });

    }

    /**
     * 为控件添加一个尾部
     * */
    private void addFooter() {
        footer = View.inflate(getContext(), R.layout.footer, null);
        footer.measure(0, 0); // 手动测量尾部的高度,否则在这里无法获取到尾部的高度
        footerrHeight = header.getMeasuredHeight();
        footer.setPadding(0, -footerrHeight, 0, 0); // 通过设置尾部的paddingTop来将尾部先隐藏
        addFooterView(footer);
    }

    /**
     * 设置接口的引用
     * */
    public void setOnPullToRefreshListener(
            OnPullToRefreshListener onPullToRefreshListener) {
        this.onPullToRefreshListener = onPullToRefreshListener;
    }

    /**
     * 初始化组件找到它们
     * */
    private void initComponent() {
        ivArrow = (ImageView) findViewById(R.id.iv_arrow);
        ivLeft = (ImageView) findViewById(R.id.iv_left);
        tvTitle = (TextView) findViewById(R.id.tv_title);
        tvTime = (TextView) findViewById(R.id.tv_time);

        upAnin = new RotateAnimation(0, 180, RotateAnimation.RELATIVE_TO_SELF,
                0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        upAnin.setDuration(200);
        upAnin.setFillAfter(true);// 动画完成时停留在那里
        downAnin = new RotateAnimation(180, 0,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        downAnin.setDuration(200);
        downAnin.setFillAfter(true);// 动画完成时停留在那里
        loadingAnim = new RotateAnimation(0, 360,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        loadingAnim.setRepeatCount(RotateAnimation.INFINITE);
        loadingAnim.setRepeatMode(RotateAnimation.RESTART);
        loadingAnim.setInterpolator(new LinearInterpolator());
        loadingAnim.setDuration(1000);
    }

    /**
     * 第一步:为listview添加一个头部
     * */
    private void addHeader() {
        header = View.inflate(getContext(), R.layout.header, null);
        header.measure(0, 0); // 手动测量头部的高度,否则在这里无法获取到头部的高度
        headerHeight = header.getMeasuredHeight();
        header.setPadding(0, -headerHeight, 0, 0); // 通过设置头部的paddingTop来将头部先隐藏
        addHeaderView(header);
    }

    // 第二步:监听触屏事件,判断是否滑动到顶部并进行相应的操作
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            downY = (int) ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            // 记录下当前滑动的垂直坐标
            int moveY = (int) ev.getY();
            int dy = moveY - downY; // 垂直位移
            downY = moveY;
            int paddingTop = header.getPaddingTop();
            // 不是正在刷新状态、到达头部并且头部在显示的时候
            if (state != REFRESHING // 不是正在刷新状态
                    && getFirstVisiblePosition() == 0 // 到达头部
                    && (dy > 0 // 下滑
                    || paddingTop > -headerHeight)) { // 上滑,头部在显示
                header.setPadding(0, paddingTop + dy, 0, 0); // 设置头部显示状态
                /**
                 * 第三步:通过下拉的高度来判断进行何种刷新动作 1.头部没有完全露出来,进行下拉刷新 2.头部完全露出来,则进行释放刷新
                 * */
                if (paddingTop >= 0) {
                    // 进入释放刷新状态
                    setState(RELEASE_TO_REFRESH);
                } else {
                    // 进入下拉刷新状态

                    setState(PULL_TO_REFRESH);
                }
                return true;
            }
            break;
        case MotionEvent.ACTION_UP:
            /**
             * 第四步:当松开手之后,判断需要进行哪种刷新 这里我们在resetHeader()方法里面进行判断
             * 
             * */
            resetHeader();
            break;

        default:
            break;
        }
        return super.onTouchEvent(ev);
    }

    /**
     * 设置刷新状态
     * */
    private int state = PULL_TO_REFRESH; // 记录当前状态
    private ImageView ivLeft;
    private RotateAnimation loadingAnim;
    private View footer;
    private int footerrHeight;

    private void setState(int state) {
        if (this.state != state) {
            if (state == PULL_TO_REFRESH) {
                tvTitle.setText("下拉刷新");
                ivArrow.startAnimation(downAnin);
            } else if (state == RELEASE_TO_REFRESH) {
                tvTitle.setText("释放刷新");
                ivArrow.startAnimation(upAnin);
            } else if (state == REFRESHING) {
                tvTitle.setText("正在刷新");
                tvTime.setText("最后刷新时间:"
                        + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                ivArrow.clearAnimation();// 清除箭头动画
                ivArrow.setVisibility(View.GONE);// 将箭头隐藏

                ivLeft.setVisibility(View.VISIBLE); // 将正在刷新图标显示出来
                ivLeft.startAnimation(loadingAnim);// 开始刷新动画

            }
            this.state = state;

        }
    }

    /**
     * 下拉刷新操作完成
     * */
    public void refreshFinish() {
        ivLeft.clearAnimation();
        ivLeft.setVisibility(View.GONE);
        ivArrow.setVisibility(View.VISIBLE);
        setState(PULL_TO_REFRESH);
        header.setPadding(0, -headerHeight, 0, 0);// 重新隐藏头
    }
    /**
     * 上拉加载操作完成
     * */
    public void loadMoreFinish() {
        isLoading = false;
        footer.setPadding(0, -footerrHeight, 0, 0);
    }

    /**
     * 重新设置头部
     * 
     * */
    private void resetHeader() {
        if (state == PULL_TO_REFRESH) {
            header.setPadding(0, -headerHeight, 0, 0); // 重新将头部隐藏
        } else if (state == RELEASE_TO_REFRESH) {
            header.setPadding(0, 0, 0, 0); // 刚好将头部完全显示出来
            setState(REFRESHING); // 设置为正在刷新状态
            // 开始加载数据
            if (onPullToRefreshListener != null) {
                onPullToRefreshListener.refresh();
            }

        }
    }

    public interface OnPullToRefreshListener {
        public void refresh(); // 刷新
        public void loadMore(); // 加载更多
    }

}

总结一下写的过程中碰到的坑:

  1. 由于我们添加头部和尾部的操作是在构造方法里面进行的,系统还没来得及为我们测量控件的高宽,因此我们在获取控件高宽的时候必须自己调用measure()方法来测量,否则获取到的控件高宽始终为0;
  2. 在监听触屏事件中,我们做了一个操作:downY = moveY;这样我们所获取到的垂直位移才是实时移动的位移。
  3. if (state != REFRESHING && getFirstVisiblePosition() == 0 && (dy > 0 || paddingTop > -headerHeight))这语句中,paddingTop > -headerHeight这句是用来判断向上滑动并且头部处于显示状态。如果没有这个判断,我们高频率的小幅度滑动屏幕时,会发现头部上面的空白区域会不断的增大。
  4. setState(int state)方法处,我们需要设置一个变量来记录当前控件所处的状态,并通过这个变量状态来决定是否执行方法里面的代码,否则你会发现,当你下拉控件的时候,那个指示图标会不停的上下转动,那是它在不断的反复执行动画的效果。
  5. 加载数据属于耗时操作,必须在子线程中进行,否则会出现“卡卡”的现象,反正我由于忘记了这个东东找了好长时间。
  6. 在刷新完成之后,即在refreshFinish()方法里面,我们需要进行动画清除,不然再次下拉的时候会出现图标“残影”,还要将控件状态还原到下拉刷新状态setState(PULL_TO_REFRESH),不然保证你拉不了第二次,就这么给力。
  7. 最后一点,为了降低代码之间的耦合性,用到了接口回调,那么,我们在需要加载数据的时候(控件类代码中)必须先进行非空判断,不然空指针异常等着你。

header.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="center"
android:orientation="horizontal" >
    <ImageView 
        android:id="@+id/iv_left"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher"
        android:visibility="gone"/>
    <ImageView 
        android:id="@+id/iv_arrow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/common_listview_headview_red_arrow"/>
    <LinearLayout 
        android:layout_marginLeft="10dp"
        android:gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        >
        <TextView 
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:textColor="#f00"
            android:text="下拉刷新"/>
        <TextView 
            android:id="@+id/tv_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#f00"
            />
    </LinearLayout>
</LinearLayout>

footer.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="center_horizontal"
android:orientation="horizontal" >

    <ProgressBar 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView 
        android:layout_gravity="center_vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:text="正在加载中..."/>
</LinearLayout>

在其他项目里面测试的代码:
布局文件activity_main.xml

<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="${relativePackage}.${activityClass}" >

    <com.example.pulltorefreshdemo.PullToRefresh 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/ptrlist">

    </com.example.pulltorefreshdemo.PullToRefresh>

</RelativeLayout>

MainActivity

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final PullToRefresh ptr = (PullToRefresh) findViewById(R.id.ptrlist);
        final List<String> data = getData();
        final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1,data);
        ptr.setAdapter(adapter);
        /**
         * 刷新加载数据,接口回调,大大降低了代码间的耦合性
         * */
        ptr.setOnPullToRefreshListener(new OnPullToRefreshListener() {

            @Override
            public void refresh() {
                //加载数据属于耗时操作,需要开启子线程
                new Thread(new Runnable() {
                    public void run() {
                        SystemClock.sleep(1000);    //模拟数据
                        String newData = "下拉刷新更多的数据";
                        data.add(0,newData);
                        //更新UI的动作只能在主线程中完成
                        runOnUiThread(new Runnable() {
                            public void run() {
                                adapter.notifyDataSetChanged();
                                ptr.refreshFinish();
                            }
                        });

                    }
                }).start();
            }

            @Override
            public void loadMore() {
                // TODO Auto-generated method stub
                new Thread(new Runnable() {
                    public void run() {
                        SystemClock.sleep(1000);
                        String newData = "上拉加载更多的数据";
                        data.add(newData);
                        //更新UI的动作只能在主线程中完成
                        runOnUiThread(new Runnable() {
                            public void run() {
                                adapter.notifyDataSetChanged();
                                ptr.loadMoreFinish();
                            }
                        });
                    }
                }).start();
            }

        });
    }
    /**
     * 初始化数据
     * */
    private List<String> getData() {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 15; i++) {
            list.add("原始数据"+i);
        }
        return list;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值