Android-下拉刷新库

来源:互联网 发布:淘宝不能搜网盘 编辑:程序博客网 时间:2024/05/29 03:44

前言

入职接近半个多月,有几天空闲,所以想着能不能自己实现一个库来练练手,因为之前一直想要实现下拉刷新的功能,因此就有了这样一个自制的下拉刷新库——RefreshWidgetLib.

关于下拉刷新

下拉刷新,作为一个几乎每个应用都会出现的一种控件,不言而喻,它对于提高用户体验有着很重要的作用,而且也已经成为了人们习惯的一种操作。说起下拉刷新这种设计,最早的引入者是在2008年上线的Tweetie,Tweetie引入了如今随处可见的“下拉刷新”设计,不仅有多达数百款App Store应用使用这种设计,就连苹果的Mail应用也采纳了这一元素。现在我们在各大平台的应用上都可以看见这一元素,而且样式丰富多彩,体验更是很不错。当然后期也出现一些质疑下拉刷新这种设计的不人性的看法,但是对于我来说,我认为这种设计不仅成为了用户习惯式的一种需求,而且它也有创造更多优质体验的可能。

关于实现思路

下拉刷新,从以前到现在,网上已经有很多开源的下拉刷新库,而且,google官方后期也推出一个下拉刷新的控件,所以可以说,我们可以使用的资源是很丰富的。那么还有什么意义去实现它呢,我觉得即使我们有很多轮子可以用,如果自己有机会,也需要学会去造轮子。

回到实现思路的话题,假如现在有一个ListView,我们想要让它具有下拉刷新的功能,那么大的方向有两个:

1)继承ListView,利用ListView本身自带的addHeaderView()方法,通过监听滚动事件,实现下拉刷新的效果。

2)自定义一个ViewGroup,依次添加HeaderView,ListView和FooterView,通过重写触摸事件onInterceptTouchEvent()方法拦截触摸事件,重写onTouchEvent()方法处理相关逻辑。
外部ViewGroup的实例不同,相对应的,下拉刷新控件的效果也不同,这里的ViewGroup可以是LinearLayout,也可以是FrameLayout.

关于本篇的实现思路

从上面我们可以看出实现思路是可以有很多选择的,本篇博客我的实现思路是,使用LinearLayout布局包裹HeaderView,ListView,FooterView,其他的实现思路我会在后面持续实现更新。

接下来是具体分析:

首先来看RefreshWidget的结构,外部布局为LinearLayout,排列方式为Vertical, 内部三个视图依次为HeaderView,ContentView,FooterView,这里的ContentView可以为ListView,也可以是GridView,也可以是SrcollView,取决于我们子类对于ContentView的实现方式。

这里以ContentView实例化为ListView作为例子,最初结构图如下:

这里写图片描述

可以看到,我们在LinearLayout中依次添加了HeaderView,ListView和FooterView,但是我们希望默认情况下,也就是没有上拉或者下拉的时候,HeaderView和FooterView隐藏起来。这时候,我们可以通过设置topMargin或者bottomMargin来使得HeaderView和FooterView被隐藏起来,如下:

这里写图片描述

然后,接下来就是重写触摸事件处理逻辑的部分,我们重写onInterceptTouchEvent()方法。因为LinearLayout内部的View仍然需要处理触摸事件,例如ListView仍然需要处理点击Item,处理ListView自身的滚动事件,所以我们不能完全拦截触摸事件,也不能只重写onTouchEvent()方法,要合理适当的拦截触摸事件。

所以正确的做法是,在onInterceptTouchEvent()方法中判断,当传递进来的是ACTION_MOVE事件,我们需要判断此时是否处于ListView的最顶部或者处于ListView的最底部,如果是最顶部或者最底部,则对触摸事件进行拦截,否则不做拦截,仍然把触摸事件交给子View(这里也就是ListView)去处理。

    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        switch (ev.getAction()) {            case MotionEvent.ACTION_DOWN:                mDownY = ev.getRawY();                break;            case MotionEvent.ACTION_MOVE:                mMoveY = ev.getRawY();                /* 如果不属于触摸滑动范围,则跳出 */                if (Math.abs(mDownY - mMoveY) < mTouchSlop) return false;                /* 如果处于顶部,且继续下拉,进入下拉刷新状态,同时拦截触摸事件 */                if (mCurrentStatus == STATUS_NORMAL && isReachHeader() && mMoveY - mDownY > 0 &&                         mRefreshEnabled) {                    mCurrentStatus = STATUS_REFRESH;                    mHeaderView.onRefresh(0);                    return true;                } else if (mCurrentStatus == STATUS_NORMAL && isReachFooter() && mMoveY - mDownY                         < 0 && mLoadMoreEnabled) {                    /* 如果处于底部,且继续上拉,进入上拉加载更多状态,同时拦截触摸事件 */                    mCurrentStatus = STATUS_LOAD_MORE;                    mFooterView.onLoadMore(0);                    /* 加上这一句,可以使得ContentView在上拉的过程中保持滑到最底部的状态 */                    makeContentViewToFooter();                    return true;                }                break;        }        /* 其他情况不拦截,默认返回false */        return super.onInterceptTouchEvent(ev);    }

