SwipeRefreshLayout及其扩展使用

概述

我们之前使用的比较多的下拉刷新、上拉加载框架有PullToRefreshListView,本文我们来说说SwipeRefreshLayout。SwipeRefreshLayout是Google官方更新的一个Widget,可以实现下拉刷新的效果。该控件继承自ViewGroup在support-v4兼容包下,不过我们需要升级supportlibrary的版本到19.1以上。

官方SwipeRefreshLayout

  • 在竖直滑动时想要刷新页面可以用SwipeRefreshLayout来实现。它通过设置OnRefreshListener来监听界面的滑动从而实现刷新。也可以通过一些方法来设置SwipeRefreshLayout是否可以刷新。如:setRefreshing(true),展开刷新动画。setRefreshing(false),取消刷新动画。setEnable(false)下拉刷新将不可用。
  • 使用这个布局要想达到刷新的目的,需要在这个布局里包裹可以滑动的子控件,如ListView等,并且只能有一个子控件。

主要方法

  • isRefreshing()
    判断当前的状态是否是刷新状态。

  • setColorSchemeResources(int… colorResIds)
    设置下拉进度条的颜色主题,参数为可变参数,并且是资源id,最多设置四种不同的颜色,每转一圈就显示一种颜色。

  • setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener)
    设置监听,需要重写onRefresh()方法,顶部下拉时会调用这个方法,在里面实现请求数据的逻辑,设置下拉进度条消失等等。

  • setProgressBackgroundColorSchemeResource(int colorRes)
    设置下拉进度条的背景颜色,默认白色。

  • setRefreshing(boolean refreshing)
    设置刷新状态,true表示正在刷新,false表示取消刷新。

使用

<android.support.v4.widget.SwipeRefreshLayout
      android:id="@+id/srl"
      android:layout_width="match_parent"
      android:layout_height="match_parent">

      <ListView
          android:id="@+id/lv"
          android:layout_width="match_parent"
          android:layout_height="match_parent"/>

</android.support.v4.widget.SwipeRefreshLayout>
...
// 设置下拉进度的背景颜色,默认就是白色的       
swipeRefreshLayout.setProgressBackgroundColorSchemeResource(android.R.color.white);
// 设置下拉进度的主题颜色        
swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary, R.color.colorPrimaryDark);

// 下拉时触发SwipeRefreshLayout的下拉动画,动画完毕之后就会回调这个方法
        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                // 开始刷新,设置当前为刷新状态,经测试这里可以省略
//                swipeRefreshLayout.setRefreshing(true);

                final Random random = new Random();
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mList.add(0, "我是刷新出来的:" + random.nextInt(100));
                        mAdapter.notifyDataSetChanged();
                        Toast.makeText(FirstActivity.this, "刷新了一条数据", Toast.LENGTH_SHORT).show();
                        // 加载完数据设置为不刷新状态,将下拉进度收起来
                        swipeRefreshLayout.setRefreshing(false);
                    }
                }, 1200);

                // 这个不能写在外边,不然会直接收起来
                //swipeRefreshLayout.setRefreshing(false);
            }
        });

这里写图片描述

扩展SwipeRefreshLayout添加上拉加载

由于谷歌并没有提供上拉加载更多的布局,所以我们只能自己去定义布局实现这个功能。这里通过自定义View继承SwipeRefreshLayout容器,然后添加上拉加载更多的功能。

自定义SwipeRefreshView

/**
 * 自定义View继承SwipeRefreshLayout,添加上拉加载更多的布局属性
 */

public class SwipeRefreshView extends SwipeRefreshLayout {
    private final int mScaledTouchSlop;
    private final View mFooterView;
    private ListView mListView;
    private OnLoadListener mOnLoadListener;
    /**
     * 正在加载状态
     */
    private boolean isLoading;

