前篇:自定义(扩展性能强!)的下拉刷新和上拉加载控件

大家好,本人挺久没写博客了,一方面不知道到有什么好写的,想写的东西虽然东西也实现了,但是代码很简练,也无法和市面上那些灰常成熟的做比较,因此就没写,但是,下拉刷新和上拉加载这东西,相信一大部分人和博主一样,都是用市面上的,一来有些确实扩展性比较差,每次修改头和脚的布局都要在自定义的listview或者自定义的view类查找位置,灰常浪费时间,因此,博主就打算写一个扩展性不错的,可以直接在activity加载头和尾布局,并在相应的回调方法实现头布局的动画效果。

老规矩,先上3张效果图,

效果图1:只有下拉刷新:


从图可以看出, 箭头的旋转是和下拉的高度有关,也就是在回调方法实现旋转效果,同时,“正在刷新”状态的时候,可以通过向上滑动,打断刷新动作。

效果图2:只有上拉加载


这里没什么好说的,唯一不同的是,加载的时候,滑动不会打算加载过程,至于为什么这样设置,一来如果要打算,代码量比较大,二来很多童鞋都试过加载的同时也会滑动吧,在这里,肯定有童鞋说,那为什么图一可以打断,其实那个打断和刷新结束用的是同一个方法,只要在方法内部不要实现打断效果就行了,实在不懂得话,等会讲解会说到。


效果图三,不用猜的知道,就是上面两者结合,即下拉刷新+上拉加载


多的不说了,也就是2个功能合并


看到这里,有童鞋可能会说,切,不就是下拉刷新和上拉加载么,如果这么想,请你重新看回文章标题哈。

看自定义的listview之前,先看看adapter

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        convertView = li.inflate(R.layout.item_list,null);
        convertView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, "position:" + position, Toast.LENGTH_SHORT).show();
            }
        });
        TextView tv = (TextView) convertView.findViewById(R.id.tv);
        tv.setText(mList.get(position));
        return convertView;
    }
如果想使用博主的listview类的童鞋,请不要调用listview的setOnItemClick();方法,因为某些情况,下拉刷新的时候也会有点中效果,因此,博主直接给convertView设置点击,抢占了listview的事件,使listview的setOnItemClick();方法失效。在这里,有些童鞋也会说到,人家点击的有时候有个“小灰灰”出现啊,这里没咋办,OK,直接给根布局设置个selector,先看代码,

这是adapter对应的item_list.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    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:background="@drawable/root_selector"
    android:gravity="center">

    <TextView
        android:id="@+id/tv"
        android:padding="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="22sp"
        tools:text="萨大大缩短"/>

</LinearLayout>

    android:background="@drawable/root_selector"

在看看selector的xml文件:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:drawable="@color/gray"/>
    <item android:state_pressed="false" android:drawable="@color/white"/>
</selector>

这里,“小灰灰”就不会丢了,点击的时候仍然有效!

好吧,gray对应的颜色是#eee,white就不说了!!!


现在开始上自定义listview的代码:

public class PullRefreshListView extends ListView implements AbsListView.OnScrollListener {
    private Context mContext;
    /**
     * listview的头部
     */
    private View headerView;
    /**
     * listview的脚部
     */
    private View footerView;

    /**
     * 头部高度
     */
    private int headerHeight;

    /**
     * 下拉刷新状态
     */
    public static final int PULL_REFRESH = 0x001;

    /**
     * 松开刷新状态
     */
    public static final int LOOSEN_REFRESH = 0x002;

    /**
     * 正在刷新状态
     */
    public static final int REFRESHING = 0x003;

    /**
     * 正在加载
     */
    public static final int LOADING = 0x004;

    /**
     * 初始化状态
     */
    private int mState = PULL_REFRESH;
    private ValueAnimator valueAnimator;
    private int footerHeight;
在这里有一些状态,以及头和脚布局的view

对应的状态也有4种之多,分别是默认的下拉刷新,然后是松开刷新,再然后是正在刷新,最后那个正在加载其实和前面三个半毛钱关系都没有,严谨的同学,最好自己在业务上处理一下,避免下拉刷新的时候上拉加载,所以,最好还是在中断刷新的方法上把打断刷新的逻辑也写上,这样,正在刷新的状态,手指向上划,就会把“正在刷新”状态变成“下拉刷新”状态(下拉刷新是最初始的状态。)


中断刷新的方法是:

    @Override
    public void stopRefresh() {
        h.removeCallbacks(r);
        Toast.makeText(this, "停止刷新可以通过手动滑动(更新状态为非“正在刷新”)停止", Toast.LENGTH_SHORT).show();
    }

这个方法等会也会说上的。

接下来,看三个构造器:

public PullRefreshListView(Context context) {
        super(context);
        mContext = context;
        setOnScrollListener(this);
    }

    public PullRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        setOnScrollListener(this);
    }

    public PullRefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        setOnScrollListener(this);
    }

这三个构造器其实都是一个样子,博主有点懒,就不短调长了。

    /**
     * 设置头部
     *
     * @param view
     */
    private void setHeaderView(View view) {
        headerView = view;
        headerView.measure(0, 0);
        headerView.setPadding(0, -headerView.getMeasuredHeight(), 0, 0);
        headerHeight = headerView.getPaddingTop();
        addHeaderView(headerView);
    }

这是设置头部的布局,看到哦,这是私有的!好,现在无视“私有”这两个字,这个方法会在activity调用,这样就可以省去繁琐的查找源码,修改布局的时间。


有头就有脚,看看脚

    /**
     * 设置脚部
     *
     * @param view
     */
    private void setFooterView(View view) {
        footerView = view;
        footerView.measure(0, 0);
        footerView.setPadding(0, 0, 0, -footerView.getMeasuredHeight());
        footerHeight = footerView.getPaddingBottom();
        addFooterView(footerView);
    }
这2个其实有点相似,头是将paddingtop设置为(负数)头的高度,脚就是将paddingBottom设置成(负数)脚的高度。这样就达到了一开始隐藏头和脚的目的。

来到本章重点了,处理滑动监听,

    /**
     * 不使用actiondown,避免adapter的点击事件与其冲突
     *
     * @param ev
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (headerView == null || isLoading)
            return super.onTouchEvent(ev);


        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
                if (isUp) {
                    isUp = false;
                    // TODO 这里进行down操作
                    lastY = (int) ev.getY();
                }
                float dY = (ev.getY() - lastY) * mDamp;
                lastY = (int) ev.getY();
                break;
            case MotionEvent.ACTION_UP:
                isUp = true;
                break;
        }

        return super.onTouchEvent(ev);
    }

事件处理就是这种模式,有童鞋会留意到,actiondown好像不见了,因为博主担心有些童鞋和博主一样,不怎么喜欢使用listview的onitemClick方法,如果在adapter上将convertView设置点击监听,listview的actionDown会被抢占,也就是说,没反应!
所以,在调用move的时候判断up了没,up了就表示是第一次move,博主也就将第一次Move当成down来使用。

现在先按顺序看看move的其他部分,

                boolean isPulling = ev.getY() - lastY > 0;
                float dY = (ev.getY() - lastY) * mDamp;
                lastY = (int) ev.getY();
                int paddingTop = (int) (headerView.getPaddingTop() + dY);

isPulling为true表示手指向下滑动,

dY表示两次move之间的间隔,其中mDamp为阻尼系数,有这么一个方法

    private float mDamp = 1.0f;

    /**
     * 设置阻尼系数,默认为1.0
     */
    private void setDamp(float damp) {
        mDamp = damp;
    }

默认阻尼系数为1.0,说明手指滑动和listview滚动的距离是一样的,如果大于1.0,listview会比手指滚得快,反之,listview比手指滚的慢。

                int paddingTop = (int) (headerView.getPaddingTop() + dY);
这个是用来判断头部将要达到的高度,目的高度=原来高度+dY,因为向下滑动的时候dY>0,所以向下滑的时候,头部高度会更高。

                if (isPulling && mState == REFRESHING)
                    return true;
这个方法,是用于当显示“正在刷新”状态的时候,禁止向上滑动,因为向上滑动会变成“松开刷新”状态,此时会允许他向下滑动,即变为“下拉刷新”状态。


接下来是move的最后一个地方,也是逻辑量比较大的

                if (paddingTop > headerHeight && getFirstVisiblePosition() == 0) {
                    scrollHeaderBy((int) dY);
                    // 露出头的百分比,超过1转松开刷新
                    float percent = (headerView.getPaddingTop() + Math.abs(headerHeight)) * 1.0f / Math.abs(headerHeight);
                    if (mCallback != null) {
                        mCallback.drag(percent, (int) dY);
                        mCallback.dragToLoosen(percent <= 1 ? percent : 1, percent <= 1 ? (int) dY : 0);
                    }
                    if (percent <= 1) {
                        if (mState != PULL_REFRESH)
                            setState(PULL_REFRESH);
                    } else {
                        if (mState != LOOSEN_REFRESH)
                            setState(LOOSEN_REFRESH);
                    }
                    return true;
                }