接下来,假如我们已经判断出手指正在执行下拉刷新的操作,那么我们将触摸事件拦截,交由onTouchEvent()处理,通过设置topMargin和bottomMargin,使得HeaderView重新进入手机的可见区域

case MotionEvent.ACTION_MOVE:                mMoveY = event.getRawY();                /* 下拉刷新状态 且正在向下滑动 */                if ((mCurrentStatus == STATUS_REFRESH || mCurrentStatus == STATUS_RELEASE_TO_REFRESH)                        && mMoveY - mDownY >= 0 ) {                    if (mMoveY - mDownY > mHeaderHeight * mHeaderPullProportion) {                        mCurrentStatus = STATUS_RELEASE_TO_REFRESH;                        mHeaderView.onReleaseToRefresh();                        setHeaderTopMargin(0);                        setHeaderBottomMargin((int) (mMoveY - mDownY - mHeaderHeight * mHeaderPullProportion));                    } else {                        mCurrentStatus = STATUS_REFRESH;                        mHeaderView.onRefresh((mMoveY- mDownY) / ((float)mHeaderHeight * mHeaderPullProportion));                        setHeaderTopMargin(-mHeaderHeight + (int) ((mMoveY - mDownY) / mHeaderPullProportion));                        setHeaderBottomMargin(0);                    }                } 

当松手之后,我们还需要一个回弹的动作,这里我们用属性动画来完成

    /**     * 下拉刷新任务     */    private void headerRefreshTask() {        final int value = mHeaderLayoutParams.bottomMargin;        mMainThreadHandler.post(new Runnable() {            @Override            public void run() {                ValueAnimator valueAnimator = ValueAnimator.ofFloat(value,                        0);                valueAnimator.setInterpolator(new LinearInterpolator());                valueAnimator.setDuration(HEADER_REFRESH_TIME);                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {                    @Override                    public void onAnimationUpdate(ValueAnimator animation) {                        float value = (float) animation.getAnimatedValue();                        setHeaderBottomMargin((int) value);                    }                });                valueAnimator.start();            }        });    }

关于我的下拉刷新库-RefreshWidgetLib

github地址:https://github.com/82367825/RefreshWidget

上面介绍完基本原理之后,让我们来看看成果,先来看看效果:

这里写图片描述

这里写图片描述

工程库库的UML图,主要的逻辑设计都在基类BaseRefreshWidget中实现了,不同的下拉刷新控件都是继承自这个基类。

这里写图片描述

如何使用RefreshWidgetLib

举例RefreshListViewWidget的使用,就如同普通的ListView一样使用

在布局文件中添加

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"              android:layout_width="match_parent"              android:layout_height="match_parent">    <com.zero.refreshwidgetlib.widget.RefreshListViewWidget        android:id="@+id/refresh_list"        android:layout_width="match_parent"        android:layout_height="match_parent">    </com.zero.refreshwidgetlib.widget.RefreshListViewWidget></LinearLayout>

然后在Activity中添加数据

    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_listview);        initData();        mRefreshListViewWidget = (RefreshListViewWidget) findViewById(R.id.refresh_list);        mRefreshListViewWidget.setAdapter(new ListViewAdapter());        mRefreshListViewWidget.setRefreshListener(new RefreshListener() {            @Override            public void onRefresh() {                refreshData();            }            @Override            public void onLoadMore() {                loadMoreData();            }        });        mRefreshListViewWidget.setOnItemClickListener(new AdapterView.OnItemClickListener() {            @Override            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {                Toast.makeText(ListViewDemoActivity.this, "click " + position + " item",                         Toast.LENGTH_SHORT).show();            }        });    }

当数据加载或者刷新完成,我们可以调用

mRefreshListViewWidget.completeRefresh();

我们也可以禁用上拉或者下拉

    /**     * 设置下拉刷新功能是否可用     * @param enabled     */    void setRefreshEnabled(boolean enabled);    /**     * 设置上拉加载功能是否可用     * @param enabled     */    void setLoadMoreEnabled(boolean enabled);

当然,这些用法都是和系统的刷新控件以及其他第三方下拉刷新控件很类似的,可能看起来就没什么新意,但是还是有一些补充的地方,就是HeaderView和FooterView的拓展实现。

实现自己的HeaderView和FooterView

