实现RecyclerView下拉刷新的功能,网上有很多文章,但大多文章都是将RecyclerView外面套了一层SwipRefreshLayout以此来达到下拉刷新的目的!个人觉得这种方式不太优雅,于是通过查找资料及阅读源码,找到了一个比较优雅的方式实现RecyclerView的下拉刷新,本文将带你以不一样的方式实现RecyclerView的下拉刷新。
从基础入手
万丈高楼平地起,做什么事都不是一蹴而就的,再伟大的工程也是一点点完成的。这里就先从简单的入手,先为RecyclerView添加头部。
为RecyclerView添加头部View
我们都知道实现RecyclerView的数据和视图的绑定是通过继承RecyclerView.Adapter类,同样,为RecyclerView添加头部也是需要继承RecyclerView.Adapter类,然后,在子类里面做文章。在添加头部之前有必要先了解一下RecyclerView.Adapter的几个常用的方法,分别为以下几个方法
onCreateViewHolder(ViewGroup parent, int viewType)
getItemViewType(int position)
onBindViewHolder(MyViewHolder holder, int position)
getItemCount()
使用过ListView的都知道,为了优化ListView的性能,我们在写Adapter时需要自己写一个ViewHolder类来重用ListView中的item视图,而在为RecyclerView实现Adapter时,强制我们要有一个ViewHolder。
onCreateViewHolder方法就是用来创建新View;
getItemViewType方法是可以根据不同的position可以返回不同的类型;
onBindViewHolder是将数据与视图绑定;
getItemCount方法就是获得需要显示数据的总数。
了解了Adapter中的几个方法,下面就利用这几个方法为RecyclerView添加头部View,下面上代码
@Override public RefreshViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { //这里的viewType即为getItemViewType返回的type if (viewType == TYPE_REFRESH_HEADER) { return new RefreshViewHolder(mInflater.inflate(R.layout.refresh_header_item, parent, false)); } return new RefreshViewHolder(mInflater.inflate(R.layout.normal_item, parent, false)); } @Override public void onBindViewHolder(RefreshViewHolder holder, int position) { } @Override public int getItemCount() { return mLists.size(); } @Override public int getItemViewType(int position) { //当position位置为0时,返回为头部的类型 if (position == 0) { return TYPE_REFRESH_HEADER; } return TYPE_NORMAL; }
可以看出,在onCreateViewHolder方法中,为不同的viewType设置了不同的类型,即为RecyclerView添加了头部,看下效果图
好了,现在已经为RecyclerView添加了头部,下一步就是将头部变成刷新的View。
添加下拉刷新的View
现在将头部View修改一下,直接上代码
<?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="bottom"> <RelativeLayout android:id="@+id/header_content" android:layout_width="match_parent" android:layout_height="80dp" android:paddingTop="10dip"> <TextView android:id="@+id/tvRefreshStatus" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/pullRefresh" android:textColor="#B5B5B5" /> <ImageView android:id="@+id/ivHeaderArrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="35dp" android:layout_marginRight="10dp" android:layout_toLeftOf="@id/tvRefreshStatus" android:src="@drawable/ic_pulltorefresh_arrow" /> </RelativeLayout> </LinearLayout>
好了,看下效果图
现在这个效果显然不是我们想要的,我们想要的效果是在正常状态下头部的下拉刷新不可见,当下拉到一定程度再显示。
这里有两点需要注意:
- 在正常的状态下,下拉刷新的头部是不可见的;
- 当下拉到一定程度再将头部刷新显示出来。
现在,来实现正常状态下的效果,正常状态下不可见,这时可以将头部刷新的View高度设置为0,下面看下代码
public ArrowRefreshHeader(Context context) { this(context,null); } public ArrowRefreshHeader(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public ArrowRefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init();//初始化视图 } private void init() { LinearLayout.LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); layoutParams.setMargins(0, 0, 0, 0); this.setLayoutParams(layoutParams); this.setPadding(0, 0, 0, 0); //将refreshHeader高度设置为0 mContentLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.refresh_header_item, null); addView(mContentLayout, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0)); //初始化控件 mArrowImageView = findViewById(R.id.ivHeaderArrow); mStatusTextView = findViewById(R.id.tvRefreshStatus); mProgressBar = findViewById(R.id.refreshProgress); //初始化动画 mRotateUpAnim = new RotateAnimation(0.0f, -180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateUpAnim.setDuration(200); mRotateUpAnim.setFillAfter(true); mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateDownAnim.setDuration(200); mRotateDownAnim.setFillAfter(true); //将mContentLayout的LayoutParams高度和宽度设为自动包裹并重新测量 measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); mMeasuredHeight = getMeasuredHeight();//获得测量后的高度 }
这里使用了自定义View,写了一个ArrowRefreshHeader继承至LinearLayout,在构造方法中将头部刷新的View进行了初始化,这里这句代码是关键
mContentLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.refresh_header_item, null); addView(mContentLayout, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0));
可以看到,这里将头部刷新的View的高度设置成了0,这样,就实现了在正常状态下,头部刷新不显示的效果。将RefreshHeaderAdapter的onCreateViewHolder方法修改一下,如下
@Override public RefreshViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == TYPE_REFRESH_HEADER) { // return new RefreshViewHolder(mInflater.inflate(R.layout.refresh_header_item, parent, false)); return new RefreshViewHolder(new ArrowRefreshHeader(mContext)); } return new RefreshViewHolder(mInflater.inflate(R.layout.normal_item, parent, false)); }
再看下效果
成功实现在正常状态下不显示头部刷新的效果,下面继续实现第2步,当下拉到一定程度进行显示。既然这里有下拉,肯定涉及到了手势监听,自然是需要一个类继承自RecyclerView,然后重写onTouchEvent方法,下面看代码
@Override public boolean onTouchEvent(MotionEvent e) { if (mLastY == -1) { mLastY = e.getRawY(); } switch (e.getAction()) { case MotionEvent.ACTION_DOWN: mLastY = e.getRawY(); sumOffSet = 0; break; case MotionEvent.ACTION_MOVE: float deltaY = (e.getRawY() - mLastY) / 2;//为了防止滑动幅度过大,将实际手指滑动的距离除以2 mLastY = e.getRawY(); sumOffSet += deltaY;//计算总的滑动的距离 if (isOnTop() && !mRefreshing) { mRefreshHeader.onMove(deltaY, sumOffSet);//接口回调,移动刷新的头部View if (mRefreshHeader.getVisibleHeight() > 0) { return false; } } break; default: mLastY = -1; // reset if (isOnTop()&& !mRefreshing) { if (mRefreshHeader.onRelease()) { //手指松开 } } break; } return super.onTouchEvent(e); }
这部分代码就是获取手指滑动的距离,然后利用接口回调,将下拉的距离传递到自定义的View中,让头部刷新的View改变距离及改变状态。先看下定义的接口,接口中主要就是集中刷新的状态以及代表各个状态的变量,代码如下
public interface IRefreshHeader { int STATE_NORMAL = 0;//正常状态 int STATE_RELEASE_TO_REFRESH = 1;//下拉的状态 int STATE_REFRESHING = 2;//正在刷新的状态 int STATE_DONE = 3;//刷新完成的状态 void onReset(); /** * 处于可以刷新的状态,已经过了指定距离 */ void onPrepare(); /** * 正在刷新 */ void onRefreshing(); /** * 下拉移动 */ void onMove(float offSet, float sumOffSet); /** * 下拉松开 */ boolean onRelease(); /** * 下拉刷新完成 */ void refreshComplete(); /** * 获取HeaderView */ View getHeaderView(); /** * 获取Header的显示高度 */ int getVisibleHeight(); }
我们的自定义View即ArrowRefreshHeader这个类实现了上面的接口,上面说道,通过接口回调的形式将移动的距离通过onMove方法回传给了ArrowRefreshHeader,现在看下ArrowRefreshHeader中onMove方法的具体实现,如下
if (getVisibleHeight() > 0 || offSet > 0) { setVisibleHeight((int) offSet + getVisibleHeight()); if (mState <= STATE_RELEASE_TO_REFRESH) { // 未处于刷新状态,更新箭头 if (getVisibleHeight() > mMeasuredHeight) { onPrepare(); } else { onReset(); } } }
这里将移动的距离通过setVisibleHeight方法进行显示,再看下这个方法的实现
public void setVisibleHeight(int height) { if (height < 0) height = 0; LayoutParams lp = (LayoutParams) mContentLayout.getLayoutParams(); lp.height = height; mContentLayout.setLayoutParams(lp); }
这个方法的主要功能就是将距离设置给了LayoutParams中的height字段,然后再重新设置mContentLayout的布局属性。
通过以上的方法,便可以实现当下拉到一定距离时显示头部刷新View的功能了,下面看下实现的效果
到了这一步已经可以实现下拉以及刷新的功能了,但是只会一直刷新,根本停不下来!这里是因为我们没有调用刷新完成的回调方法,下面开始实现刷新完成是的功能。