MaoRecyclerView


Vimous · 2016-06-21 · Android

Android RecyclerView已经出来很久了, 官方推荐来替换原有的ListView.

现在项目中的ListView使用的是第三方框架PullToRefresh来实现View的下拉刷新和加载更多. 虽然MaterialDesign有自己的一套刷新设计, 但是产品还是要求UI使用仿照IOS的下拉刷新T^T. So, 想用RecyclerView, 就只好自己实现下拉刷新和上拉加载了.

下拉刷新

1. 下拉刷新View的几种状态:
  • IDLE(空闲)
  • PULL(下拉)
  • PULL_TO_REFRESH(可刷新)
  • REFRESHING(刷新中)
  • COMPLETED(刷新完成)
2. View在拉取的时候弹性处理

采用的方法是 根据RecyclerView传过来的滑动坐标差值来针对View高度做属性动画. 抽象关键代码如下:

public abstract class MaoRefreshView extends LinearLayout {
    private static final int COMPLETE_TIME = 1000; //完成等待时间

    private int mHeight;

    private enum State {IDLE, PULL, PULL_TO_REFRESH, REFRESHING, COMPLETED}
    private State mState = State.IDLE; //初始化状态

    public MaoRefreshView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //在要实现的View外层包装一层View 下拉时可向下拉动
        LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        lp.setMargins(0, 0, 0, 0);
        this.setLayoutParams(lp);
        this.setPadding(0, 0, 0, 0);

        addView(onCreateView(),
            new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        setGravity(Gravity.BOTTOM);

        measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        mHeight = getMeasuredHeight();
        setVisibleHeight(0);
    }

    private void setState(State state){ //状态管理
        if(state == mState) return;
        mState = state;
        switch (state){
            case IDLE: smoothScrollTo(0); break;
            case PULL: onPull(); break;
            case PULL_TO_REFRESH: onPullToRefresh(); break;
            case REFRESHING: onRefresh(); break;
            case COMPLETED: onCompleted(); finishComplete(); break;
        }
    }

    private void setVisibleHeight(int height) {
        if (height < 0) height = 0;
        ViewGroup.LayoutParams lp = getLayoutParams();
        lp.height = height;
        setLayoutParams(lp);
    }

    private int getVisibleHeight() {
        return getLayoutParams().height;
    }

    private void smoothScrollTo(int destHeight) {
        ValueAnimator animator = ValueAnimator.ofInt(getVisibleHeight(), destHeight);
        animator.setDuration(300).start();
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation)
            {
                setVisibleHeight((int) animation.getAnimatedValue());
            }
        });
        animator.start();
    }

    protected abstract View onCreateView();

    protected abstract void onPull();

    protected abstract void onPullToRefresh();

    protected abstract void onRefresh();

    protected abstract void onCompleted();

    private void finishComplete(){
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                setState(State.IDLE);
            }
        }, COMPLETE_TIME);
    }

    /**
     * 下拉
     * @param offsetY //下拉Y轴偏移
     * @return true//如果正常消费了下拉事件
     * */
    public boolean pull(float offsetY){
        if(offsetY > 0){
            if(!((mState == State.REFRESHING || mState == State.COMPLETED) && offsetY < mHeight))
                setVisibleHeight((int) offsetY);
            if(mState == State.IDLE) {
                setState(State.PULL);
                return true;
            }
            if(mState == State.PULL && getVisibleHeight() >= mHeight){
                setState(State.PULL_TO_REFRESH);
                return true;
            }
            if(mState == State.PULL_TO_REFRESH && getVisibleHeight() < mHeight){
                setState(State.PULL);
            }
            return true;
        }
        return false;
    }

    /**
     * 释放
     * @return true//如果释放后刷新
     * */
    public boolean release(){
        if(mState == State.PULL_TO_REFRESH){
            setState(State.REFRESHING);
            smoothScrollTo(mHeight);
            return true;
        }
        if(mState == State.PULL)
            setState(State.IDLE);
        else
            smoothScrollTo(mHeight);
        return false;
    }

    /**
     * 刷新完成
     * */
    public boolean complete(){
        if(mState == State.REFRESHING ||
            mState == State.COMPLETED) {
            setState(State.COMPLETED);
            return true;
        }
        return false;
    }
}

加载更多

加载更多需要实现三种状态: + 空闲 + 加载中 + 没有更多了

相关代码逻辑和下拉刷新的View逻辑是类似的, 不在重复代码

事件传递