首先看判断,因为之前做了预判paddingTop,也就是知道了目的高度是怎样,再和头部本身高度作对比,后面那个,可以当作当listview拼命向下滑到最高度的时候,它的值就是0(这里的0不是指第一个item,而是指头布局,因为头布局已经被addHeader,也就会在所有item的上面。


    /**
     * 通过偏移量移动头部
     *
     * @param dY
     */
    private void scrollHeaderBy(int dY) {
        int paddingTop = headerView.getPaddingTop();
        int end = paddingTop + dY;
        headerView.setPadding(0, end <= headerHeight ? headerHeight : end, 0, 0);
    }

这个方法是通过一点点的小小偏移量dY来偏移头布局。当头部刚好完全不可见的时候,paddingTop刚好为(负数)头的高度,再小就没意义了,因此作纠正。


                    // 露出头的百分比,超过1转松开刷新
                    float percent = (headerView.getPaddingTop() + Math.abs(headerHeight)) * 1.0f / Math.abs(headerHeight);

假设头部完全不可见的时候,getPaddingTop = -高度,而(-高度)的绝对值等于正高度,两者抵消为0,此时percent为0,刚手指慢慢向下滑动,paddingTop慢慢增加,而分子的值慢慢向上增大,percent从0 到0.01 到0.1到1,然后再超过1。


                    if (mCallback != null) {
                        mCallback.drag(percent, (int) dY);
                        mCallback.dragToLoosen(percent <= 1 ? percent : 1, percent <= 1 ? (int) dY : 0);
                    }

callback是一个回调,当没有设置监听的时候,也就不需要回调方法,drag和dragToLoosen的区别就是,drag意思就是拖动,也就是,头部从完全不可见到恰好完全可见,到继续往下拖动的时候(状态为“松开刷新”),这个方法在松开手指之前,会一直调用,percent将可以达到大于1的值,对于一些特别奇葩的动画,相信这个方法可以用上,dragToLoosen的意思就是,只会在“下拉刷新”状态的时候调用,也就是percent的值为0-1,

总结起来就是这样,当状态为"松开刷新"时,松手(Up)的话,也会有一个回调执行drag,让它的percent从大于1到1.0,当状态为“下拉刷新”时,松手的话,也会有一个回调执行dragToLoosen,让它的percent从小于1大于0 到0.0。


                    if (percent <= 1) {
                        if (mState != PULL_REFRESH)
                            setState(PULL_REFRESH);
                    } else {
                        if (mState != LOOSEN_REFRESH)
                            setState(LOOSEN_REFRESH);
                    }

当percent<=1 说明此刻状态是"下拉刷新",否则就是"松开刷新"。


    private void setState(int state) {
        switch (state) {
            // 下拉刷新状态
            case PULL_REFRESH:
                if (mState == REFRESHING)
                    if (mCallback != null && headerView != null)
                        mCallback.stopRefresh();

                if (mCallback != null && headerView != null)
                    mCallback.toRullRefresh();
                if (mState != PULL_REFRESH)
                    mState = PULL_REFRESH;
                break;
            // 松开刷新状态
            case LOOSEN_REFRESH:
                if (mCallback != null && headerView != null)
                    mCallback.toLoosenRefresh();
                if (mState != LOOSEN_REFRESH)
                    mState = LOOSEN_REFRESH;
                break;
            // 正在刷新状态
            case REFRESHING:
                if (mState == LOOSEN_REFRESH && headerView != null) {
                    mState = REFRESHING;
                    if (mCallback != null)
                        mCallback.toRefreshing();
                }
                break;
            case LOADING:
                if (mState == PULL_REFRESH) {
                    mState = LOADING;
                    if (!isLoading) {
                        mCallback.toLoading();
                        isLoading = true;
                    }
                }
                break;
            default:
                break;
        }
    }

当想设置状态为“下拉刷新”时,上一个状态如果是“正在刷新”,会通过回调调用停止刷新这个方法,也就是之前说到的打断。此时,回调调用下拉刷新方法,将状态设置为“下拉刷新”。

当想设置状态为“松开刷新”,回调调用松开刷新方法,将状态设置为“松开刷新”。

当想设置状态为“正在刷新”,先判断上一个状态是不是松开刷新,如果是,回调调用正在刷新方法,并将状态设置为“正在刷新”。

当想设置状态为“正在加载”,先判断上一个状态是不是“下拉刷新”,如果是,回调调用正在加载方法,并将isLoading设为true。


    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (footerView == null )
            return;


        if (getLastVisiblePosition() == getCount() - 1 && mState != LOADING) {
            footerView.setPadding(0, 0, 0, 0);
            if (!isLoading) {
                setState(LOADING);
                setSelection(getCount());
            }
        }
    }

