(二十七)RecyclerView 常用封装、修复及优化

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、RecycleView 的分割线

RecycleView 的分割线需要自定义控件去实现,继承一个 RecyclerView.ItemDecoration 的抽象类,这个网上有较多的实现类,不记录。

这边要采用的分割线方式是横线绘制,模仿 LinearLayoutCompat 去实现的。横线分割线有上下左右四个坐标,然后会有一个横线的厚度。然后在 RecyclerView 的 canvas 上面画这个矩形,矩形填充的内容可以是我们定义的 drawable。

LinearLayoutCompat 绘制分割线核心代码:

    void drawDividersVertical(Canvas canvas) {
        final int count = getVirtualChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);

            if (child != null && child.getVisibility() != GONE) {
                if (hasDividerBeforeChildAt(i)) {
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    final int top = child.getTop() - lp.topMargin - mDividerHeight;
                    drawHorizontalDivider(canvas, top);
                }
            }
        }

        if (hasDividerBeforeChildAt(count)) {
            final View child = getVirtualChildAt(count - 1);
            int bottom = 0;
            if (child == null) {
                bottom = getHeight() - getPaddingBottom() - mDividerHeight;
            } else {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                bottom = child.getBottom() + lp.bottomMargin;
            }
            drawHorizontalDivider(canvas, bottom);
        }
    }

    void drawDividersHorizontal(Canvas canvas) {
        final int count = getVirtualChildCount();
        final boolean isLayoutRtl = ViewUtils.isLayoutRtl(this);
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);

            if (child != null && child.getVisibility() != GONE) {
                if (hasDividerBeforeChildAt(i)) {
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    final int position;
                    if (isLayoutRtl) {
                        position = child.getRight() + lp.rightMargin;
                    } else {
                        position = child.getLeft() - lp.leftMargin - mDividerWidth;
                    }
                    drawVerticalDivider(canvas, position);
                }
            }
        }

        if (hasDividerBeforeChildAt(count)) {
            final View child = getVirtualChildAt(count - 1);
            int position;
            if (child == null) {
                if (isLayoutRtl) {
                    position = getPaddingLeft();
                } else {
                    position = getWidth() - getPaddingRight() - mDividerWidth;
                }
            } else {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (isLayoutRtl) {
                    position = child.getLeft() - lp.leftMargin - mDividerWidth;
                } else {
                    position = child.getRight() + lp.rightMargin;
                }
            }
            drawVerticalDivider(canvas, position);
        }
    }

    void drawHorizontalDivider(Canvas canvas, int top) {
        mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
                getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
        mDivider.draw(canvas);
    }

    void drawVerticalDivider(Canvas canvas, int left) {
        mDivider.setBounds(left, getPaddingTop() + mDividerPadding,
                left + mDividerWidth, getHeight() - getPaddingBottom() - mDividerPadding);
        mDivider.draw(canvas);
    }

具体实现可以参考鸿洋大神的博客:http://blog.csdn.net/lmj623565791/article/details/45059587

二、RecyclerView 添加 header 和 footer

1.方案一

在 Adapter 里面添加 Type 来动态加载 header 和 footer。这种比较简单,多定义两个类型,根据 item 下标去判断进行加载布局的 Type。(谷歌推荐就是这样处理

2.方案二

仿照 ListView,让用户自定义 HeaderVIew 和 FooterView, 然后,包装这个用户传的 Adapter。

先来看一下 ListView 的几个方法。

ListView 的 setAdapter:

    public void setAdapter(ListAdapter adapter) {
		...
        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
            mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }
        ...
    }

ListView 的 addHeaderView:

    public void addHeaderView(View v, Object data, boolean isSelectable) {
	    ...
        if (mAdapter != null) {
            if (!(mAdapter instanceof HeaderViewListAdapter)) {
                wrapHeaderListAdapterInternal();
            }

            // In the case of re-adding a header view, or adding one later on,
            // we need to notify the observer.
            if (mDataSetObserver != null) {
                mDataSetObserver.onChanged();
            }
        }
    }

ListView 的 addFooterView:

    public void addFooterView(View v, Object data, boolean isSelectable) {
        ...
        // Wrap the adapter if it wasn't already wrapped.
        if (mAdapter != null) {
            if (!(mAdapter instanceof HeaderViewListAdapter)) {
                wrapHeaderListAdapterInternal();
            }

            // In the case of re-adding a footer view, or adding one later on,
            // we need to notify the observer.
            if (mDataSetObserver != null) {
                mDataSetObserver.onChanged();
            }
        }
    }

ListView 的 wrapHeaderListAdapterInternal:

    protected void wrapHeaderListAdapterInternal() {
        mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
    }

