zoukankan      html  css  js  c++  java
  • Android 5.X新特性之为RecyclerView添加下拉刷新和上拉加载及SwipeRefreshLayout实现原理

    RecyclerView已经写过两篇文章了,分别是Android 5.X新特性之RecyclerView基本解析及无限复用Android 5.X新特性之为RecyclerView添加HeaderView和FooterView,既然来到这里还没学习的,先去学习下吧。

    今天我们的主题是学习为RecyclerView添加下拉刷新和上拉加载功能。

    首先,我们先来学习下拉刷新,google公司已经为我们提供的一个很好的包装类,那就是SwipeRefreshLayout,这个类可以支持我们向下滑动并进行监听。那么我们先了解一些基本知识,然后再从源码的角度来解析它。

    A. SwipeRefreshLayout 是一个容器,直接继承于ViewGroup。

    从其源码中我们可以直接看出,它是直接继承于ViewGroup的,所以它是一个容器,既然是一个容器,那么我们就可以向其中添加View。
    

    B. SwipeRefreshLayout 封装了一些列的方法供我们使用,其中较常用的包括以下几个。

    1. setColorSchemeResources: 刷新时动画的颜色,可以设置4个
    2. setProgressBackgroundColorSchemeResource: 设置刷新时进度圆环的背景颜色
    3. setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener): 设置手势滑动监听器。
    4. setRefreshing(Boolean refreshing): 设置组件的刷洗状态。
    5. setSize(int size):设置进度圈的大小,只有两个值:DEFAULT、LARGE
    

    其中最主要的是setOnRefreshListener,它是用来监听我们下拉手势的回调方法。

    C. 接下来我们再从源码的角度来了解这个类:

    SwipeRefreshLayout 是一个ViewGroup容器,那在向它添加子View的时候,那首先会去测量各个子View的大小来确定本身的大小,并且还会制定子View的坐标位置,最后绘制View并显示出来。针对ViewGroup的绘制我之前有写过一篇博文,大家可以去参考下Android自定义控件之继承ViewGroup创建新容器(四) ,里面有详细的讲解。而我们今天所要讲解的是从SwipeRefreshLayout 的事件机制来说起,也更符合我们下拉刷新的主题。

    在SwipeRefreshLayout 的事件拦截分发器onInterceptTouchEvent中,它是这么定制的,源码如下:

    	@Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            ensureTarget();
    
            final int action = MotionEventCompat.getActionMasked(ev);
    
            if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
                mReturningToStart = false;
            }
    
            if (!isEnabled() || mReturningToStart || canChildScrollUp()
                    || mRefreshing || mNestedScrollInProgress) {
                // Fail fast if we're not in a state where a swipe is possible
                return false;
            }
    
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                    mIsBeingDragged = false;
                    final float initialDownY = getMotionEventY(ev, mActivePointerId);
                    if (initialDownY == -1) {
                        return false;
                    }
                    mInitialDownY = initialDownY;
                    break;
    
                case MotionEvent.ACTION_MOVE:
                    if (mActivePointerId == INVALID_POINTER) {
                        Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                        return false;
                    }
    
                    final float y = getMotionEventY(ev, mActivePointerId);
                    if (y == -1) {
                        return false;
                    }
                    final float yDiff = y - mInitialDownY;
                    if (yDiff > mTouchSlop && !mIsBeingDragged) {
                        mInitialMotionY = mInitialDownY + mTouchSlop;
                        mIsBeingDragged = true;
                        mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
                    }
                    break;
    
                case MotionEventCompat.ACTION_POINTER_UP:
                    onSecondaryPointerUp(ev);
                    break;
    
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mIsBeingDragged = false;
                    mActivePointerId = INVALID_POINTER;
                    break;
            }
    
            return mIsBeingDragged;
        }
    

    它最终返回的是代表是否滑动的mIsBeingDragged布尔值。在我们按下,抬起,或取消时mIsBeingDragged的值是false,意思是在这几个动作中,SwipeRefreshLayout 本身是不拦截事件的,而是传递给父类,让父类进行处理。而我们主要来看MotionEvent.ACTION_MOVE:这个动作,它首先判断是否是可用的活动id: mActivePointerId,然后根据得到mActivePointerId来获取滑动的中坐标距离值:Y,然后做出判断:如果Y==-1就代表没滑动,所以直接返回false表示不拦截;如果Y值大于规定的最小滑动距离mTouchSlop值,并且!mIsBeingDragged为真,那么就让mIsBeingDragged == true;并返回,也就是在这种情况下,SwipeRefreshLayout 它自己消化了事件,而不是传递给父类。因此,当我们在向下滑动了一定的距离时,SwipeRefreshLayout 就是捕捉到当前的事件。

    那么我们再来看看它是怎么处理当前捕捉到的事件的。请看源码:

    	@Override
        public boolean onTouchEvent(MotionEvent ev) {
            final int action = MotionEventCompat.getActionMasked(ev);
            int pointerIndex = -1;
    
            if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
                mReturningToStart = false;
            }
    
            if (!isEnabled() || mReturningToStart || canChildScrollUp() || mNestedScrollInProgress) {
                // Fail fast if we're not in a state where a swipe is possible
                return false;
            }
    
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                    mIsBeingDragged = false;
                    break;
    
                case MotionEvent.ACTION_MOVE: {
                    pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    if (pointerIndex < 0) {
                        Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
                        return false;
                    }
    
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    if (mIsBeingDragged) {
                        if (overscrollTop > 0) {
                            moveSpinner(overscrollTop);
                        } else {
                            return false;
                        }
                    }
                    break;
                }
                case MotionEventCompat.ACTION_POINTER_DOWN: {
                    pointerIndex = MotionEventCompat.getActionIndex(ev);
                    if (pointerIndex < 0) {
                        Log.e(LOG_TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
                        return false;
                    }
                    mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
                    break;
                }
    
                case MotionEventCompat.ACTION_POINTER_UP:
                    onSecondaryPointerUp(ev);
                    break;
    
                case MotionEvent.ACTION_UP: {
                    pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    if (pointerIndex < 0) {
                        Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
                        return false;
                    }
    
                    final float y = MotionEventCompat.getY(ev, pointerIndex);
                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
                    mIsBeingDragged = false;
                    finishSpinner(overscrollTop);
                    mActivePointerId = INVALID_POINTER;
                    return false;
                }
                case MotionEvent.ACTION_CANCEL:
                    return false;
            }
    
            return true;
        }
    

    同样的道理在MotionEvent.ACTION_DOWN和case MotionEvent.ACTION_CANCEL时不处理事件,交给父类处理。而在MotionEvent.ACTION_MOVE:中获取到与顶端窗口的overscrollTop,如果overscrollTop值大于0就调用moveSpinner(overscrollTop);方法来初始化mCircleView旋转的。最后在MotionEvent.ACTION_UP:抬起事件中,同样获取overscrollTop,且调用finishSpinner(overscrollTop);方法来完成mCircleView的旋转事件并回复一些属性配置值。

    然后我们再来看看finishSpinner(overscrollTop);方法中是怎么处理的。

    private void finishSpinner(float overscrollTop) {
            if (overscrollTop > mTotalDragDistance) {
                setRefreshing(true, true /* notify */);
            } else {
                // cancel refresh
                mRefreshing = false;
                mProgress.setStartEndTrim(0f, 0f);
                Animation.AnimationListener listener = null;
                if (!mScale) {
                    listener = new Animation.AnimationListener() {
    
                        @Override
                        public void onAnimationStart(Animation animation) {
                        }
    
                        @Override
                        public void onAnimationEnd(Animation animation) {
                            if (!mScale) {
                                startScaleDownAnimation(null);
                            }
                        }
    
                        @Override
                        public void onAnimationRepeat(Animation animation) {
                        }
    
                    };
                }
                animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
                mProgress.showArrow(false);
            }
        }
    

    方法里面很简单,if (overscrollTop > mTotalDragDistance) 就调用setRefreshing(true, true /* notify */);用来设置刷新事件的,否则就回复初始前的属性配置值。

    再来看看setRefreshing(true, true)方法:

    private void setRefreshing(boolean refreshing, final boolean notify) {
            if (mRefreshing != refreshing) {
                mNotify = notify;
                ensureTarget();
                mRefreshing = refreshing;
                if (mRefreshing) {
                    animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
                } else {
                    startScaleDownAnimation(mRefreshListener);
                }
            }
        }
    

    也很好理解,因为传进来的refreshing值为true,所以它会调用animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);来开启mCircleView的动画展示,并传进了mRefreshListener监听器,这个监听器是什么呢?来看看

    private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }
    
            @Override
            public void onAnimationRepeat(Animation animation) {
            }
    
            @Override
            public void onAnimationEnd(Animation animation) {
                if (mRefreshing) {
                    // Make sure the progress view is fully visible
                    mProgress.setAlpha(MAX_ALPHA);
                    mProgress.start();
                    if (mNotify) {
                        if (mListener != null) {
                            mListener.onRefresh();
                        }
                    }
                    mCurrentTargetOffsetTop = mCircleView.getTop();
                } else {
                    reset();
                }
            }
        };
    

    它是一个动画监听器,在动画结束时调用mListener.onRefresh();而mListener是一个接口,里面封装了一个onRefresh()的方法,并且它暴露了对外调用的方法setOnRefreshListener(),所以我们可以在Activity中调用该方法可以实现我们自己的逻辑业务。

    ok,到这里,相信大家都知道了wipeRefreshLayout.setOnRefreshListener();的工作原理,那么我们现在来实现我们的刷新功能吧;

    首先,我们的布局文件先把RecyclerView放到SwipeRefreshLayout容器中:

    recycer_view.xml文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:custom="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/srl_refresh"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <android.support.v7.widget.RecyclerView
                android:id="@+id/recycler_view"
                custom:listDividerSize="2dp"
                custom:listDividerBackgroundColor="#FF0000"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
            </android.support.v7.widget.RecyclerView>
        </android.support.v4.widget.SwipeRefreshLayout>
    </LinearLayout>
    

    然后RecycerActivity中配置一些SwipeRefreshLayout属性值,并调用setOnRefreshListener方法并在onRefresh()实现自己的逻辑业务:

    srl_refresh.setColorSchemeResources(android.R.color.holo_blue_light,
                    android.R.color.holo_red_light,android.R.color.holo_orange_light,
                    android.R.color.holo_green_light);
            srl_refresh.setProgressBackgroundColorSchemeResource(android.R.color.white);
            srl_refresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
                @Override
                public void onRefresh() {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            List<String> newDatas = new ArrayList<String>();
                            for (int i = 0; i <5; i++) {
                                int index = i + 1;
                                newDatas.add("new item" + index);
                            }
                            mBaseRecyclerAdapter.addDatas(newDatas);
                            srl_refresh.setRefreshing(false);
                            Toast.makeText(RecycerActivity.this, "更新了五条数据...", Toast.LENGTH_SHORT).show();
                        }
                    }, 5000);
                }
            });
    

    来看看结果吧
    这里写图片描述

    好了,RecyclerView利用SwipeRefreshLayout实现上拉刷新我们已经实现了,并且也带大家看过它的实现原理了,相信大家一定能更好的掌握它了,那么接下来我们就来实现上拉加载了。

    在上一讲中,我们已经实现了在底部添加上了一个FooterView,那么我们现在可以利用它来实现我们的上拉加载。
    其思想我们可以这样设计,当我们滑动到最后一个ItemView时,让它去加载数据,那怎么获取到列表的最后一个ItemView呢?所幸的是,在RecyclerView中封装的LayoutManger子类中有这样的方法可以供我们获取到最后一个ItemView,该方法是findLastVisibleItemPosition();那我们又该怎么监听RecyclerView滑动呢?可以调用它的addOnScrollListener()方法,由此我们找到了解决方案

    mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    if(newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItem + 1 == mBaseRecyclerAdapter.getItemCount()){
                        mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOADING_MORE);
                        new Handler().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                List<String> newDatas = new ArrayList<String>();
                                for (int i = 0; i< 5; i++) {
                                    int index = i +1;
                                    newDatas.add("more item" + index);
                                }
                                if(newDatas == null){
                                    mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOADED_MORE);
                                    return;
                                }
                                mBaseRecyclerAdapter.addMoreDatas(newDatas);
                                mBaseRecyclerAdapter.changeStatus(BaseRecyclerAdapter.LOAD_MORE);
                                Toast.makeText(RecycerActivity.this,"已加载了数据", Toast.LENGTH_SHORT).show();
                            }
                        },1000);
                    }
                }
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    super.onScrolled(recyclerView, dx, dy);
                    lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition();
                }
            });
    

    代码解释:首先我们会在onScrolled方法中回去到最后一行的ItenView,然后再onScrollStateChanged方法中进行必要的判断,如果lastVisibleItem + 1 == mBaseRecyclerAdapter.getItemCount(),那么就可以确定给ItemView是最后一个ItemView,然后就可以用来实现我们的业务逻辑了,在这里我让它新加了5条数据,然后更新Adapter。

    最后在onBindViewHolder稍作修改,如下

     @Override
        public void onBindViewHolder(BaseViewHolderHelper holder, int position) {
            //把每一个itemView设置一个标签,方便以后根据标签获取到该itemView以便做其他事项,比较点击事件
            if(getItemViewType(position) == TYPE_HEADER){
                return;
            }else if(getItemViewType(position) == TYPE_FOOTER){
                FooterViewHolder footViewHolder=(FooterViewHolder)holder;
                footViewHolder.footView.setText("上拉加载更多...");
                switch (status){
                    case LOAD_MORE:
                        footViewHolder.footView.setText("上拉加载更多...");
                        break;
                    case LOADING_MORE:
                        footViewHolder.footView.setText("正在加载中...");
                        break;
                    case LOADED_MORE:
                        footViewHolder.footView.setText("已加载完毕");
                        break;
                }
            } else{
               ...
            }
        }
    

    ok,来看看结果吧:
    这里写图片描述

    好了,已经实现了上拉加载的功能了,相信大家也都可以做很多事情了。

    总结:本节主题是为RecyclerView添加下拉刷新和上拉加载的功能,基本的思路也都已讲清楚了,而且着重的讲解了一下利用SwipeRefreshLayout实现下拉刷新的实现原理,相信大家通过这节更能学到一些原理性的东西,ok,今天就讲到这里吧。祝大家学习愉快。

    更多资讯请关注微信平台,有博客更新会及时通知。爱学习爱技术。

    这里写图片描述

  • 相关阅读:
    mysql数据库连接报错ERRoR1130(HY000)
    Ladap相关知识精简资料
    Github访问慢,解决方案
    IIS短文件漏洞成因,及修复方案
    Kali linux简单匿名处理方法
    NHibernate实践总结(二) 在mapping文件中设置抓取策略对HQL与Criteria造成不同影响的测试与验证
    NHibernate 3.x新功能实践(二) QueryOver(下)
    NHibernate实践总结(三)HQL的thetastyle join对无关联实体的连接与应用
    NHibernate实践总结(一)
    Scott Hanselman's 2009 NET与Windows终极开发工具列表
  • 原文地址:https://www.cnblogs.com/guanmanman/p/6100307.html
Copyright © 2011-2022 走看看