滑动时候,如果有footerView,并判断最后一个可见的position是不是总数-1(开始为0),并且状态不是正在加载,此时,将脚布局显示出来,并根据条件执行。


此时,看up:

                // 最终位置
                int endHeight = 0;
                if (mState == PULL_REFRESH)
                    endHeight = headerHeight;
                else if (mState == LOOSEN_REFRESH)
                    endHeight = 0;

根据状态,决定最终的动画停止位置,if条件时,停止在(负)头布局的高度,else if条件时,停止在0,即头部恰好完全可见。

                valueAnimator = ValueAnimator.ofInt(headerView.getPaddingTop(), endHeight);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        int padding = (int) animation.getAnimatedValue();
                        lastPadding = padding;
                        float percent = (padding + Math.abs(headerHeight)) * 1.0f / Math.abs(headerHeight);
                        mCallback.drag(percent,padding - lastPadding);
                        if (padding < 0) {
                            if (mCallback != null)
                                mCallback.dragToLoosen(percent, padding - lastPadding);
                        }
                        headerView.setPadding(0, padding, 0, 0);
                    }
                });

此时,是通过动画,实现回弹效果。

                valueAnimator.addListener(new SimpleAnimatorListener() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (lastPadding == 0) {
                            if (mState != REFRESHING)
                                setState(REFRESHING);
                        } else if (lastPadding == headerHeight) {
                            if (mState != PULL_REFRESH)
                                setState(PULL_REFRESH);
                        }
                    }
                });

这是监听动画结束时候,来决定状态。


在move时候,有一个地方是用来取消动画的,动画取消的时候也会执行onAnimationEnd,

                if (isUp) {
                    isUp = false;
                    // TODO 这里进行down操作
                    lastY = (int) ev.getY();

                    if (valueAnimator != null && valueAnimator.isRunning())
                        valueAnimator.cancel();

                }


    /**
     * 避免繁琐的判断
     */
    private StateCallBack mCallback;

    public void setStateCallBace(StateCallBack callback) {
        mCallback = callback;
    }

    public interface StateCallBack {
        /**
         * 从下拉刷新到松开刷新的瞬间
         */
        void toLoosenRefresh();

        /**
         * 从松开刷新到下拉刷新的瞬间
         */
        void toRullRefresh();

        /**toLoading
         * 状态为正在刷新
         */
        void toRefreshing();

        /**
         * 状态为正在加载
         */
        void toLoading();

        /**
         * 从下拉刷新拖拽到松开刷新的移动百分比
         *
         * @param percent 0-1
         * @param dY      调用间隔的偏移量
         */
        void dragToLoosen(float percent, int dY);

        /**
         * 头部有高度后移动的百分比
         *
         * @param percent 0-N
         * @param dY      调用间隔的偏移量
         */
        void drag(float percent, int dY);

        /**
         * 停止正在刷新(手动和被动都会执行)
         */
        void stopRefresh();

        /**
         * 停止正在加载(被动执行)
         */
        void stopLoad();

    }

这是回调的接口


    public class Builder {
        private PullRefreshListView mLv;

        public Builder(PullRefreshListView listView) {
            mLv = listView;
        }

        /**
         * 这是头布局
         */
        public Builder setHeaderView(View view) {
            mLv.setHeaderView(view);
            return this;
        }

        /**
         * 这是脚布局
         */
        public Builder setFooterView(View view) {
            mLv.setFooterView(view);
            return this;
        }

        /**
         * 设置动画时间
         */
        public Builder setDuration(int duration) {
            mLv.setDuration(duration);
            return this;
        }

        /**
         * 设置滑动时阻尼系数
         */
        public Builder setDamp(int damp) {
            mLv.setDamp(damp);
            return this;
        }

        /**
         * 关闭加载
         */
        public Builder closeLoading() {
            mLv.closeLoading();
            return this;
        }

        /**
         * 关闭刷新
         */
        public Builder closeRefreshing() {
            mLv.closeRefreshing();
            return this;
        }
    }

}