可以发现,setAdapter、addHeaderView 和 addFooterView 都有调用到:

	mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);

在这里进行 adapter 的偷偷修饰,把他换成 wrapHeaderListAdapterInternal,从而支持 header 和 footer。

根据这个思想,实现我们自己的 RecyclerView 以及封装的 Adapter。
WrapRecyclerView :

public class WrapRecyclerView extends RecyclerView {
    private ArrayList mHeaderViewInfos = new ArrayList<>();
    private ArrayList mFooterViewInfos = new ArrayList<>();
    private Adapter mAdapter;

    public WrapRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public void addHeaderView(View v) {
        mHeaderViewInfos.add(v);

        if (mAdapter != null) {
            if (mAdapter instanceof HeaderViewRecyclerAdpater) {
                mAdapter = new HeaderViewRecyclerAdpater(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }
        }
    }

    public void addFooterView(View v) {
        mFooterViewInfos.add(v);

        if (mAdapter != null) {
            if (mAdapter instanceof HeaderViewRecyclerAdpater) {
                mAdapter = new HeaderViewRecyclerAdpater(mHeaderViewInfos, mFooterViewInfos, mAdapter);
            }
        }
    }

    @Override
    public void setAdapter(Adapter adapter) {
        if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) {
            mAdapter = new HeaderViewRecyclerAdpater(mHeaderViewInfos, mFooterViewInfos, adapter);
        } else {
            mAdapter = adapter;
        }
        super.setAdapter(mAdapter);
    }
}

HeaderViewRecyclerAdpater :

public class HeaderViewRecyclerAdpater extends RecyclerView.Adapter{
    private ArrayList mHeaderViewInfos = new ArrayList<>();
    private ArrayList mFooterViewInfos = new ArrayList<>();
    private RecyclerView.Adapter mAdapter;

    public HeaderViewRecyclerAdpater(ArrayList headerViewInfos, ArrayList footerViewInfos, RecyclerView.Adapter adapter) {
        mAdapter = adapter;
        if (mHeaderViewInfos == null) {
            mHeaderViewInfos = new ArrayList<>();
        } else {
            mHeaderViewInfos = headerViewInfos;
        }
        if (mFooterViewInfos == null) {
            mFooterViewInfos = new ArrayList<>();
        } else {
            mFooterViewInfos = footerViewInfos;
        }

    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //Headerview
        if (viewType == RecyclerView.INVALID_TYPE) {
            return new HeaderViewHolder(mHeaderViewInfos.get(0));
        } else if (viewType == RecyclerView.INVALID_TYPE - 1) { // FooterView
            return new HeaderViewHolder(mFooterViewInfos.get(0));
        }
        return mAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int viewType = getItemViewType(position);
        if (viewType == RecyclerView.INVALID_TYPE) {
            return ;
        } else if (viewType == RecyclerView.INVALID_TYPE - 1) {
            return ;
        } else {
            mAdapter.onBindViewHolder(holder, position - mHeaderViewInfos.size());
        }

    }

    @Override
    public int getItemViewType(int position) {
        int numHeaders = mHeaderViewInfos.size();
        if (numHeaders > position) {
            return RecyclerView.INVALID_TYPE;
        }
        final int adjPosition = position - numHeaders;
        int adapterCOunt = 0;
        if (mAdapter != null) {
            adapterCOunt = mAdapter.getItemCount();
            if (adapterCOunt > adjPosition) {
                return mAdapter.getItemViewType(adjPosition);
            }
        }

        return RecyclerView.INVALID_TYPE - 1;
    }

    @Override
    public int getItemCount() {
        return mAdapter != null ? mAdapter.getItemCount() + mFooterViewInfos.size() + mHeaderViewInfos.size() :
                mFooterViewInfos.size() + mHeaderViewInfos.size() ;
    }

    private static class HeaderViewHolder extends RecyclerView.ViewHolder {
        public HeaderViewHolder(View view) {
            super(view);
        }
    }
    private static class FooterViewHolder extends RecyclerView.ViewHolder {
        public FooterViewHolder(View view) {
            super(view);
        }
    }
}

这样 RecyclerView 就可以像 LiseView 一样,直接调用 addHeaderView 和 addFooterView 进行 header 和 footer 的设置。

三、RecyclerView 嵌套滑动问题

1.嵌套滑动问题

(二十七)RecyclerView 常用封装、修复及优化_第1张图片

这是一个可以垂直滑动的 RecyclerView,每一个 item 是一个可以水平滑动的 RecyclerView。
这时候,当想要水平滑动一个 item,稍微倾斜一点,则很容易被执行为垂直滑动。(已经在最上面了,触发的是上方水波那个效果)

