业余时间:RecyclerView的封装

本文介绍了一种基于RecyclerView的刷新加载控件的封装方法,详细解析了刷新头部的触摸事件处理及高度变化计算,并展示了如何通过适配器添加头部和尾部。

前言


上一篇已经实现了头部和尾部的加载标识,接下来只需要将它们与RecyclerView组合封装就OK了,不得不说自己要去封装一个好用的刷新加载控件还是得费好多心思去实现和优化的。

PS:更好的封装方式,并已提交到JCenter上:http://blog.youkuaiyun.com/hzwailll/article/details/75285924

Begin


刷新头部比较麻烦点,当然也是重点,需要处理touch和控件高度,这里我的思路是:

1. 当头部处于刷新状态时不在接收任何触摸事件

2. 当刷新头部处于当前列表在屏幕上的第一个Item并且有下拉手势时开始响应下拉事件,当然这也是必须的

3. 刷新头部内部只处理触摸事件,是否处于头部在RecyclerView中判断

4. 通过手指在屏幕Y方向的距离计算刷新头部的高度,通过刷新头部的高度计算时钟的角度


刷新头部的触摸事件处理如下:

    protected void touch(MotionEvent event, int appbarState) {
        //如果当前正在刷新,不接收任何的触摸事件
        if (currentState == STATE_REFRESH) return;
        if (event.getAction() == MotionEvent.ACTION_MOVE) {
            if (isFirstMove) {
                lastY = event.getRawY();
                isFirstMove = false;
            }
            float delaY = (event.getRawY() - lastY) / 3;
            if (delaY > 0 || getCurrentHeight() > 0) {
                currentHeight = (int) (delaY + getCurrentHeight());
                clockView.setClockAngle(currentHeight);//设置时钟的角度
                currentState = STATE_PREPARE;//当前的刷新状态为准备状态
                if (delaY > 0 && appbarState == XRecyclerView.APP_BAR_NORMAL) {//下拉
                    changeHeight(currentHeight);
                } else if (delaY < 0) {//上滑
                    layout(0, currentHeight, width, currentHeight);
                    changeHeight(currentHeight);
                }
                changeRefreshState(true);
            }
        } else if (event.getAction() == MotionEvent.ACTION_UP) {
            isFirstMove = true;
            changeRefreshState(false);
        }
        lastY = event.getRawY();
    }

    private void changeHeight(int height) {
        LayoutParams params = (LayoutParams) refreshView.getLayoutParams();
        params.height = height > 0 ? height : 0;
        params.width = width;
        refreshView.setLayoutParams(params);
    }

这里的touch事件是从RecyclerView中传递过来的,同时传递过来的还有AppbarLayout的状态,这个稍后再说。这里通过float delaY = (event.getRawY() - lastY) / 3计算在Y方向的移动距离,除以3以减小移动距离。刷新状态共有三种,分别为正常状态,准备状态,也就是刷新头部响应触摸事件且手指还在屏幕上的状态,还有正在刷新的状态。当手指离开屏幕时高度的渐变,通过属性动画来实现,并根据起始与结束值在动画结束时更改刷新头部处于的对应状态

    private void changeHeightAnim(final int start, final int end) {
        if (animator == null || !animator.isRunning()) {
            animator = ValueAnimator.ofInt(start, end);
            animator.setDuration(300).setInterpolator(new DecelerateInterpolator(1.2f));
            animator.start();
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int value = (int) animation.getAnimatedValue();
                    changeHeight(value);
                    if (start == currentHeight && end == initHeight) {
                        clockView.setClockAngle(value);
                    }
                }
            });
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    if (end == (int) refreshHeight) {
                        currentState = STATE_REFRESH;
                    } else if (end == initHeight) {
                        currentState = STATE_NORMAL;
                        clockView.stopClockAnim();
                    }
                }
            });
        }
    }

为了效果好看点,这里设置了一个减速的插值器DecelerateInterpolator,并设置因子为1.2,这个通过http://inloop.github.io/interpolator/这个网站可以很方便的查看不同的插值器对应的曲线,强烈推荐。