这是使用了建造者模式,避免方法过多找不着,因此用建造者把不属于listview的方法封装起来,所以调用的时候,只能通过建造者来调用方法。


下面看看activity如何调用。

public class MainActivity extends AppCompatActivity implements PullRefreshListView.StateCallBack{

    private PullRefreshListView lv;
    private View header;
    private View footer;
    private ImageView iv;
    private TextView tv;
    private ProgressBar pb;
    private int refresh;
    private int counts;
    private Adapter a;

    /**
     * listview的建造者,里面封装了额外的方法
     */
    private PullRefreshListView.Builder builder;

开始的地方没啥好说的。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        lv = (PullRefreshListView) findViewById(R.id.lv_pullrefresh);
        builder = lv.new Builder(lv);
        init();
        iv = (ImageView) header.findViewById(R.id.iv_rotate);
        tv = (TextView) header.findViewById(R.id.tv_text);
        pb = (ProgressBar) header.findViewById(R.id.pb);
    }
在这里先找到listview,然后初始化,初始化后再找到头布局里面的一些小控件,


    List<String> list;
    private void init() {
        list = new ArrayList<>();
        for (int i=0;i<20;i++){
            list.add(String.valueOf(counts++)+"  第"+refresh+"次刷新。");
        }
        a = new Adapter(this,list);
        lv.setAdapter(a);
        builder.setHeaderView(header =View.inflate(this,R.layout.header_view,null));
        builder.setFooterView(footer = View.inflate(this,R.layout.footer_view,null));
        lv.setStateCallBace(this);
    }
这里是初始化的地方,留意一下,这里是通过构造者添加头部和脚部的。

        builder.setHeaderView(header =View.inflate(this,R.layout.header_view,null));
        builder.setFooterView(footer = View.inflate(this,R.layout.footer_view,null));

    @Override
    public void toLoosenRefresh() {
        tv.setText("松开刷新");
    }

    @Override
    public void toRullRefresh() {
        tv.setText("下拉刷新");
        iv.setVisibility(View.VISIBLE);
        pb.setVisibility(View.GONE);
    }

    private Handler h ;
    private Runnable r;
    @Override
    public void toRefreshing() {
        tv.setText("正在刷新");
        iv.setVisibility(View.GONE);
        pb.setVisibility(View.VISIBLE);
        h =new Handler();
        h.postDelayed(r =new Runnable() {

            @Override
            public void run() {
                counts = 0;
                list.clear();
                refresh++;
                for (int i=0;i<20;i++){
                    list.add(String.valueOf(counts++)+"  第"+refresh+"次刷新。");
                }
                a.notifyDataSetChanged();
                builder.closeRefreshing();
            }
        },5000);
    }

    @Override
    public void toLoading() {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                for (int i=0;i<20;i++){
                    list.add(String.valueOf(counts++)+"  第"+refresh+"次刷新。");
                }
                a.notifyDataSetChanged();
                builder.closeLoading();
            }
        },2000);
    }

    @Override
    public void dragToLoosen(float percent, int dY) {
        iv.setRotation(180*percent);
    }

    @Override
    public void drag(float percent, int dY) {
    }

    @Override
    public void stopRefresh() {
        h.removeCallbacks(r);
        Toast.makeText(this, "停止刷新可以通过手动滑动(更新状态为非“正在刷新”)停止", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void stopLoad() {
        Toast.makeText(this, "停止加载不允许通过手动滑动停止", Toast.LENGTH_SHORT).show();
    }

然后,可以在回调方法内,实现一些控件的变化,注意一下这个方法,

    @Override
    public void dragToLoosen(float percent, int dY) {
        iv.setRotation(180*percent);
    }
动画效果可以在这里实现,还是比较方便的。


就到这里了,实现了解耦,但写博客的过程发现了一些不足,就是只可以用于打断了,就是说“正在刷新”的时候,如果向下滑动,就应该处理打断逻辑,因为箭头都改变了,向上划的时候也不会滑到“正在刷新”这个字样,所以感觉还是有些不完善的,抽空会修改一下,到时候下拉刷新不允许加载,加载不允许下拉刷新,就当是两个下拉刷新-上拉加载的类吧,一个允许打断,一个不允许打断,哈哈。

所以,如果使用这个自定义的listview,最好还是处理一下打断逻辑吧!!!

下篇:

后篇:自定义(扩展性能强!)的下拉刷新和上拉加载控件



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值