Android RecyclerView下拉刷新的实现和源码分析
2017-04-04 23:29
519 查看
目前RecyclerView是主流的列表显示控件,RecyclerView支持的特性很多,但是并没有自带官方的下拉刷新功能。谷歌提供了一个SwipeRefreshLayout的下拉刷新控件,就是一个小圆圈在转动,自定义效率有限,并不能满足日常的需求开发。现在github上也有很多RecyclerView的衍生控件实现了自定义下拉刷新效果,它们的实现原理各有同,总的来说,目前主要可以实现下拉刷新效果的方案有2种,一种是添加一个刷新头部,另一种是实现一个外部可滑动的容器来包装RecyclerView。本文主要分析添加刷新头部实现RecyclerView下拉刷新效果的实现和源码分析。
实现效果:
![](http://img.blog.csdn.net/20170404233527886?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXE0MDIxNjQ0NTI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](http://img.blog.csdn.net/20170404164627422?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXE0MDIxNjQ0NTI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
虚线代表刷新头部的完整高度,刷新头部的高度一般是需要写死固定的,所以可以把这当成一个标准线。黄线代表下拉时头部底部位置处于刷新头部标准高度内,蓝色代表下拉时头部底部位置处于刷新头部标准高度外。
在下拉刷新时需要四种状态,即普通状态(刷新头部标准线内,即黄线位置),手指离开屏幕释放状态(刷新头部标准线外,即蓝线位置),正在刷新状态(刷新头部标准线位置),刷新完成状态。从此可得到涉及下拉操作的事件回调方法,即刷新头部底部移动(高度变化)onMove,刷新完成触发onComplete,手指释放触发onRelease,刷新头部状态改变触发onStateChange,同时我们可以定义一个包含上述状态和触发事件方法的接口BaseRefreshHeader。
![](http://img.blog.csdn.net/20170404171054136?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcXE0MDIxNjQ0NTI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
布局代码如下所示:
刷新头部通过addView将container头部UI布局添加到LinearLayout容器中。上述代码主要是初始化了2个旋转动画,这2个动画是用于指示箭头的,当刷新头部底部下拉至标准线下面时,rotateUpAnim动画使箭头从向下旋转至向上;当刷新头部底部滑动至标注线内时,rotateDownAnim动画使箭头从向上旋转至向下。最后通过调用measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT)来测量刷新头部UI布局,然后调用getMeasureHeight来获取完整的刷新头部高度,即标准线高度位置。
下面分析下拉使刷新头部滑动的实现方法。刷新头部下拉滑动的主要方式是动态通过改变容器的LayoutParams.height高度来实现的,这个是核心方法。
从上面代码可以分析出setVisibleHeight方法调用getLayoutParams来获取头部布局属性,给这个属性的高度height重新赋值并设置给container。getVisibleHeight也是同样通过LayoutParams来获取头部的实时高度。onMove方法是通过参数值动态改变头部布局的高度的,delta是滑动过程中的位移变化量,下面会分析这个变量。同时还要设置mState的状态,<=STATE_RELEASE即只有两种状态(普通状态,释放刷新状态),当头部布局底部大于标准线时,将状态设置为释放刷新状态;当头部布局底部小于标准线时,将状态设置为普通状态。
下面分析刷新完成后的操作。当头部刷新完成后,需要将正在刷新状态设置为刷新完成状态,并头部布局滚动至隐藏起来。
刷新操作完成后,reset方法需要通过Handler来发送延迟刷新UI,因为刷新完成后提示文字会显示为“刷新完成”并上滑至隐藏起来,如果没有延迟会立即将提示文字修改为下拉刷新,这样不友好。smoothScrollTo(int destHeight)方法的实现也很值得借鉴,通过ValueAnimator.ofInt属性动画的线性变化,添加动画运行过程的监听器来获取动画线性变化的数值并调用setVisibleHeight实时动态改变头部的高度,并线性向上滑动至隐藏起来。
手指离开屏幕后触发的操作也是不同的,需要几种情况进行分析:
当刷新头部底部位置大于标准线并且是释放刷新状态,触发进行刷新操作
当刷新头部状态不是正在刷新状态,触发头部滑动至隐藏操作
当刷新头部处于正在刷新状态,触发头部滑动至完整高度(即标准线)
刷新头部里面的onStateChange方法主要是通过状态参数值来改变UI控件状态的,比较简单,注意的是参数中的state是表示最新的状态的,而mState变量是上一次的最新状态。
这里需要注意的是从STATE_RELEASE释放刷新状态到STATE_NORMAL普通状态时,必须使用rotateDownAnim将箭头旋转为向下;从STATE_NORMAL普通状态到STATE_RELEASE释放刷新状态时,必须调用rotateUpAnim将箭头旋转为向上。
从上面可以看到两个比较重要的类WrapAdapter和AdapterDataObserver。WrapAdapter是一个继承自RecyclerView.Adapter< ViewHolder>的类,在这里可以称为内部Adapter类,因为我们RecyclerView的Item数据源跟刷新头部是分开的,而且为了能够做到跟原生RecyclerView一样的无缝接合,所以需要这个内部Adapter类来包装我们传递进去的adapter类,这样我们在使用的时候就可以直接调用setAdapter直接使用自定义adapter,不用再去分开实现刷新头部item了。AdapterDataObserver是一个Adapter数据监听器,因为使用了内部Adapter封装了我们的自定义Adapter,所以需要给自定义Adapter添加数据监听器,当我们修改自定义Adapter里面的数据时内部Adapter就会进行相应变化。代码如下所示:
自定义RecyclerView除了上面两个类之后,最重要的另一个方法是onTouchEvent方法了,因为下拉刷新操作涉及到了点击屏幕,滑动屏幕和离开屏幕等操作,需要在onTouchEvent方法里面进行相应的判断和调用刷新头部里面的方法进行操作。如下所示:
其中isOnTop方法主要是用来判断刷新头部是否可见的,即是否滑动到RecyclerView的顶部。这里使用了refreshHeader.getParent()获取到的ViewParent是否为null来判断,当refreshHeader刷新头部可见时,就可以获取到相应的ViewParent,而当不可见时就返回null,这里涉及到了RecyclerView的视图复用机制。
接下来就是最核心的onTouchEvent了,这里主要分为了3种情况:
手指点击屏幕,获取点击的Y轴坐标
手指滑动屏幕,获取滑动距离deltaY,调用刷新头部refreshHeader的onMove方法动态改变刷新头部高度从而实现下拉滑动头部布局的效果
手指离开屏幕,即释放操作状态,需要调用刷新头部的onRelease进行判断释放操作是否会触发刷新操作,然后触发用户自定义监听事件
这里用户的监听事件接口很简单,只有一个onRefresh方法,如下所示:
使用方法跟一般的RecyclerView完全相同,不用担心需要自己去处理刷新头部的问题。
源代码:https://github.com/QQ402164452/PullToRefreshRecyclerView
下一章将解析上拉刷新的实现和源码分析。
2017.4.5 新增刷新超时功能
参考:XRecyclerView
实现效果:
需求分析
在实现刷新头部之前,首先需要进行下拉刷新的需求分析,分析整个下拉过程中需要什么状态和动作,然后根据这些动作来判断执行刷新操作。首先看下面一幅下拉刷新头部的位置图:虚线代表刷新头部的完整高度,刷新头部的高度一般是需要写死固定的,所以可以把这当成一个标准线。黄线代表下拉时头部底部位置处于刷新头部标准高度内,蓝色代表下拉时头部底部位置处于刷新头部标准高度外。
在下拉刷新时需要四种状态,即普通状态(刷新头部标准线内,即黄线位置),手指离开屏幕释放状态(刷新头部标准线外,即蓝线位置),正在刷新状态(刷新头部标准线位置),刷新完成状态。从此可得到涉及下拉操作的事件回调方法,即刷新头部底部移动(高度变化)onMove,刷新完成触发onComplete,手指释放触发onRelease,刷新头部状态改变触发onStateChange,同时我们可以定义一个包含上述状态和触发事件方法的接口BaseRefreshHeader。
public interface BaseRefreshHeader { int STATE_NORMAL=0; int STATE_RELEASE=1; int STATE_REFRESHING=2; int STATE_COMPLETE=3; void onMove(float delta); void onComplete(); boolean onRelease(); void onStateChange(int state); }
刷新头部UI布局
本文刷新头部的UI布局很简单,这里仅作实现原理的分析,不做复杂的UI。刷新头部仅包含一个ImageView箭头图片,一个TextView状态提示文字,一个ProgressBar进度展示。如下图所示。布局代码如下所示:
<?xml version="1.0" encoding="utf-8"?> <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="wrap_content" android:background="@android:color/darker_gray" android:gravity="bottom"> <RelativeLayout android:layout_width="match_parent" android:layout_height="60dp"> <TextView android:id="@+id/PullToRefresh_Header_HintTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp" tools:text="Pull To Refresh" android:text="@string/PullToRefresh_Header_Hint_Normal" android:layout_centerInParent="true" android:layout_marginStart="10dp"/> <ImageView android:id="@+id/PullToRefresh_Header_ArrowImageView" android:layout_width="20dp" android:layout_height="20dp" android:layout_centerVertical="true" android:src="@drawable/ic_pulltorefresh_arrow" android:layout_toStartOf="@id/PullToRefresh_Header_HintTextView"/> <ProgressBar android:id="@+id/PullToRefresh_Header_ProgressBar" android:layout_width="20dp" android:layout_height="20dp" android:layout_centerVertical="true" android:visibility="gone" android:layout_toStartOf="@id/PullToRefresh_Header_HintTextView"/> </RelativeLayout> </RelativeLayout>
刷新头部的实现和源码分析
刷新头部是一个容器,需要继承自LinearLayout,并实现上面的基类接口BaseRefreshHeader。如下所示:public class RefreshHeader extends LinearLayout implements BaseRefreshHeader { private RelativeLayout container;//内部容器 private ImageView arrowImageView;//指示箭头 private TextView hintTextView;//提示文字 private ProgressBar progressBar;//进度提示 private Animation rotateDownAnim;//向下旋转动画 private Animation rotateUpAnim;//向上旋转动画 private final int ROTATE_ANIM_DURATION=200;//动画持续时间 private int mState=STATE_NORMAL;//当前头部状态 private int measuredHeight;//刷新头部的高度 public RefreshHeader(Context context) { this(context,null); } public RefreshHeader(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public RefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){//初始化 container= (RelativeLayout) LayoutInflater.from(getContext()).inflate(R.layout.refresh_header,null); setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); addView(container,new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0)); arrowImageView= (ImageView) container.findViewById(R.id.PullToRefresh_Header_ArrowImageView); hintTextView= (TextView) container.findViewById(R.id.PullToRefresh_Header_HintTextView); progressBar= (ProgressBar) container.findViewById(R.id.PullToRefresh_Header_ProgressBar); rotateUpAnim=new RotateAnimation(0.0f,-180.0f, Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);//RELATIVE_TO_SELF是相对 rotateUpAnim.setDuration(ROTATE_ANIM_DURATION); rotateUpAnim.setFillAfter(true); rotateDownAnim=new RotateAnimation(-180.0f,0.0f, Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f); rotateDownAnim.setDuration(ROTATE_ANIM_DURATION); rotateDownAnim.setFillAfter(true); measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);//调用measure进行测量 measuredHeight=getMeasuredHeight();//获取刷新头部的标准高度 }
刷新头部通过addView将container头部UI布局添加到LinearLayout容器中。上述代码主要是初始化了2个旋转动画,这2个动画是用于指示箭头的,当刷新头部底部下拉至标准线下面时,rotateUpAnim动画使箭头从向下旋转至向上;当刷新头部底部滑动至标注线内时,rotateDownAnim动画使箭头从向上旋转至向下。最后通过调用measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT)来测量刷新头部UI布局,然后调用getMeasureHeight来获取完整的刷新头部高度,即标准线高度位置。
下面分析下拉使刷新头部滑动的实现方法。刷新头部下拉滑动的主要方式是动态通过改变容器的LayoutParams.height高度来实现的,这个是核心方法。
public int getVisibleHeight(){ return container.getLayoutParams().height;//获取刷新头部的实时高度 } public void setVisibleHeight(int height){//实时改变刷新头部的实时高度 if(height<0){ height=0; } LinearLayout.LayoutParams params= (LayoutParams) container.getLayoutParams(); params.height=height; container.setLayoutParams(params); } @Override public void onMove(float delta) {//主要方法 调用此方法进行刷新头部高度的变化 if(getVisibleHeight()>0||delta>0){ setVisibleHeight((int)delta+getVisibleHeight());//改变刷新头部的高度 if(mState<=STATE_RELEASE){ if(getVisibleHeight()>measuredHeight){ onStateChange(STATE_RELEASE);//如果下拉高度 大于 标准高度,将状态设置为 释放刷新 }else{ onStateChange(STATE_NORMAL);//如果下拉高度 小于 标准高度,将状态设置为 普通状态 } } } }
从上面代码可以分析出setVisibleHeight方法调用getLayoutParams来获取头部布局属性,给这个属性的高度height重新赋值并设置给container。getVisibleHeight也是同样通过LayoutParams来获取头部的实时高度。onMove方法是通过参数值动态改变头部布局的高度的,delta是滑动过程中的位移变化量,下面会分析这个变量。同时还要设置mState的状态,<=STATE_RELEASE即只有两种状态(普通状态,释放刷新状态),当头部布局底部大于标准线时,将状态设置为释放刷新状态;当头部布局底部小于标准线时,将状态设置为普通状态。
下面分析刷新完成后的操作。当头部刷新完成后,需要将正在刷新状态设置为刷新完成状态,并头部布局滚动至隐藏起来。
private void smoothScrollTo(int destHeight){//线性动画 改变 刷新头部的高度 ValueAnimator animator=ValueAnimator.ofInt(getVisibleHeight(),destHeight);//使用属性动画 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) {//在动画运行中监听动画数值的改变 setVisibleHeight((int)animation.getAnimatedValue());//用动画线性改变的数值 动态改变 刷新头部的高度 } }); animator.setDuration(300).start(); } public void reset(){//重置刷新头部的状态 将刷新头部的状态重置为隐藏 smoothScrollTo(0); new Handler().postDelayed(new Runnable() { @Override public void run() { onStateChange(STATE_NORMAL); } },500); } @Override public void onComplete() {//下拉刷新完成 onStateChange(STATE_COMPLETE);//将状态设置为 刷新完成 new Handler().postDelayed(new Runnable() { @Override public void run() { reset();//延迟进行重置 } },200); }
刷新操作完成后,reset方法需要通过Handler来发送延迟刷新UI,因为刷新完成后提示文字会显示为“刷新完成”并上滑至隐藏起来,如果没有延迟会立即将提示文字修改为下拉刷新,这样不友好。smoothScrollTo(int destHeight)方法的实现也很值得借鉴,通过ValueAnimator.ofInt属性动画的线性变化,添加动画运行过程的监听器来获取动画线性变化的数值并调用setVisibleHeight实时动态改变头部的高度,并线性向上滑动至隐藏起来。
手指离开屏幕后触发的操作也是不同的,需要几种情况进行分析:
当刷新头部底部位置大于标准线并且是释放刷新状态,触发进行刷新操作
当刷新头部状态不是正在刷新状态,触发头部滑动至隐藏操作
当刷新头部处于正在刷新状态,触发头部滑动至完整高度(即标准线)
@Override public boolean onRelease() {//手指离开屏幕 即进行释放时,触发此方法 boolean isOnRefresh=false;//标志位,判断是否是在刷新触发用户的接口回调事件 int height=getVisibleHeight(); if(height==0){ isOnRefresh=false; } if(height>=measuredHeight&&mState==STATE_RELEASE){//如果下拉高度大于标准高度,并且是释放刷新状态 onStateChange(STATE_REFRESHING);//将状态设置为刷新状态 isOnRefresh=true; } if(mState!=STATE_REFRESHING){//如果手指释放时,不是正在刷新状态,将头部高度设置为0 smoothScrollTo(0); } if(mState==STATE_REFRESHING){//如果手指释放时,是正在刷新状态,将头部高度设置为标准高度 smoothScrollTo(measuredHeight); } return isOnRefresh; }
刷新头部里面的onStateChange方法主要是通过状态参数值来改变UI控件状态的,比较简单,注意的是参数中的state是表示最新的状态的,而mState变量是上一次的最新状态。
@Override public void onStateChange(int state) {//状态改变时触发此方法 if(mState==state){//注意state是最新状态,mState是上一次的状态 return; } switch (state){ case STATE_NORMAL: arrowImageView.setVisibility(View.VISIBLE); progressBar.setVisibility(View.GONE); hintTextView.setText(R.string.PullToRefresh_Header_Hint_Normal); if(mState==STATE_REFRESHING){//当由正在刷新状态转变为普通状态时 arrowImageView.clearAnimation(); } if(mState==STATE_RELEASE){//当从滑动释放状态转变为普通状态时 arrowImageView.clearAnimation(); arrowImageView.startAnimation(rotateDownAnim);//将箭头转向下 } break; case STATE_RELEASE: arrowImageView.setVisibility(View.VISIBLE); progressBar.setVisibility(View.GONE); if(mState==STATE_NORMAL){//从普通状态转变为滑动释放状态 arrowImageView.clearAnimation(); arrowImageView.startAnimation(rotateUpAnim);//将箭头转向上 } hintTextView.setText(R.string.PullToRefresh_Header_Hint_Release); break; case STATE_REFRESHING: arrowImageView.clearAnimation(); arrowImageView.setVisibility(View.GONE); progressBar.setVisibility(View.VISIBLE); smoothScrollTo(measuredHeight);//将头部高度设置为标准高度 hintTextView.setText(R.string.PullToRefresh_Header_Hint_Refreshing); break; case STATE_COMPLETE: arrowImageView.setVisibility(View.GONE); progressBar.setVisibility(View.GONE); hintTextView.setText(R.string.PullToRefresh_Header_Hint_Complete); break; } mState=state; }
这里需要注意的是从STATE_RELEASE释放刷新状态到STATE_NORMAL普通状态时,必须使用rotateDownAnim将箭头旋转为向下;从STATE_NORMAL普通状态到STATE_RELEASE释放刷新状态时,必须调用rotateUpAnim将箭头旋转为向上。
自定义RecyclerView
上面主要分析了刷新头部的实现和源码分析,其实刷新头部相当于RecyclerView中的一个Item,有了刷新头部Item之后,还必须自定义一个继承RecyclerView的自定义View来实现下拉刷新效果。public class PullToRefreshRecyclerView extends RecyclerView{ private boolean pullToRefreshEnabled=true;//下拉刷新 开关 private static final int TYPE_REFRESH_HEADER=10000;//刷新头部 类型标号 private static final float DRAG_RATE=3;//拖动阻力系数 private WrapAdapter mWrapAdapter;//内部Adapter private RefreshHeader refreshHeader;//刷新头部 private AdapterDataObserver dataObserver;//数据监听器 private float lastY=-1; private OnRefreshListener onRefreshListener; public PullToRefreshRecyclerView(Context context) { this(context,null); } public PullToRefreshRecyclerView(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public PullToRefreshRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init(){ dataObserver=new DataObserver(); if(pullToRefreshEnabled){ refreshHeader=new RefreshHeader(getContext());//获取刷新头部 } }
从上面可以看到两个比较重要的类WrapAdapter和AdapterDataObserver。WrapAdapter是一个继承自RecyclerView.Adapter< ViewHolder>的类,在这里可以称为内部Adapter类,因为我们RecyclerView的Item数据源跟刷新头部是分开的,而且为了能够做到跟原生RecyclerView一样的无缝接合,所以需要这个内部Adapter类来包装我们传递进去的adapter类,这样我们在使用的时候就可以直接调用setAdapter直接使用自定义adapter,不用再去分开实现刷新头部item了。AdapterDataObserver是一个Adapter数据监听器,因为使用了内部Adapter封装了我们的自定义Adapter,所以需要给自定义Adapter添加数据监听器,当我们修改自定义Adapter里面的数据时内部Adapter就会进行相应变化。代码如下所示:
@Override public void setAdapter(Adapter adapter){ mWrapAdapter=new WrapAdapter(adapter);//使用内部Adapter包装用户的Adapter super.setAdapter(mWrapAdapter); adapter.registerAdapterDataObserver(dataObserver);//注册Adapter数据监听器 dataObserver.onChanged(); } @Override public Adapter getAdapter(){ if(mWrapAdapter!=null){ return mWrapAdapter.getOriginalAdapter();//获取内部包装的用户的Adapter }else{ return null; } } private class WrapAdapter extends Adapter<ViewHolder>{//内部Adapter 使用装饰者模式包装用户传进来的Adapter private Adapter adapter; private WrapAdapter(Adapter adapter){ this.adapter=adapter; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if(viewType==TYPE_REFRESH_HEADER){//刷新头部类型 return new SimpleViewHolder(refreshHeader); } return adapter.onCreateViewHolder(parent,viewType);//其它为自定义Adapter里面的Item类型 } @Override public void onBindViewHolder(ViewHolder holder, int position) { if(isRefreshHeader(position)){ return; } int adjPosition=position-1;//减去刷新头部Item的数量1 if(adapter!=null){ if(adjPosition<adapter.getItemCount()){ adapter.onBindViewHolder(holder,adjPosition); } } } @Override public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads){ if(isRefreshHeader(position)){ return; } int adjPosition=position-1;//减去刷新头部Item的数量1 if(adapter!=null){ if(adjPosition<adapter.getItemCount()){ adapter.onBindViewHolder(holder,adjPosition,payloads); } } } @Override public int getItemCount() { if(adapter!=null){ return adapter.getItemCount()+1;//加上刷新头部Item的数量1 } return 0; } @Override public int getItemViewType(int position){ int adjPosition=position-1;//减去刷新头部Item的数量1 if(isRefreshHeader(position)){ return TYPE_REFRESH_HEADER; } if(adapter!=null){ if(adjPosition<adapter.getItemCount()){ return adapter.getItemViewType(position); } } return 0; } @Override public long getItemId(int position){ if(adapter!=null&&position>=1){ int adjPosition=position-1;//减去刷新头部Item的数量1 if(adjPosition<adapter.getItemCount()){ return adapter.getItemId(adjPosition); } } return -1; } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView){ super.onAttachedToRecyclerView(recyclerView); LayoutManager manager=getLayoutManager(); if(manager instanceof GridLayoutManager){ final GridLayoutManager gridLayoutManager= (GridLayoutManager) manager; gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return isRefreshHeader(position)?gridLayoutManager.getSpanCount():1; } }); } if(adapter!=null){ adapter.onAttachedToRecyclerView(recyclerView); } } @Override public void onDetachedFromRecyclerView(RecyclerView recyclerView){ if(adapter!=null){ adapter.onDetachedFromRecyclerView(recyclerView); } } @Override public void onViewAttachedToWindow(ViewHolder holder){ super.onViewAttachedToWindow(holder); ViewGroup.LayoutParams layoutParams=holder.itemView.getLayoutParams(); if(layoutParams!=null&& layoutParams instanceof StaggeredGridLayoutManager.LayoutParams&& isRefreshHeader(holder.getLayoutPosition())){ StaggeredGridLayoutManager.LayoutParams params= (StaggeredGridLayoutManager.LayoutParams) layoutParams; params.setFullSpan(true); } if(adapter!=null){ adapter.onViewAttachedToWindow(holder); } } @Override public void onViewDetachedFromWindow(ViewHolder holder){ if(adapter!=null){ adapter.onViewDetachedFromWindow(holder); } } @Override public void onViewRecycled(ViewHolder holder){ if(adapter!=null){ adapter.onViewRecycled(holder); } } @Override public boolean onFailedToRecycleView(ViewHolder holder){ if(adapter!=null){ return adapter.onFailedToRecycleView(holder); } return false; } @Override public void registerAdapterDataObserver(AdapterDataObserver observer){ if(adapter!=null){ adapter.registerAdapterDataObserver(observer); } } @Override public void unregisterAdapterDataObserver(AdapterDataObserver observer){ if(adapter!=null){ adapter.unregisterAdapterDataObserver(observer); } } public Adapter getOriginalAdapter(){ return this.adapter; } public boolean isRefreshHeader(int position){ return position==0; } private class SimpleViewHolder extends ViewHolder{ public SimpleViewHolder(View itemView) { super(itemView); } } } private class DataObserver extends AdapterDataObserver{//Adapter数据监听器 与 WrapAdapter联动作用 @Override public void onChanged(){ if(mWrapAdapter!=null){ mWrapAdapter.notifyDataSetChanged(); } } @Override public void onItemRangeInserted(int positionStart,int itemCount){ mWrapAdapter.notifyItemRangeInserted(positionStart,itemCount); } @Override public void onItemRangeChanged(int positionStart,int itemCount){ mWrapAdapter.notifyItemRangeChanged(positionStart,itemCount); } @Override public void onItemRangeChanged(int positionStart,int itemCount,Object payload){ mWrapAdapter.notifyItemRangeChanged(positionStart,itemCount,payload); } @Override public void onItemRangeRemoved(int positionStart,int itemCount){ mWrapAdapter.notifyItemRangeRemoved(positionStart,itemCount); } @Override public void onItemRangeMoved(int fromPosition,int toPosition,int itemCount){ mWrapAdapter.notifyItemMoved(fromPosition,toPosition); } }
自定义RecyclerView除了上面两个类之后,最重要的另一个方法是onTouchEvent方法了,因为下拉刷新操作涉及到了点击屏幕,滑动屏幕和离开屏幕等操作,需要在onTouchEvent方法里面进行相应的判断和调用刷新头部里面的方法进行操作。如下所示:
private boolean isOnTop(){//判断刷新头部是否可见 if(refreshHeader.getParent()!=null){//当刷新头部可见时,getParent()获取到的值不为null return true; }else{ return false; } } @Override public boolean onTouchEvent(MotionEvent ev){//重点 监听触屏事件来触发头部的状态和实时高度 if(lastY==-1){ lastY=ev.getRawY(); } switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: lastY=ev.getRawY();//获取点击的Y轴位置 break; case MotionEvent.ACTION_MOVE: float deltaY=ev.getRawY()-lastY;//获取滑动的距离高度 lastY=ev.getRawY();//获取点击的Y轴位置 if(isOnTop()&&pullToRefreshEnabled){ refreshHeader.onMove(deltaY/DRAG_RATE);//实时改变刷新头部的高度 if(refreshHeader.getVisibleHeight()>0&&refreshHeader.getState()<=RefreshHeader.STATE_RELEASE){ return true; } } break; default://手指离开屏幕 释放状态 lastY=-1; if(isOnTop()&&pullToRefreshEnabled){ if(refreshHeader.onRelease()){//判断手指离开屏幕时的状态,决定是否调用用户监听器 if(onRefreshListener!=null){ onRefreshListener.onRefresh(); Log.e("pullToRefresh","release"); } } } break; } return super.onTouchEvent(ev); }
其中isOnTop方法主要是用来判断刷新头部是否可见的,即是否滑动到RecyclerView的顶部。这里使用了refreshHeader.getParent()获取到的ViewParent是否为null来判断,当refreshHeader刷新头部可见时,就可以获取到相应的ViewParent,而当不可见时就返回null,这里涉及到了RecyclerView的视图复用机制。
接下来就是最核心的onTouchEvent了,这里主要分为了3种情况:
手指点击屏幕,获取点击的Y轴坐标
手指滑动屏幕,获取滑动距离deltaY,调用刷新头部refreshHeader的onMove方法动态改变刷新头部高度从而实现下拉滑动头部布局的效果
手指离开屏幕,即释放操作状态,需要调用刷新头部的onRelease进行判断释放操作是否会触发刷新操作,然后触发用户自定义监听事件
这里用户的监听事件接口很简单,只有一个onRefresh方法,如下所示:
public interface OnRefreshListener { void onRefresh(); }
使用方法跟一般的RecyclerView完全相同,不用担心需要自己去处理刷新头部的问题。
recyclerView= (PullToRefreshRecyclerView) findViewById(R.id.PullToRefreshRecyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(adapter); recyclerView.setOnRefreshListener(new OnRefreshListener() { @Override public void onRefresh() { new Handler().postDelayed(new Runnable() { @Override public void run() { recyclerView.refreshComplete(); } },2000);//模拟刷新完成 } });
总结
在RecyclerView中添加刷新头部Item来实现下拉刷新效果的原理还是挺简单的,主要是在自定义RecyclerView的onTouchEvent中判断操作,然后在调用刷新头部的相应方法进行操作头部UI布局。刷新头部的核心在于状态的判断和LayoutParams布局属性的改变,需要对每一种状态改变过程进行分析和总结,才能更好的掌握下拉刷新的原理。源代码:https://github.com/QQ402164452/PullToRefreshRecyclerView
下一章将解析上拉刷新的实现和源码分析。
2017.4.5 新增刷新超时功能
参考:XRecyclerView
相关文章推荐
- Android RecyclerView上拉加载更多的实现和源码分析
- Android使用RecyclerView实现自定义列表、点击事件以及下拉刷新
- Android View系统源码分析(四)—— 各种消息监测的基本实现方法&View.dispatchTouchEvent()
- Android开发 ---RecyclerView实现下拉刷新
- android 简单实现 RecyclerView 下拉刷新上拉加载
- Android RecyclerView实现下拉刷新和上拉加载更多
- Android 5.X新特性之为RecyclerView添加下拉刷新和上拉加载及SwipeRefreshLayout实现原理
- Android中使用RecyclerView实现下拉刷新和上拉加载
- Android-详解RecyclerView+BGARefreshLayout实现自定义下拉刷新、上拉加载和侧滑删除效果
- 用Recyclerview实现列表分组、下拉刷新以及上拉加载--源码
- Android实现RecyclerView的下拉刷新和上拉加载更多
- Android中RecyclerView实现下拉刷新上拉加载更多
- Android实现RecyclerView自定义列表,SwipeRefreshLayout实现下拉刷新
- Android-RecyclerView使用(三) 实现下拉刷新,上拉自动加载
- Android LRecyclerView实现下拉刷新,滑动到底部自动加载更多
- Android RecyclerView的Item自定义动画及DefaultItemAnimator源码分析
- Android RecyclerView实现下拉刷新和上拉加载
- Android之RecyclerView轻松实现下拉刷新和加载更多示例
- Android——实现酷炫的RecyclerView心形交错下拉刷新动画
- Android 5.X新特性之为RecyclerView添加下拉刷新和上拉加载及SwipeRefreshLayout实现原理