接下来是处理Recycler中的事件传递, 主要处理在顶部下拉事件时触发下拉刷新和最后n个Item可见时进行加载更多. 只需要重写Recyclerview的onTouchEventonScrollStateChanged方法就好了 在这里有一点一定要注意, 不能仅仅在MotionEvent.ACTION_DOWN中取得坐标来计算差值, 在实际中ItemView可能会消费掉事件, 导致坐标值取不到出现错误, 所以要在TouchEvent中直接去坐标差值. 关键代码如下:

    private float mDownY = -1;
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if(mDownY == -1) mDownY = ev.getRawY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownY = ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                final float offsetY = ev.getRawY() - mDownY;
                if (isOnTop() && isRefreshEnable) {
                    if(mHeaderView.pull(offsetY / RATE))
                        return true;
                }
                if(isOnBottom() && isLoadMoreEnable && isNoMore){
                    mFooterView.setHeight(-(int)offsetY/RATE);
                }
                break;
            default:
                mDownY = -1;
                if (isOnTop() && isRefreshEnable && mHeaderView.release() &&
                        mRecyclerListener != null) {
                    mRecyclerListener.onRefresh();
                    isRefreshing = true;
                }
                if(isOnBottom() && isLoadMoreEnable && isNoMore){
                    mFooterView.hide();
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        if(state != RecyclerView.SCROLL_STATE_IDLE || mRecyclerListener == null ||
            !isLoadMoreEnable || isLoadMoring || isNoMore) return;

        if(isRefreshing) {
            mFooterView.setGone();
            return;
        }
        mFooterView.setVisiable();

        LayoutManager layoutManager = getLayoutManager();
        if(!(layoutManager instanceof  LinearLayoutManager)) return;
        int last = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();

        if (layoutManager.getChildCount() > 0 &&
            layoutManager.getItemCount() > layoutManager.getChildCount() &&
            last >= layoutManager.getItemCount() - 1){

            mFooterView.loadMore();
            mRecyclerListener.onLoadMore();
            isLoadMoring = true;
        }
    }

AdapterWrapper

Recycler中没有HeaderView和FooterView 我们只能通过增加ItemView的Type来实现增加Header和Footer 抽象出一个Adapter来实现该功能:

private class WrapAdapter extends RecyclerView.Adapter<ViewHolder> {

        private RecyclerView.Adapter<ViewHolder> mAdapter;

        @SuppressWarnings("unchecked")
        public WrapAdapter(RecyclerView.Adapter adapter) {
            mAdapter = adapter;
        }

        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
        }

        @Override
        public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
            super.onViewAttachedToWindow(holder);
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if(viewType < TYPE_BASE){
                if(viewType >= TYPE_FOOTER){
                    return new SimpleViewHolder(mFooterViews.get(viewType - TYPE_FOOTER));
                }
                if(viewType >= TYPE_HEADER){
                    return new SimpleViewHolder(mHeaderViews.get(viewType - TYPE_HEADER));
                }
                throw new IllegalStateException();
            }
            return mAdapter.onCreateViewHolder(parent, viewType - TYPE_BASE);
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if(mAdapter != null && getItemViewType(position) >= TYPE_BASE)
                mAdapter.onBindViewHolder(holder, position - getHeadersCount());
        }

        @Override
        public int getItemCount() {
            return getHeadersCount() + getFootersCount() +
                (mAdapter != null ? mAdapter.getItemCount() : 0);
        }

        public int getHeadersCount() {
            return mHeaderViews.size();
        }

        public int getFootersCount() {
            return mFooterViews.size();
        }

        @Override
        public int getItemViewType(int position) {
            if(position < 0 || position >= getItemCount()) return -1;
            if(position < getHeadersCount()){
                return TYPE_HEADER + position;
            }
            if(position >= getItemCount() - getFootersCount()){
                return TYPE_FOOTER + getItemCount() - position - 1;
            }
            return mAdapter == null ? -1 :
                (TYPE_BASE + mAdapter.getItemViewType(position - getHeadersCount()));
        }

        @Override
        public long getItemId(int position) {
            if(mAdapter == null || position < getHeadersCount() ||
                position >= mAdapter.getItemCount() + getHeadersCount()) return -1;
            return mAdapter.getItemId(position - getHeadersCount());
        }

        @Override
        public void unregisterAdapterDataObserver(AdapterDataObserver observer) {
            if (mAdapter != null) {
                mAdapter.unregisterAdapterDataObserver(observer);
            }
        }

        @Override
        public void registerAdapterDataObserver(AdapterDataObserver observer) {
            if (mAdapter != null) {
                mAdapter.registerAdapterDataObserver(observer);
            }
        }

        private class SimpleViewHolder extends RecyclerView.ViewHolder {
            public SimpleViewHolder(View itemView) {
                super(itemView);
            }
        }
    }

分割线

RecyclerView中是没有ListView的分割线的, 我们要实现分割线效果就要自己实现Decoration

public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
    private int space;

    public SpacesItemDecoration(int space) {
        this.space = space;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {

        // Add top margin only for the first item to avoid double space between items
        if(parent.getChildLayoutPosition(view) > 1 ||
            parent.getChildLayoutPosition(view) < parent.getChildCount())
            outRect.top = space;
    }
}

运行截图