    public SwipeRefreshView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 填充底部加载布局
        mFooterView = View.inflate(context, R.layout.view_footer, null);
        // 表示控件移动的最小距离,手移动的距离大于这个距离才能拖动控件
        mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // 获取ListView,设置ListView的布局位置
        if (mListView == null) {
            // 判断容器有多少个孩子
            if (getChildCount() > 0) {
                // 判断第一个孩子是不是ListView(所以这个SwipeRefreshView容器只能包裹ListView)
                if (getChildAt(0) instanceof ListView) {
                    // 创建ListView对象
                    mListView = (ListView) getChildAt(0);

                    // 设置ListView的滑动监听
                    setListViewOnScroll();
                }
            }
        }
    }

    /**
     * 在分发事件的时候处理子控件的触摸事件
     * @param ev
     * @return
     */
    private float mDownY, mUpY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 移动的起点
                mDownY = ev.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                // 移动过程中判断时候能下拉加载更多
                if (canLoadMore()) {
                    // 加载数据
                    loadData();
                }
                break;
            case MotionEvent.ACTION_UP:
                // 移动的终点
                mUpY = getY();
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 判断是否满足加载更多条件
     * @return
     */
    private boolean canLoadMore() {
        // 1. 是上拉状态
        boolean condition1 = (mDownY - mUpY) >= mScaledTouchSlop;
        if (condition1) {
            System.out.println("是上拉状态");
        }

        // 2. 当前页面可见的item是最后一个条目
        boolean condition2 = false;
        if (mListView != null && mListView.getAdapter() != null) {
            condition2 = mListView.getLastVisiblePosition() == (mListView.getAdapter().getCount() - 1);
        }

        if (condition2) {
            System.out.println("是最后一个条目");
        }
        // 3. 正在加载状态
        boolean condition3 = !isLoading;
        if (condition3) {
            System.out.println("不是正在加载状态");
        }
        return condition1 && condition2 && condition3;
    }

    /**
     * 处理加载数据的逻辑
     */
    private void loadData() {
        System.out.println("加载数据...");
        if (mOnLoadListener != null) {
            // 设置加载状态,让布局显示出来
            setLoading(true);
            mOnLoadListener.onLoad();
        }

    }

    /**
     * 设置加载状态,是否加载传入boolean值进行判断
     *
     * @param loading
     */
    public void setLoading(boolean loading) {
        // 修改当前的状态
        isLoading = loading;
        if (isLoading) {
            // 显示布局
            mListView.addFooterView(mFooterView);
        } else {
            // 隐藏布局
            mListView.removeFooterView(mFooterView);

            // 重置滑动的坐标
            mDownY = 0;
            mUpY = 0;
        }
    }


    /**
     * 设置ListView的滑动监听
     */
    private void setListViewOnScroll() {
        mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {
                // 移动过程中判断时候能下拉加载更多
                if (canLoadMore()) {
                    // 加载数据
                    loadData();
                }
            }

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

    /**
     * 上拉加载的接口回调
     */

    public interface OnLoadListener {
        void onLoad();
    }

    public void setOnLoadListener(OnLoadListener listener) {
        this.mOnLoadListener = listener;
    }
}

使用

    <!--自定义View实现SwipeRefreshLayout,添加上拉加载更多的功能-->
    <com.hx.pullrefresh.SwipeRefreshView
        android:id="@+id/srl"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/lv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    </com.hx.pullrefresh.SwipeRefreshView>
...
...
// 下拉时触发SwipeRefreshLayout的下拉动画,动画完毕之后就会回调这个方法
        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                // 开始刷新,设置当前为刷新状态,经测试这里可以省略
//                swipeRefreshLayout.setRefreshing(true);

                final Random random = new Random();
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mList.add(0, "我是刷新出来的:" + random.nextInt(100));
                        mAdapter.notifyDataSetChanged();
                        Toast.makeText(FirstActivity.this, "刷新了一条数据", Toast.LENGTH_SHORT).show();
                        // 加载完数据设置为不刷新状态,将下拉进度收起来
                        swipeRefreshLayout.setRefreshing(false);
                    }
                }, 1200);

                // 这个不能写在外边,不然会直接收起来
                //swipeRefreshLayout.setRefreshing(false);
            }
        });

        // 设置下拉加载更多
        swipeRefreshView.setOnLoadListener(new SwipeRefreshView.OnLoadListener() {
            @Override
            public void onLoad() {
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        // 添加数据
                        for (int i = 30; i < 35; i++) {
                            mList.add("我是上拉加载出来的" + i);
                            mAdapter.notifyDataSetChanged();
                        }
                        Toast.makeText(SecondActivity.this, "加载了" + 5 + "条数据", Toast.LENGTH_SHORT).show();
                        // 加载完数据设置为不加载状态,将加载进度收起来
                        swipeRefreshView.setLoading(false);
                    }
                }, 1200);
            }
        });

这里写图片描述

自定义View实现下拉刷新上拉加载

上面的提到的SwipeRefreshLayout和SwipeRefreshView有很多局限性,譬如下拉的动画效果很难更改,这里我们用自定义View来实现下拉刷新上拉加载的框架。

原理