这个最终是一个事件分发的问题,手指滑动的事件被垂直的 RecyclerView 拦截了,所以先进行垂直的滑动,下面看一下源码来分析这个原因。

RecyclerView 的 onInterceptTouchEvent:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        ...
        switch (action) {
            ...
            case MotionEvent.ACTION_MOVE: {
                final int index = e.findPointerIndex(mScrollPointerId);
                if (index < 0) {
                    Log.e(TAG, "Error processing scroll; pointer index for id " +
                            mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                    return false;
                }

                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                if (mScrollState != SCROLL_STATE_DRAGGING) {
                    final int dx = x - mInitialTouchX;
                    final int dy = y - mInitialTouchY;
                    boolean startScroll = false;
                    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                        mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
                        startScroll = true;
                    }
                    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                        mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
                        startScroll = true;
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }
            } break;
            ...
        }
        return mScrollState == SCROLL_STATE_DRAGGING;
    }

在 20 - 26 行,判断 x 方向与 y 方向上的移动距离是否大于 mTouchSlop(系统判断发生生滑动的最小距离)。大于的话,则在 29 行调用 setScrollState 方法。

RecyclerView 的 setScrollState:

    void setScrollState(int state) {
        if (state == mScrollState) {
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
                    new Exception());
        }
        mScrollState = state;
        if (state != SCROLL_STATE_SETTLING) {
            stopScrollersInternal();
        }
        dispatchOnScrollStateChanged(state);
    }

setScrollState 主要是把 SCROLL_STATE_DRAGGING 赋值给 mScrollState,这时候 onInterceptTouchEvent 最后的 return 就是 true 了。也就是滑动事件被拦截了,不能继续往下传递。

2.嵌套滑动解决方案

这边要实现的是判断 X 方向与 Y 方向上滑动的距离哪个大,X 方向滑动的距离大的时候进行 item 的水平滑动。
(二十七)RecyclerView 常用封装、修复及优化_第2张图片

实现了一个自定义控件,继承 RecyclerView,重新定义拦截规则,即重写 onInterceptTouchEvent 方法。
BetterRecyclerView:

public class BetterRecyclerView extends RecyclerView {
    //touchSlop 为系统判断发生滑动的最小距离
    //这边定义是因为 RecyclerView 中这个为私有变量,没办法获取
    //处理与 RecyclerView 中一致
    private int touchSlop;
    private Context mContext;
    private int INVALID_POINTER = -1;
    private int scrollPointerId = INVALID_POINTER;
    private int initialTouchX;
    private int initialTouchY;
    private final static String TAG = "BetterRecyclerView";

    public BetterRecyclerView(Context context) {
//        super(context);
        this(context, null);
    }

    public BetterRecyclerView(Context context, @Nullable AttributeSet attrs) {
//        super(context, attrs);
        this(context, attrs, 0);
    }

    public BetterRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        ViewConfiguration vc = ViewConfiguration.get(context);
        touchSlop = vc.getScaledEdgeSlop();
        mContext = context;
    }

    @Override
    public void setScrollingTouchSlop(int slopConstant) {
        super.setScrollingTouchSlop(slopConstant);
        ViewConfiguration vc = ViewConfiguration.get(mContext);
        switch (slopConstant) {
            case TOUCH_SLOP_DEFAULT:
                touchSlop = vc.getScaledTouchSlop();
                break;
            case TOUCH_SLOP_PAGING:
                touchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(vc);
                break;

        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        if (e == null) {
            return false;
        }
        int action = MotionEventCompat.getActionMasked(e);
        int actionIndex = MotionEventCompat.getActionIndex(e);
        switch (action) {
            case MotionEvent.ACTION_DOWN :
                //获取放下去的点的坐标
                scrollPointerId = MotionEventCompat.getPointerId(e, 0);
                initialTouchX = Math.round(e.getX() + 0.5f);
                initialTouchY = Math.round(e.getY() + 0.5f);
                return super.onInterceptTouchEvent(e);
            case MotionEvent.ACTION_POINTER_DOWN:
                //获取放下去的点的坐标
                scrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
                initialTouchX = Math.round(MotionEventCompat.getX(e, actionIndex) + 0.5f);
                initialTouchY = Math.round(MotionEventCompat.getY(e, actionIndex) + 0.5f);
                return super.onInterceptTouchEvent(e);
            case MotionEvent.ACTION_MOVE:
                int index = MotionEventCompat.findPointerIndex(e, scrollPointerId);
                if (index < 0) {
                    return false;
                }
                //计算滑动偏移量
                int x = Math.round(MotionEventCompat.getX(e, index) + 0.5f);
                int y = Math.round(MotionEventCompat.getY(e, index) + 0.5f);
                
                if (getScrollState() != SCROLL_STATE_DRAGGING ) {
                    int dx = x - initialTouchX;
                    int dy = y - initialTouchY;
                    boolean startScroll = false;
                    //将斜率添加进来,这样可以减少 startScroll 为 true 的机会。这个机会就会给需要这个返回值
                    if (getLayoutManager().canScrollHorizontally() && Math.abs(dx) > touchSlop &&
                            (getLayoutManager().canScrollVertically() || Math.abs(dx) > Math.abs(dy))) {
                        startScroll = true;
                    }
                    if(getLayoutManager().canScrollVertically() && Math.abs(dy) > touchSlop &&
                            (getLayoutManager().canScrollHorizontally() || Math.abs(dy) > Math.abs(dx))) {
                        startScroll = true;
                    }
                    Log.d(TAG, "startScroll: " + startScroll);
                    return startScroll && super.onInterceptTouchEvent(e);
                }
                return super.onInterceptTouchEvent(e);
            default:
                return super.onInterceptTouchEvent(e);
        }
    }
}

