美文网首页Android知识手机移动程序开发开源工具技巧
是时候自己来造个轮子了--增强型RecyclerView

是时候自己来造个轮子了--增强型RecyclerView

作者: sugaryaruan | 来源:发表于2017-01-23 10:48 被阅读413次

    简述

    该增强型RecyclerView,增加了以下特性:

    • 上拉滑动到底部,加载更多
    • 支持添加Header头视图
    • 支持加载数据为空时,显示特定视图
    • 支持拖拽,侧滑删除

    下拉刷新实现通过给RecyclerView包一层SwipRefreshLayout来实现。

    本文重点分享上拉加载更多的实现,同时实现添加头部视图,侧滑,拖拽功能实现,该实现存在以下注意点:

    1. 如何判断RecyclerView滑动到了底部
    2. 通常RecyclerView显示的item布局相同,怎么做到上拉加载更多时出现一个底栏视图
    3. 滑动到底栏出现加载更多动画,这个动画什么时候结束?动画生命周期是?
    4. ReclcerViewyou多种布局,如果是网格布局(有多列),怎么做到让上拉加载更多的动画视图和头部视图占用一整行?
    5. 自定义了RecyclerView,如何做到像使用标准RecyclerView那样使用?
    6. 如何实现item的拖拽和侧滑删除?

    实现的注意点解析

    如何判断RecyclerView滑动到了底部

    关于布局的逻辑设置,就找LayoutManager。的确,通过查阅官方API手册,有findLastVisibleItemPosition()/findLastCompletelyVisibleItePosition(),在滑动监听里,使用这两个方法就能实现判断

    怎么做到上拉加载更多时出现一个底栏视图

    这里涉及到RecyclerView如何实现多布局显示的知识,通过getItemViewType(),底部上拉加载更多视图设定一种ItemViewType值,对应新建一个ViewHolder.

    滑动到底栏出现加载更多动画,这个动画什么时候结束?动画生命周期是?

    动画的开始时刻是列表滑倒底部,当滑倒底部时,在客户类(Fragment/Activity)里接口回调,开始网络请求数据,动画的结束时刻是网络加载完成,刷新列表时

    ReclcerViewyou多种布局,如果是网格布局(有多列),怎么做到让上拉加载更多的动画视图和头部视图占用一整行?

    和布局相关的,找LayoutManager,这里要找GridLayoutManager,它提供了setSpanSizeLookup(GridLayoutManager.SpanSizeLookup),通过这个方法,可以根据位置来设置item占用一整行还是正常显示

    自定义了RecyclerView,如何做到像使用标准RecyclerView那样使用?

    使用装饰器设计模式,能很好的实现对用户透明使用效果

    如何实现item的拖拽和侧滑删除?

    使用android提供的ItemTouchHelper工具类,能快速的实现

    核心代码

    EnhanceRecyclerView

    public class EnhanceRecyclerView extends RecyclerView {
    
    private static final String TAG = "EnhanceRecyclerView";
    
    private OnLoadMoreListener mOnLoadMoreListener;
    private InternalAdapter mInternalAdapter;
    private View mEmptyView;
    private @LayoutRes int mHeaderResId;
    private AdapterDataObserver mAdapterDataObserver = new EnhanceAdapterDataObserver();
    /**
     * 滚动方向
     */
    private int mScrollDy = 0;
    
    public EnhanceRecyclerView(Context context) {
        super(context);
    }
    
    public EnhanceRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    
    public EnhanceRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    
    
    @Override
    public void onScrolled(int dx, int dy) {
        super.onScrolled(dx, dy);
        mScrollDy = dy;
    }
    
    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);
        switch (state) {
            case SCROLL_STATE_IDLE:
                LayoutManager layoutManager = getLayoutManager();
                int itemCount = getAdapter().getItemCount();
                int lastVisibleItemPosition = 0;
    
                if (layoutManager instanceof GridLayoutManager) {
                    GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
                    lastVisibleItemPosition = gridLayoutManager.findLastVisibleItemPosition();
                } else if (layoutManager instanceof LinearLayoutManager) {
                    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                    lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
                }
    
                if (lastVisibleItemPosition >= itemCount - 1) {
                    if (getParent() instanceof SwipeRefreshLayout) {
                        SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) getParent();
                        if (swipeRefreshLayout.isRefreshing()) {
                            break;
                        }
                    }
                    if (mOnLoadMoreListener != null && mScrollDy > 0) {
                        mInternalAdapter.setLoadingIndicatorViewVisible(VISIBLE);
                        mOnLoadMoreListener.onLoadMore();
                    }
                }
                break;
        }
    }
    
    
    /**
     * 重写此方法,设置GridLayout的上拉加载更多视图的位置
     *
     * @param layout
     */
    @Override
    public void setLayoutManager(LayoutManager layout) {
        if (layout instanceof GridLayoutManager) {
            final GridLayoutManager externalGridLayoutManager = (GridLayoutManager) layout;
            final int spanCount = externalGridLayoutManager.getSpanCount();
            int orientation = externalGridLayoutManager.getOrientation();
    
            final GridLayoutManager innerGridLayoutManager = new GridLayoutManager(getContext(), spanCount, orientation, false);
            innerGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int headerViewCount = mInternalAdapter.getHeaderViewCount();
                    int footViewCount = mInternalAdapter.getFootViewCount();
                    if (position < headerViewCount) {
                        return spanCount;
                    }
    
                    int totalItemCount = innerGridLayoutManager.getItemCount();
                    if (position >= totalItemCount - footViewCount) {
                        return spanCount;
                    }
    
                    return externalGridLayoutManager.getSpanSizeLookup().getSpanSize(position - headerViewCount);
                }
            });
            super.setLayoutManager(innerGridLayoutManager);
        } else {
            super.setLayoutManager(layout);
        }
    }
    
    public View getEmptyView() {
        return mEmptyView;
    }
    
    public final void setEmptyView(View emptyView) {
        mEmptyView = emptyView;
        setupEmptyViewHierarchy(emptyView);
    }
    
    protected void setupEmptyViewHierarchy(View emptyView) {
        ((ViewGroup) getParent().getParent()).addView(emptyView,0);
    }
    
    public void addHeaderResId(@LayoutRes int resId) {
        mHeaderResId = resId;
        if (mInternalAdapter != null) {
            mInternalAdapter.setExternalHeaderResId(resId);
        }
    }
    
    
    
    @Override
    public void setAdapter(Adapter adapter) {
        mInternalAdapter = new InternalAdapter(adapter);
        super.setAdapter(mInternalAdapter);
        //addHeaderView方法依赖于setAdapter方法
        if (mHeaderResId > 0) {
            addHeaderResId(mHeaderResId);
        }
        mInternalAdapter.registerAdapterDataObserver(mAdapterDataObserver);
        mAdapterDataObserver.onChanged();
    }
    
    public void setOnLoadMoreListener(OnLoadMoreListener onLoadMoreListener) {
        mOnLoadMoreListener = onLoadMoreListener;
    }
    
    
    public void loadMoreOnSuccess() {
        if (mInternalAdapter != null) {
            mInternalAdapter.loadMoreOnSuccess();
        }
    }
    
    public void loadMoreOnError() {
        if (mInternalAdapter != null) {
            mInternalAdapter.loadMoreOnError();
        }
    }
    
    public void loadMoreOnComplete() {
        if (mInternalAdapter != null) {
            mInternalAdapter.loadMoreOnComplete();
        }
    }
    
    
    public final void notifyDataSetChanged() {
        mInternalAdapter.notifyDataSetChanged();
    }
    
    public final void notifyItemChanged(int position) {
        mInternalAdapter.notifyItemChanged(position);
    }
    
    public final void notifyItemChanged(int position, Object payload) {
        position = position + mInternalAdapter.getHeaderViewCount();
        mInternalAdapter.notifyItemChanged(position, payload);
    }
    
    public final void notifyItemRangeChanged(int positionStart, int itemCount) {
        positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
        mInternalAdapter.notifyItemRangeChanged(positionStart, itemCount);
    }
    
    public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
        positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
        mInternalAdapter.notifyItemRangeChanged(positionStart, itemCount, payload);
    }
    
    public final void notifyItemInserted(int position) {
        position = position + mInternalAdapter.getHeaderViewCount();
        mInternalAdapter.notifyItemInserted(position);
    }
    
    public final void notifyItemMoved(int fromPosition, int toPosition) {
        fromPosition = fromPosition + mInternalAdapter.getHeaderViewCount();
        toPosition = toPosition + mInternalAdapter.getHeaderViewCount();
        mInternalAdapter.notifyItemMoved(fromPosition, toPosition);
    }
    
    public final void notifyItemRangeInserted(int positionStart, int itemCount) {
        positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
        mInternalAdapter.notifyItemRangeInserted(positionStart, itemCount);
    }
    
    public final void notifyItemRemoved(int position) {
        position = position + mInternalAdapter.getHeaderViewCount();
        mInternalAdapter.notifyItemRemoved(position);
    }
    
    public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
        positionStart = positionStart + mInternalAdapter.getHeaderViewCount();
        mInternalAdapter.notifyItemRangeRemoved(positionStart, itemCount);
    }
    
    
    public InternalAdapter getInternalAdapter() {
        return mInternalAdapter;
    }
    
    /**
     * 上拉加载更多回调
     */
    public interface OnLoadMoreListener {
        void onLoadMore();
    }
    
    private class EnhanceAdapterDataObserver extends AdapterDataObserver {
    
        @Override
        public void onChanged() {
            super.onChanged();
            if (getEmptyView() != null && getAdapter() != null) {
                int itemCount = getAdapter().getItemCount();
                if (itemCount == 0) {
                    getEmptyView().setVisibility(VISIBLE);
                    setVisibility(GONE);
                } else {
                    getEmptyView().setVisibility(GONE);
                    setVisibility(VISIBLE);
                }
            }
        }
    
        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            super.onItemRangeChanged(positionStart, itemCount);
            onChanged();
        }
    
        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            super.onItemRangeChanged(positionStart, itemCount, payload);
            onChanged();
        }
    
        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            onChanged();
        }
    
        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            super.onItemRangeRemoved(positionStart, itemCount);
            onChanged();
        }
    
        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            super.onItemRangeMoved(fromPosition, toPosition, itemCount);
            onChanged();
        }
    }
    }
    

    InternalAdapter

    public class InternalAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
    private static final String TAG = "InternalAdapter";
    
    private static final int HEADER_ITEM_TYPE = 170118;
    private static final int FOOTER_ITEM_TYPE = 170116;
    
    private RecyclerView.Adapter<RecyclerView.ViewHolder> mExternalAdapter;
    private int mBodyItemCount;
    private FooterView mFooterView;
    private @LayoutRes int mExternalHeaderResId;
    
    
    public InternalAdapter(RecyclerView.Adapter<RecyclerView.ViewHolder> externalAdapter) {
        mExternalAdapter = externalAdapter;
        mBodyItemCount = externalAdapter.getItemCount();
    }
    
    
    @Override
    public int getItemViewType(int position) {
        if(isHeaderView(position)){
            return HEADER_ITEM_TYPE;
        }
    
        else if (isFootView(position)) {
            return FOOTER_ITEM_TYPE;
        }
        return mExternalAdapter.getItemViewType(position);
    }
    
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch (viewType) {
            case HEADER_ITEM_TYPE:
                View headerView = LayoutInflater.from(parent.getContext()).inflate(mExternalHeaderResId, parent, false);
                return new HeaderView(headerView);
            case FOOTER_ITEM_TYPE:
                View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_footer_indicator, parent, false);
                mFooterView = new FooterView(view);
                return mFooterView;
            default:
                return mExternalAdapter.onCreateViewHolder(parent, viewType);
        }
    }
    
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if(isHeaderView(position)){
            return;
        }
        if (isFootView(position)) {
            return;
        }
        if(mExternalHeaderResId > 0){
            position = position - getHeaderViewCount();
        }
        mExternalAdapter.onBindViewHolder(holder, position);
    }
    
    @Override
    public int getItemCount() {
        mBodyItemCount = mExternalAdapter.getItemCount();
        if(mBodyItemCount == 0){
            return 0;
        }
        else{
            return getHeaderViewCount() + mBodyItemCount + getFootViewCount();
        }
    }
    
    
    
    
    
    private boolean isHeaderView(int position){
        return mExternalHeaderResId > 0 && position == 0;
    }
    
    private boolean isFootView(int position) {
        return (position >= mBodyItemCount + getHeaderViewCount());
    }
    
    public int getFootViewCount() {
        return 1;
    }
    
    public int getHeaderViewCount(){
        return mExternalHeaderResId > 0 ? 1 : 0;
    }
    
    
    
    
    
    public void setLoadingIndicatorViewVisible(int visible){
        if(mFooterView != null){
            mFooterView.setLoadingIndicatorViewVisible(visible);
        }
    }
    
    public void setExternalHeaderResId(int externalHeaderResId) {
        mExternalHeaderResId = externalHeaderResId;
    }
    
    public void loadMoreOnSuccess(){
        setLoadingIndicatorViewVisible(View.GONE);
    }
    
    public void loadMoreOnError(){
        setLoadingIndicatorViewVisible(View.GONE);
    }
    
    public void loadMoreOnComplete(){
        setLoadingIndicatorViewVisible(View.GONE);
    }
    
    
    
    
    
    
    static class HeaderView extends RecyclerView.ViewHolder{
    
        HeaderView(View itemView) {
            super(itemView);
        }
    }
    
    static class FooterView extends RecyclerView.ViewHolder {
    
        @Bind(R.id.item_footer_indicator)
        LoadingIndicatorView mLoadingIndicatorView;
    
        FooterView(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            mLoadingIndicatorView.setVisibility(View.GONE);
        }
    
        void setLoadingIndicatorViewVisible(int visible){
            mLoadingIndicatorView.setVisibility(visible);
        }
    }
    }
    

    底部FooterView的布局item_footer_indicator.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:background="@android:color/transparent"
    >
    
    <com.sugary.refreshrecyclerview.enhancerecycler.indicator.LoadingIndicatorView
        android:id="@+id/item_footer_indicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        app:indicator_color="@color/indicator_loading_more_orange"
        />
    
    </RelativeLayout>
    

    LoadingIndicatorView

    public class LoadingIndicatorView extends View {
    
    //Sizes (with defaults in DP)
    public static final int DEFAULT_SIZE = 50;
    
    private Paint mPaint;
    
    private BaseIndicatorController mIndicatorController;
    
    private boolean mHasAnimation;
    
    
    public LoadingIndicatorView(Context context) {
        this(context, null);
    }
    
    public LoadingIndicatorView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public LoadingIndicatorView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }
    
    private void init(AttributeSet attrs) {
    
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.LoadingIndicatorView);
        int indicatorColor = a.getColor(R.styleable.LoadingIndicatorView_indicator_color, Color.GRAY);
        a.recycle();
    
        mPaint = new Paint();
        mPaint.setColor(indicatorColor);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
    
        mIndicatorController = new BallPulseIndicator();
        mIndicatorController.setTarget(this);
    }
    
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = measureDimension(dp2px(DEFAULT_SIZE), widthMeasureSpec);
        int height = measureDimension(dp2px(DEFAULT_SIZE), heightMeasureSpec);
        setMeasuredDimension(width, height);
    }
    
    private int measureDimension(int defaultSize, int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else if (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(defaultSize, specSize);
        } else {
            result = defaultSize;
        }
        return result;
    }
    
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (!mHasAnimation) {
            mHasAnimation = true;
            mIndicatorController.initAnimation();
        }
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawIndicator(canvas);
    }
    
    
    
    
    
    void drawIndicator(Canvas canvas) {
        mIndicatorController.draw(canvas, mPaint);
    }
    
    
    private int dp2px(int dpValue) {
        return (int) getContext().getResources().getDisplayMetrics().density * dpValue;
    }
    
    
    
    
    
    @Override
    public void setVisibility(int v) {
        if (getVisibility() != v) {
            super.setVisibility(v);
            if (v == GONE || v == INVISIBLE) {
                mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.END);
            } else {
                mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START);
            }
        }
    }
    
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mHasAnimation) {
            mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.START);
        }
    }
    
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mIndicatorController.setAnimationStatus(BaseIndicatorController.AnimStatus.CANCEL);
    }
    
    }
    

    小结:

    列表数据刷新,改成了调用EnhanceRecylerView方法,用自己建的Adapter刷新数据无效(这是这个轮子的缺陷,有待改进)。

    底部滑动动画使用了他人的开源动画

    在自制增强型RecyclerView过程中,也刷了一些资料,推荐阅读。

    参考资料

    RecyclerView必知必会(五星推荐)

    Github:RecyclerView优秀文集

    Github:BeautifulRefreshLayout

    Github:BaseRecyclerViewAdapterHelper

    Github:XRecyclerView

    相关文章

      网友评论

        本文标题:是时候自己来造个轮子了--增强型RecyclerView

        本文链接:https://www.haomeiwen.com/subject/szkabttx.html