接下来就是RecyclerView的封装,这里取一个高逼格的名字就叫XRecyclerView,当然此XRecyclerView非彼XRecyclerView,网上的XRecyclerView(https://github.com/jianghejie/XRecyclerView)是github上开源的封装RecyclerView实现的刷新加载控件,功能齐全,很不错!

这里刷新头部和加载尾部在RecyclerView的添加有两种方式,一种是采用装饰者模式,在RecyclerView内部构建一个适配器并添加头部尾部,另外一种嘛就是封装一个可添加头部和尾部的适配器,在RecyclerView内部强转,然后添加头部和尾部,但耦合性高。归根到底,实际上就是一种方式:即通过适配器添加头部尾部。这里使用的是第二种,因为第一种添加方式在实际使用过程中发现发现很不容扩展,与封装的适配器存在下标冲突,所以当使用装饰着模式添加头部尾部时,我只能老老实实的写RecyclerView的原生的适配器,而不能用自己封装的,这个是我忍受不了了的,毕竟会多出很多代码。两者共用的方式我目前没有好的解决方法,只能用这种笨方法了。


初始化刷新头部和加载尾部:

    private void initRefresh() {
        isRefreshEnable = true;
        if (refreshHeader == null) {
            refreshHeader = new RefreshHeader(getContext(), refreshHeight);
        }
        refreshHeader.setRefreshListener(new RefreshHeader.RefreshListener() {
            @Override
            public void refresh() {
                loadingListener.refresh();
            }
        });
    }

    private void initLoading() {
        isLoadMoreEnable = true;
        if (loadingFooter == null) {
            loadingFooter = new LoadingFooter(getContext(), loadingHeight);
        }
        loadingFooter.setVisibility(GONE);
        LayoutManager manager = getLayoutManager();
        if (manager instanceof LinearLayoutManager) {
            //只支持LinearLayoutManager和GridLayoutManager布局,不支持StaggeredGridLayoutManager
            final LinearLayoutManager layoutManager = (LinearLayoutManager) manager;
            addOnScrollListener(new OnScrollListener() {
                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    super.onScrolled(recyclerView, dx, dy);
                    judgeLastItem(layoutManager.findLastVisibleItemPosition(), dy);
                }
            });
        }
    }

通过重写setAdapter方法,添加头部和尾部,并注册数据变化的观察者,通过AdapterDataObserver可以监听RecyclerView的数据变化,从而设置对应的空值界面的显示与隐藏:

    private AdapterDataObserver mObserver = new AdapterDataObserver() {
        @Override
        public void onChanged() {
            super.onChanged();
            checkEmpty();
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            checkEmpty();
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            super.onItemRangeRemoved(positionStart, itemCount);
            checkEmpty();
        }
    };
    @Override
    public void setAdapter(Adapter adapter) {
        if (this.getAdapter() != null) {
            this.getAdapter().unregisterAdapterDataObserver(mObserver);
        }
        super.setAdapter(adapter);
        if (refreshHeader == null || loadingFooter == null) {
            refreshHeader = new RefreshHeader(getContext(), refreshHeight);
            loadingFooter = new LoadingFooter(getContext(), loadingHeight);
        }
        ((BaseRVAdapter) getAdapter()).addHeaderView(refreshHeader);
        ((BaseRVAdapter) getAdapter()).addFooterView(loadingFooter);
        adapter.registerAdapterDataObserver(mObserver);
    }