代码还是比较简单,就是减少了 onInterceptTouchEvent 返回值为 true 的概率(减少过滤事件)。

3.快速切换滑动

当 item 先横向滑动时候,快速切换到垂直滑动;或先垂直滑动,然后快速切换到横向滑动,这时候,后一个事件是不生效的。
(二十七)RecyclerView 常用封装、修复及优化_第3张图片

RecyclerView 的 onInterceptTouchEvent:

  @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        ...
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                ...

                if (mScrollState == SCROLL_STATE_SETTLING) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
                ...
    }

在 RecyclerView 的 onInterceptTouchEvent 的方法里,手指 DOWN 事件的时候会判断是否正在滑动,是的话调用 requestDisallowInterceptTouchEvent 方法。这个方法在 RecyclerView 重写了:

RecyclerView 的 requestDisallowInterceptTouchEvent:

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        final int listenerCount = mOnItemTouchListeners.size();
        for (int i = 0; i < listenerCount; i++) {
            final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
            listener.onRequestDisallowInterceptTouchEvent(disallowIntercept);
        }
        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }

在这里调用了 onRequestDisallowInterceptTouchEvent 方法。我们看一下 onRequestDisallowInterceptTouchEvent 的注解,调用这个方法则不会再去接收其他的事件。

        /**
         * Called when a child of RecyclerView does not want RecyclerView and its ancestors to
         * intercept touch events with
         * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
         *
         * @param disallowIntercept True if the child does not want the parent to
         *            intercept touch events.
         * @see ViewParent#requestDisallowInterceptTouchEvent(boolean)
         */
        public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);

处理这个问题的方案也很简单,直接重写 requestDisallowInterceptTouchEvent 方法,空实现即可。
FeedRootRecyclerView:

public class FeedRootRecyclerView extends BetterRecyclerView {
    public FeedRootRecyclerView(Context context) {
        this(context,null);
    }

    public FeedRootRecyclerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FeedRootRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    //这个接口的作用是不允许父类打断这个onTouch 事件,
    //那么我设置一个空的函数,override 父类的方法,就可以达到相反的效果
    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}

四、RecycleView 优化

RecycleView 里面有一个缓存机制,他有一个内部类 RecycledViewPool,按注释说的是允许在多个 RecyclerView 之间共享 View 的缓存。比如我们上面应用的例子,在比如 viewPager + adapter + tab 。

    /**
     * RecycledViewPool lets you share Views between multiple RecyclerViews.
     * 

* If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}. *

* RecyclerView automatically creates a pool for itself if you don't provide one. * */ public static class RecycledViewPool { }

RecycleView 有三级缓存:
1、通过 recycler.getViewForPosition 方法,该方法返回ViewHolder对象,这个方法会按顺序检查 mChangedScrap (RecyclerView中需要改变的Viewholder)、 mAttachedScrap (还没有和RecyclerView 分离的 ViewHolder)、 mCachedViews(RecyclerView 的 ViewHolder 的缓存) ,如果有则返回 ViewHolder 进行复用。

2、调用 ViewCacheExtension.getViewForPositionAndType 方法。(只是一个接口,给开放者自己创建的缓存)

3、缓存池 RecyclerViewPool。

RecyclerViewPool 的使用也很简单,创建完 RecyclerViewPool 后调用 RecyclerViewPool 的 setRecycledViewPool 即可。使用的时候,系统会自动通过三级缓存去调用。

你可能感兴趣的:(高级UI,安卓)