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

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

实现效果:
《Android RecyclerView下拉刷新的实现和源码分析》

需求分析

在实现刷新头部之前,首先需要进行下拉刷新的需求分析,分析整个下拉过程中需要什么状态和动作,然后根据这些动作来判断执行刷新操作。首先看下面一幅下拉刷新头部的位置图:
《Android 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进度展示。如下图所示。
《Android RecyclerView下拉刷新的实现和源码分析》
布局代码如下所示:

<?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源码分析
    原文地址: https://blog.csdn.net/qq402164452/article/details/69063149
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