继续:
接下来处理touch事件,但是需要注意的是,当AppbarLayout作为RecyclerView父View与CoordinatorLayout协同处理滑动时会有手势冲突,可以通过监听AppbarLayout的滑动来处理,思路很简单:找到AppbarLayout,设置偏移监听器:
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (appBarLayout == null) {
            ViewParent parent = getParent();
            while (parent != null) {
                if (parent instanceof CoordinatorLayout) break;
                parent = parent.getParent();
            }
            if (parent != null) {
                CoordinatorLayout layout = (CoordinatorLayout) parent;
                appBarLayout = null;
                for (int i = 0; i < layout.getChildCount(); i++) {
                    View child = layout.getChildAt(i);
                    if (child instanceof AppBarLayout) {
                        appBarLayout = (AppBarLayout) child;
                        break;
                    }
                }
                if (appBarLayout != null) {
                    appBarLayout.addOnOffsetChangedListener(this);
                }
            }
        }
    }
    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
        appBarState = verticalOffset == 0 ? APP_BAR_NORMAL : APP_BAR_UP;
    }

最后一步:处理RecyclerView的onTouchEvent事件:

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        if (isRefreshEnable && isTop()) {//处理刷新头部
            refreshHeader.touch(e, appBarState);
        }
        return super.onTouchEvent(e);
    }


还有一些无关紧要的方法,就不贴代码了,整体来说难度不大,但还是要花费不少心思去琢磨细节和优化代码,刷新头部的触摸事件前前后后写了好几个版本,有的版本在使用的时候才发现,从最开始的思路就是错的,有的版本觉得刷新头部和RecyclerView的耦合性太高了,几乎把刷新头部的刷新处理全写在了RecyclerView里,不利于刷新头部的更换和扩展,当前版本算是比较满意的一个版本,当然可能也有很多潜在问题,后续会一直改进,说不定哪一天就用在真实项目上了,哈哈!


End


在封装的实现过程中遇到了很多不明白的地方,在很多方法中寻求最佳的解决方案,以更少的代码实现更好的功能,这是我的追求!虽然达不到我师父那种每天除了吃饭睡觉都是在写代码的状态,但是也不能差太多!


最后附上使用装饰者模式实现添加头和尾部的代码:

private class WrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

        private static final int HEADER_TYPE = 100;
        private static final int FOOTER_TYPE = 101;
        private Adapter adapter;

        WrapperAdapter(Adapter adapter) {
            this.adapter = adapter;
        }

        private boolean isHeader(int position) {
            return position < getHeaderCount();
        }

        private boolean isFooter(int position) {
            return position >= (getDataCount() + getHeaderCount());
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == HEADER_TYPE) {
                return com.hzw.freetime.adapter.ViewHolder.getViewHolder(refreshHeader);
            } else if (viewType == FOOTER_TYPE) {
                return com.hzw.freetime.adapter.ViewHolder.getViewHolder(loadingFooter);
            }
            return adapter.onCreateViewHolder(parent, viewType);
        }

        @SuppressWarnings("unchecked")
        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            if (isHeader(position) || isFooter(position)) return;
            adapter.onBindViewHolder(holder, position - getHeaderCount());
        }

        @Override
        public int getItemViewType(int position) {
            if (isHeader(position)) {
                return HEADER_TYPE;
            } else if (isFooter(position)) {
                return FOOTER_TYPE;
            }
            return adapter.getItemViewType(position - getHeaderCount());
        }

        @Override
        public int getItemCount() {
            return getDataCount() + getHeaderCount() + getFooterCount();
        }

        private int getHeaderCount() {
            return refreshHeader == null ? 0 : 1;
        }

        private int getFooterCount() {
            return loadingFooter == null ? 0 : 1;
        }

        private int getDataCount() {
            return adapter.getItemCount();
        }
    }

    @Override
    public void setAdapter(Adapter adapter) {
        if (this.getAdapter() != null) {
            this.getAdapter().unregisterAdapterDataObserver(mObserver);
        }
	if (refreshHeader == null || loadingFooter == null) {
            refreshHeader = new RefreshHeader(getContext(), refreshHeight);
            loadingFooter = new LoadingFooter(getContext(), loadingHeight);
        }
        WrapperAdapter adapter1 = new WrapperAdapter(adapter);
        super.setAdapter(adapter1);
        adapter.registerAdapterDataObserver(mObserver);
    }

OVER
















评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值