Android RecyclerView 悬浮标题

1,098 阅读3分钟

RecyclerView 悬浮标题 如何实现?

    //添加 itemDecoration
    public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
        this.addItemDecoration(decor, -1);
    }

看一下ItemDecoration 的源码

public abstract static class ItemDecoration {
        public ItemDecoration() {
        }
        //绘制方法
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.onDraw(c, parent);
        }

        /** @deprecated */
        @Deprecated
        public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }
        //onDraw 执行完毕执行的方法
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.onDrawOver(c, parent);
        }

        /** @deprecated */
        @Deprecated
        public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) {
        }

        /** @deprecated */
        @Deprecated
        public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }
        //获取Item 位置信息
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.getItemOffsets(outRect, ((RecyclerView.LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
        }
    }

新建MyItemDecoration 继承ItemDecoration 重载这三个方法

  • 获取headerViewHolder 显示位置
  • 绘制要显示的headerViewHolder
   public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.onDraw(c, parent);
        }
   public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.onDrawOver(c, parent);
        }
 public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            this.getItemOffsets(outRect, ((RecyclerView.LayoutParams)view.getLayoutParams()).getViewLayoutPosition(), parent);
        }        
private final StickyHeaderAdapter mAdapter; //接口
public interface StickyHeaderAdapter {
    int getHeaderId(int position);//获取position
    //创建HeaderViewHolder
    RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position);
    //绑定实现
    void onBindHeaderViewHolder(RecyclerView.ViewHolder holder, int position);
}
private final SparseArray<Rect> mHeaderRectSet = new SparseArray<>(); // 存储Rect 对象
private final HashMap<Integer, RecyclerView.ViewHolder> mHeaderViews = new HashMap<>();//存储 对应position 下的ViewHolder
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position = parent.getChildAdapterPosition(view);
        int headerHeight = 0;
        //在使用adapterPosition时最好的加上这个判断
        if (position != RecyclerView.NO_POSITION) { 
            //获取到ItemDecoration所需要的高度
            View header = getHeader(parent, position);
            headerHeight = header.getHeight();
        }
        outRect.set(0, headerHeight, 0, 0); //修改hearderHeight 高度
    }
    
    /**
     * 获得自定义的 Header
     * @param parent
     * @param position
     * @return
     */
    public View getHeader(RecyclerView parent, int position) {
        //根据位置获取每一组的头部id
        final int headerId = mAdapter.getHeaderId(position);
        //通过头部id,从保存的头部view数组中获取改组的头部view
        RecyclerView.ViewHolder holder = mHeaderViews.get(headerId);
        //如果为空,就通过adapert创建
        if (holder == null) {
            //创建HeaderViewHolder
            holder = mAdapter.onCreateHeaderViewHolder(parent, position);
            View header = holder.itemView;
            header.setClickable(true);
            header.setFocusable(true);
            //绑定数据
            mAdapter.onBindHeaderViewHolder(holder, position);
            //测量View并且layout
            int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
            int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
            //根据父View的MeasureSpec和子view自身的LayoutParams以及padding来获取子View的MeasureSpec
            int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
                    parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width);
            int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
                    parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height);
            //进行测量
            header.measure(childWidth, childHeight);
            //根据测量后的宽高放置位置
            header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight());
            //将创建好的头部view保存在数组中,避免每次重复创建
            mHeaderViews.put(headerId, holder);
        }
        return holder.itemView;

    }
    
   @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        changDrawOver(c, parent);
    }

    private void changDrawOver(@NotNull Canvas c, @NotNull RecyclerView parent) {
        //mHeaderRectSet为存放屏幕上显示的header的点击区域,每次重新绘制头部的时候清空数据
        mHeaderRectSet.clear();
        final int count = parent.getChildCount();
        //遍历屏幕上加载的item
        for (int layoutPos = 0; layoutPos < count; layoutPos++) {
            final View child = parent.getChildAt(layoutPos);
            //获取该item在列表数据中的位置
            final int adapterPos = parent.getChildAdapterPosition(child);
            //只有在最上面一个item或者有header的item才绘制header
            if (adapterPos != RecyclerView.NO_POSITION && (layoutPos == 0 || hasHeader(adapterPos))) {
                View header = getHeader(parent, adapterPos);
                c.save();
                //获取绘制header的起始位置(left,top)
                final int left = child.getLeft();
                final int top = getHeaderTop(parent, child, header, adapterPos, layoutPos);
                //将画布移动到绘制的位置
                c.translate(left, top);
                //绘制header
                header.draw(c);
                c.restore();
                //保存绘制的header的区域
                mHeaderRectSet.put(adapterPos, new Rect(left, top, left + header.getWidth(), top + header.getHeight()));
            }
        }
    }