默认情况下,如果不添加自定义的HeaderView或者FooterView,RefreshWidgetLib会默认帮你生成一个只显示文字效果的HeaderView或者FooterView.

假如我们想要实现自己的HeaderView,当下拉的时候,加入一些自己的效果,让控件更加好看,也是很容易的。我们只需要继承BaseHeader,然后实现以下三个已经嵌入代码框架里的方法就可以了。

onRefresh()是当RefreshListViewWidget正在下拉操作的时候被调用;
onReleaseToRefresh()是当RefreshListViewWidget已经下拉超过指定范围被调用;
onRefreshIng()则是当RefreshListViewWidget松手之后被调用。

percent是一个关键的参数,它表示下拉的比例大小,通常情况下为:下拉距离 / HeaderView高度

    /**     * 正在下拉刷新     * @param percent 下拉完成比例     */    void onRefresh(float percent);    /**     * 松手刷新     */    void onReleaseToRefresh();    /**     * 正在刷新     */    void onRefreshIng();

举个例子,比如我想实现上面gif图里的波浪下拉HeaderView,那么就像上面说的,继承BaseHeader,实现三个方法。

package com.zero.refreshwidgetlib.header;import android.animation.ValueAnimator;import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Path;import android.graphics.PointF;import android.os.Handler;import android.util.AttributeSet;import android.view.Gravity;import android.view.animation.LinearInterpolator;import android.widget.TextView;import com.zero.refreshwidgetlib.utils.DrawUtils;/** * Anim HeaderView * @author linzewu * @date 16-6-29 */public class HeaderAnimView extends BaseHeader{    private static final String TAG = "HeaderAnimView";    private static final int STATUS_NORMAL = 0;    private static final int STATUS_START_TO_REFRESH = 1;    private static final int STATUS_RELEASE_TO_REFRESH = 2;    private static final int STATUS_REFRESH_ING = 3;    private int mCurrentStatus = STATUS_NORMAL;    private static final float MAX_HEIGHT = 100;    private static final int DEFAULT_COLOR = 0x4400AAFF;    private int mColor = DEFAULT_COLOR;    private Paint mPaint;    private Path mPath;    protected float mWidth;    protected float mHeight;    protected boolean mHasInit = false;    private Handler mMainThreadHandler = new Handler();    /**     * 正在刷新TextView     */    private TextView mRefreshingView;    /**     * 下拉曲线的变量点     */    private PointF mLeftPoint,mRightPoint,mControlPoint;    /**     * 刷新波浪的变量点     */    private PointF mPointF1,mPointF2,mPointF3,mPointF4,mPointF5;    public HeaderAnimView(Context context) {        super(context);        init();    }    public HeaderAnimView(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public HeaderAnimView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        setMeasuredDimension(widthMeasureSpec, DrawUtils.dip2px(getContext(), MAX_HEIGHT));    }    @Override    protected void onSizeChanged(int w, int h, int oldW, int oldH) {        super.onSizeChanged(w, h, oldW, oldH);        if (!mHasInit) {            mWidth = w;            mHeight = h;        }    }    private void init() {        mPaint = new Paint();        mPaint.setAntiAlias(true);        mPaint.setStyle(Paint.Style.FILL);        mPaint.setColor(mColor);        mRefreshingView = new TextView(getContext());        mRefreshingView.setGravity(Gravity.CENTER);        mPath = new Path();        mLeftPoint = new PointF();        mRightPoint = new PointF();        mControlPoint = new PointF();        mPointF1 = new PointF();        mPointF2 = new PointF();        mPointF3 = new PointF();        mPointF4 = new PointF();        mPointF5 = new PointF();    }    @Override    protected void dispatchDraw(Canvas canvas) {        super.dispatchDraw(canvas);        mPath.reset();        if (mCurrentStatus == STATUS_START_TO_REFRESH || mCurrentStatus ==                STATUS_RELEASE_TO_REFRESH) {            mLeftPoint.x = 0;            mLeftPoint.y = mHeight - mHeight * mPercent;            mRightPoint.x = mWidth;            mRightPoint.y = mHeight - mHeight * mPercent;            mControlPoint.x = mWidth / 2;            mControlPoint.y = mHeight + mHeight * mPercent * 0.9f;            mPath.moveTo(mLeftPoint.x, mLeftPoint.y);            mPath.quadTo(mControlPoint.x, mControlPoint.y, mRightPoint.x, mRightPoint.y);            mPath.moveTo(mLeftPoint.x, mLeftPoint.y);            canvas.drawPath(mPath, mPaint);        }  else if (mCurrentStatus == STATUS_REFRESH_ING) {            /* 绘制波浪 */            mPath.moveTo(mPointF1.x, mPointF1.y);            mPath.quadTo((mPointF1.x + mPointF2.x) / 2, mPointF2.y + 20, mPointF2.x, mPointF2.y);            mPath.quadTo((mPointF2.x + mPointF3.x) / 2, mPointF3.y - 20, mPointF3.x, mPointF3.y);            mPath.quadTo((mPointF3.x + mPointF4.x) / 2, mPointF4.y + 20, mPointF4.x, mPointF4.y);            mPath.quadTo((mPointF4.x + mPointF5.x) / 2, mPointF5.y - 20, mPointF5.x, mPointF5.y);            mPath.lineTo(mPointF5.x, 0);            mPath.lineTo(mPointF1.x, 0);            mPath.lineTo(mPointF1.x, mPointF1.y);            canvas.drawPath(mPath, mPaint);        }    }    @Override    public void onRefresh(float percent) {        super.onRefresh(percent);        this.mCurrentStatus = STATUS_START_TO_REFRESH;        this.mPercent = percent;        invalidate();    }    @Override    public void onReleaseToRefresh() {        super.onReleaseToRefresh();        this.mCurrentStatus = STATUS_RELEASE_TO_REFRESH;        this.mPercent = 1;        invalidate();    }    @Override    public void onRefreshIng() {        super.onRefreshIng();        this.mCurrentStatus = STATUS_REFRESH_ING;        drawWave();    }    /**     * draw the wave     */    private void drawWave() {        mPointF1.x = -mWidth;        mPointF1.y = 0.3f * mHeight;        mMainThreadHandler.post(new Runnable() {            @Override            public void run() {                ValueAnimator valueAnimator = ValueAnimator.ofFloat(-mWidth, 0);                valueAnimator.setInterpolator(new LinearInterpolator());                valueAnimator.setDuration(1500);                valueAnimator.setRepeatMode(ValueAnimator.RESTART);                valueAnimator.setRepeatCount(ValueAnimator.INFINITE);                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {                    @Override                    public void onAnimationUpdate(ValueAnimator animation) {                        mPointF1.x = (float) animation.getAnimatedValue();                        mPointF1.y = 0.3f * mHeight;                        mPointF2.x = mPointF1.x + mWidth * 0.5f;                        mPointF2.y = mPointF1.y;                        mPointF3.x = mPointF1.x + mWidth;                        mPointF3.y = mPointF1.y;                        mPointF4.x = mPointF1.x + mWidth * 1.5f;                        mPointF4.y = mPointF1.y;                        mPointF5.x = mPointF1.x + mWidth * 2;                        mPointF5.y = mPointF1.y;                        invalidate();                    }                });                valueAnimator.start();            }        });    }}