其实是一个ViewGroup,通过对手势的处理,使子控件实现拉动的动画效果,并再加上两个子控件,上拉的loading和下拉的loading(把loading用控件来封装可以很方便的更改动画,真是贴心~),在处理手势拉动的时候,通知他们显示出对应的效果。代码很长,有很多小细节需要注意,在这里我只介绍几个关键的位置,源代码会发在文章的最后。

拖拽弹力效果

大家可以看到,拖拽的时候,是有个弹力效果的,也就是说当拖拽的距离大于某个值,拖动的位移就会慢慢减小,最后会变得拖不动看上去有点酷炫,其实实现起来就是高中数学知识啦,看下关键代码

final float scrollTop = yDiff * DRAG_RATE;
float originalDragPercent = scrollTop / mTotalDragDistance;
mDragPercent = Math.min(1f, Math.abs(originalDragPercent));//拖动的百分比
float extraOS = Math.abs(scrollTop) - mTotalDragDistance;//弹簧效果的位移
float slingshotDist = mSpinnerFinalOffset;
//当弹簧效果位移小余0时,tensionSlingshotPercent为0,否则取弹簧位移于总高度的比值,最大为2
float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist);
//对称轴为tensionSlingshotPercent = 2的二次函数,0到2递增
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2)) * 2f;
float extraMove = (slingshotDist) * tensionPercent * 2;
targetY = (int) ((slingshotDist * mDragPercent) + extraMove);

解释下几个参数:
yDiff —— 根据手势算出的滑动位移
scrollTop —— yDiff乘上一个固定的比率(现在是0.5),可以用来调节“弹簧”的弹性系数
mTotalDragDistance —— 当进度显示100%时的位移
originalDragPercent —— 根据scrollTop与mTotalDragDistance的比值
mDragPercent —— 由于originalDragPercent可能大于1,所以mDragPercent才是拖动的百分比
slingshotDist —— 超过100%后可以被允许拖动的最大距离的二分之一,也是一个常数(现在值 = mSpinnerFinalOffset = mTotalDragDistance)
extraMove —— 弹力距离
targetY —— 想要移动到的目标位置

啊!? 被发现有两个个参数没解释,哈哈,至于tensionSlingshotPercent和tensionPercent,就是弹力效果的关键啦:

extraOS 在scrollTop>=0时,是从-mTotalDragDistance开始线性递增的,在scrollTop = mTotalDragDistance时,extraOS = 0

tensionSlingshotPercent 在scrollTop从0到mTotalDragDistance阶段,始终为0,在smTotalDragDistance到3*mTotalDragDistance阶段,线性递增,之后一直为2

extraMove 的变化同tensionSlingshotPercent

tensionPercent 是个二次函数,同样映射到scrollTop的变化,在scrollTop从0到mTotalDragDistance阶段,始终为0,在mTotalDragDistance到3mTotalDragDistance阶段,二次函数递增,在3mTotalDragDistance之后恒为0.5

而targetY,在scrollTop从0到mTotalDragDistance阶段,也就是mDragPercent从0到1,extramMove始终为0,然后二次函数递增,在scrollTop > 3*mTotalDragDistance 变为恒值

总结下来targetY相对于scrollTop对函数图像如下:
这里写图片描述
其实看到这个图,我想大家就基本上知道具体出来的效果了,再后面就是一些位移的操作,大家可以看文章最后面源码,值得注意的是,之前都是分析scrollTop > 0 的情况,也就是下拉操作,上拉targetY要取负的,而且上拉下拉都是走这套逻辑,所以计算extraOS的时候scrollTop加上了绝对值

loading动画

在前文中我们说过,loading效果其实是交给两个子控件完成的,这样有利于更改loading的动画效果。那么,具体是怎么实现的呢?

在代码中我们可以看到如下几个对象,其中mRefreshView和mLoadView就是我们所说的loading控件,但它们只是一个容器,只控制显隐,而具体的动画实现是交给对应的mRefreshDrawable和mLoadDrawable;

...
mRefreshView = new ImageView(context);
setRefreshDrawable(new PlaneDrawable(getContext(), this));
mRefreshView.setVisibility(GONE);
addView(mRefreshView, 0);

mLoadView = new ImageView(context);
setLoadDrawable(new PlaneLoadDrawable(getContext(), this));
mLoadView.setVisibility(GONE);
addView(mLoadView, 0);
...
    public void setRefreshDrawable(RefreshDrawable drawable) {
        setRefreshing(false);
        mRefreshDrawable = drawable;
        mRefreshView.setImageDrawable(mRefreshDrawable);
    }