RecyclerView ItemDecoration 绘制的HeaderViewHolder 如何实现点击事件?

    //添加 Item Touch 监听
    public void addOnItemTouchListener(@NonNull RecyclerView.OnItemTouchListener listener) {
        this.mOnItemTouchListeners.add(listener);
    }
    
      @Override
    public boolean onInterceptTouchEvent(@NonNull RecyclerView view, @NonNull MotionEvent e) {
        //将事件交给GestureDetector类进行处理,通过onSingleTapUp返回的值,判断是否要拦截事件
        boolean tapDetectorResponse = this.mTapDetector.onTouchEvent(e);
        //如果是点击在header区域,则拦截事件
        if (tapDetectorResponse) {
            return true;
        }

        if (e.getAction() == MotionEvent.ACTION_UP) {
            int position = mDecor.findHeaderPositionUnder((int) e.getX(), (int) e.getY());
            return position != -1;
        }
        return false;
    }
    
    mTapDetector = new GestureDetectorCompat(recyclerView.getContext(), new GestureDetector.SimpleOnGestureListener() {

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                //根据点击的坐标查找是不是点击在header的区域
                int position = mDecor.findHeaderPositionUnder((int) e.getX(), (int) e.getY());
                if (position != -1) {
                    //如果position不等于-1,则表示点击在header区域,然后在判断是否在header需要响应的区域
                    View header = mDecor.getHeader(mRecyclerView, position);
                    if (header instanceof ViewGroup) {
                        int childCount = ((ViewGroup) header).getChildCount();
                        for (int i = 0; i < childCount; i++) {
                            View childAt = ((ViewGroup) header).getChildAt(i);
                            if (mDecor.findHeaderClickView(childAt, (int) e.getX(), (int) e.getY())) {
                                //如果在header需要响应的区域,该区域的view模拟点击
                                childAt.performClick();
                                break;
                            }
                        }
                    }
                    return true;
                }
                return false;
            }

            @Override
            public boolean onDoubleTap(MotionEvent e) {
                return true;
            }
        });
    public int findHeaderPositionUnder(int x, int y) {
        for (int i = 0; i < mHeaderRectSet.size(); i++) {
            Rect rect = mHeaderRectSet.get(mHeaderRectSet.keyAt(i));
            if (rect.contains(x, y)) {
                return mHeaderRectSet.keyAt(i);
            }
        }
        return -1;
    }
     public boolean findHeaderClickView(View view, int x, int y) {
        if (view == null) return false;
        for (int i = 0; i < mHeaderRectSet.size(); i++) {
            Rect rect = mHeaderRectSet.get(mHeaderRectSet.keyAt(i));
            if (rect.contains(x, y)) {
                Rect vRect = new Rect();
                // 需要响应点击事件的区域在屏幕上的坐标
                vRect.set(rect.left + view.getLeft(), rect.top + view.getTop(), rect.left + view.getLeft() + view.getWidth(), rect.top + view.getTop() + view.getHeight());
                return vRect.contains(x, y);
            }
        }
        return false;
    }