通过继承BaseHeader,我们实现了自己的HeaderView,然后我们再调用添加,同样的,FooterView也一样。

mRefreshListViewWidget.addHeaderView(new HeaderAnimView(ListViewDemoActivity.this));mRefreshListViewWidget.addFooterView(new FooterAnimView(ListViewDemoActivity.this));

其他下拉刷新控件

通过这个库,我们可以轻松自定义自己想要的效果的headerView和footerView,也可以实现让自己希望的控件具备下拉刷新的功能。上面提到RefreshListViewWidget使得ListView具备了下拉刷新的功能,当然也可以让GridView具备下拉刷新的效果,还可以让ScrollView具备下拉刷新的效果。

哈哈,当然前提是,目标View本身像ListView、GridView等具有滚动的能力。

怎么做呢,继承基类BaseRefreshWidget,然后实现几个关键的方法,以ScrollView为例子:

    /**     * 滑动到头部     * @return     */    protected boolean isReachHeader(){        return mContentView.getScrollY() == 0;    }    /**     * 滑动到底部     * @return     */    protected boolean isReachFooter(){        View contentView = ((ScrollView)mContentView).getChildAt(0);        return contentView.getMeasuredHeight() <= mContentView.getScrollY() + mContentView.getHeight();    }    @Override    protected View getContentView() {        return new ScrollView(getContext());    }    /**     * 使得ContentView保持滚到底部     */    @Override    protected void makeContentViewToFooter() {        View contentView = ((ScrollView)mContentView).getChildAt(0);        int realHeight = contentView.getMeasuredHeight();        mContentView.scrollTo(0, realHeight - mContentView.getHeight());    }    /**     * 使得ContentView不再保持滚到底部     */     @Override    protected void makeContentViewRestore() {    }

具体更多的代码细节可以看我的源码,也是上面提到的我的工程源码:

https://github.com/82367825/RefreshWidget

这个库算是我第一个完全靠自己思考和编码的一个工程库,可能会有很多不如人意或者错漏的地方,以后我也会不断地更新和维护这个工程,希望做得更好。

如有疑问,欢迎指正~

2 0
原创粉丝点击