setRefreshDrawable方法走进去,发现其实就是把一个Drawable对象赋给mRefreshView,那我们来看一下传入的参数PlaneDrawable,这个是我写的那个火箭飞行的动画效果,代码很简单,但是我们发现PlaneDrawable是继承了一个叫RefreshDrawable的类,它才是将动画效果解耦于PullRefreshLayout的关键

public abstract class RefreshDrawable extends Drawable implements Drawable.Callback, Animatable {

    private PullRefreshLayout mRefreshLayout;

    public RefreshDrawable(Context context, PullRefreshLayout layout) {
        mRefreshLayout = layout;
    }

    public Context getContext(){
        return mRefreshLayout != null ? mRefreshLayout.getContext() : null;
    }

    public PullRefreshLayout getRefreshLayout(){
        return mRefreshLayout;
    }

    public abstract void setPercent(float percent);
    public abstract void setColorSchemeColors(int[] colorSchemeColors);

    public abstract void offsetTopAndBottom(int offset);

    @Override
    public void invalidateDrawable(Drawable who) {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.invalidateDrawable(this);
        }
    }

    @Override
    public void scheduleDrawable(Drawable who, Runnable what, long when) {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.scheduleDrawable(this, what, when);
        }
    }

    @Override
    public void unscheduleDrawable(Drawable who, Runnable what) {
        final Callback callback = getCallback();
        if (callback != null) {
            callback.unscheduleDrawable(this, what);
        }
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public void setAlpha(int alpha) {
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
    }
}

它是一个抽象类,抽象方法有

public abstract void setPercent(float percent);
public abstract void setColorSchemeColors(int[] colorSchemeColors);
public abstract void offsetTopAndBottom(int offset);

为了代码简洁,主题颜色我没有用,还剩下setPercent和offsetTopAndBottom,这两个方法会分别在PullRefreshLayout里面拉动的进度改变和被拉动目标控件位移变化时被调用。这样,我们想更改动画效果就简单了,直接写一个类,继承至RefreshDrawable,然后在对应的setPercent和offsetTopAndBottom里面做出相应的动画数据改变,就如PlaneDrawable那样,然后再调用PullRefreshLayout的setRefreshDrawable或setLoadDrawable方法进行设值,是不是很方便?

同时显示两个动画处理

在添加上拉加载效果时,我发现,假如你先下拉然后在不松手的情况下再上拉,那就会同时出现两个loading动画,然而此时list还不在底部,也就是不应该显示上拉loading效果的。这是由于在拉动时,判断子控件是否可以向上滑动的那个方法会返回false,那么如何解决这个问题呢?用一个变量mLastDirection储存本次动画的,如果下次的动画与本次不同,则不进行下次动画,并在ACTION_UP和ACTION_CANCEL时,判断被拉动目标控件的top位置

...
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                final float y = MotionEventCompat.getY(ev, pointerIndex);
                final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                mIsBeingDragged = false;
                /**记录mLastDirection**/
                if (overscrollTop > mTotalDragDistance && mCurrentOffsetTop > mTotalDragDistance) {
                    setRefreshing(true, true);
                    mLastDirection = RefreshMode.PULL_FROM_START;
                }
                else if (Math.abs(overscrollTop) > mTotalDragDistance && mCurrentOffsetTop < -mTotalDragDistance){
                    setLoading(true);
                    mLastDirection = RefreshMode.PULL_FROM_END;
                }
                else {
                    mRefreshing = false;
                    animateOffsetToStartPosition();
                }

使用

    <com.hx.pullrefresh.view.PullRefreshLayout
        android:id="@+id/swipeRefreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </com.hx.pullrefresh.view.PullRefreshLayout>
        final PullRefreshLayout layout = (PullRefreshLayout) findViewById(R.id.swipeRefreshLayout);
        layout.setOnRefreshListener(new PullRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                layout.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        /**停止刷新**/
                        layout.setRefreshing(false);
                    }
                }, 1000);
            }
        });
        layout.setOnLoadListener(new PullRefreshLayout.OnLoadListener() {
            @Override
            public void onLoad() {
                layout.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        /**停止加载**/
                        layout.setLoading(false);
                    }
                }, 1000);
            }
        });

这里写图片描述

Demo下载地址

SmartRefreshLayout

SmartRefreshLayout的目标是打造一个强大,稳定,成熟的下拉刷新框架,并集成各种的炫酷、多样、实用、美观的Header和Footer。

一个功能超级强大的刷新框架,强烈推荐,具体参见链接文章,Android智能下拉刷新框架-SmartRefreshLayout

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值