您的位置:首页 > 移动开发 > Android开发

Android RecyclerView下拉刷新的实现和源码分析

2017-04-04 23:29 519 查看
目前RecyclerView是主流的列表显示控件,RecyclerView支持的特性很多,但是并没有自带官方的下拉刷新功能。谷歌提供了一个SwipeRefreshLayout的下拉刷新控件,就是一个小圆圈在转动,自定义效率有限,并不能满足日常的需求开发。现在github上也有很多RecyclerView的衍生控件实现了自定义下拉刷新效果,它们的实现原理各有同,总的来说,目前主要可以实现下拉刷新效果的方案有2种,一种是添加一个刷新头部,另一种是实现一个外部可滑动的容器来包装RecyclerView。本文主要分析添加刷新头部实现RecyclerView下拉刷新效果的实现和源码分析。

实现效果:



需求分析

在实现刷新头部之前,首先需要进行下拉刷新的需求分析,分析整个下拉过程中需要什么状态和动作,然后根据这些动作来判断执行刷新操作。首先看下面一幅下拉刷新头部的位置图:



虚线代表刷新头部的完整高度,刷新头部的高度一般是需要写死固定的,所以可以把这当成一个标准线。黄线代表下拉时头部底部位置处于刷新头部标准高度内,蓝色代表下拉时头部底部位置处于刷新头部标准高度外。

在下拉刷新时需要四种状态,即普通状态(刷新头部标准线内,即黄线位置),手指离开屏幕释放状态(刷新头部标准线外,即蓝线位置),正在刷新状态(刷新头部标准线位置),刷新完成状态。从此可得到涉及下拉操作的事件回调方法,即刷新头部底部移动(高度变化)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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