RecyclerView粘性頭部,支持線性和網格佈局,支持item添加和刪除(無需再次排序)

RecyclerView粘性頭部

     1.實現原理:在外部添加一個與RecyclerView對齊的headerView,動態添加需要展示的header

     2.支持線性和網格佈局

     3.支持item添加和刪除(無需再次排序)

 

博主文筆太菜,不想多說,直接上項目鏈接

github:  https://github.com/PPQingZhao/StickyHeaderDemo

需要的夥伴自行看源碼,主要實現在 StickyHeaderScrollerListener 滑動監聽類中

 

下面貼出關鍵代碼:  

package com.pp.stickyheaderdemo.adapter;

import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;

import com.pp.stickyheaderdemo.mutilitem.StickyHeader;

/**
 * @author qing
 * 滑動監聽
 * 用於實現粘性頭部
 */
public class StickyHeaderScrollerListener extends RecyclerView.OnScrollListener {
    private static final String TAG = "StickyHeaderScroller";
    private final StickHeaderContainer mHeaderContainer;
    private final SparseArray<RecyclerView.ViewHolder> mHolderMap;

    public StickyHeaderScrollerListener(StickHeaderContainer container) {
        if (null == container) {
            throw new RuntimeException("StickHeaderContainer must not be null.");
        }
        this.mHeaderContainer = container;
        mHolderMap = new SparseArray<>();
    }

    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        if (null == mHeaderAdapter) {
            throw new RuntimeException("StickyHeaderAdapter must not be null.");
        }

        if (dy > 0) {
            int headerHeight = mHeaderContainer.getMeasuredHeight();
            // header top邊下的view
            View viewTop = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, 1);
            // 獲取view 在列表中的位置
            int positionTop = recyclerView.getChildAdapterPosition(viewTop);
            StickyHeader topHeaderItem = mHeaderAdapter.getHeaderItem(positionTop);
            boolean isHeaderTop = null != topHeaderItem && topHeaderItem.isHeader();

            // header交換時機: viewTop是header 並且 viewTop.getTop() <= 0
            if (isHeaderTop && viewTop.getTop() <= 0) {
                if (positionTop != mHeaderContainer.getHeaderPosition()) {

                    // 更新old header記錄
                    mHeaderContainer.setOldHeaderPosition(mHeaderContainer.getHeaderPosition());

                    // 更新當前header記錄
                    mHeaderContainer.setHeaderPosition(positionTop);

                    // 更新map , key:當前header  value: 當前header的前一個header
                    mHeaderContainer.putPreviousValue(mHeaderContainer.getOldHeaderPosition());

                    RecyclerView.ViewHolder holder = getHolder(mHeaderContainer.getHeaderPosition());
                    if (null != holder && null == holder.itemView.getParent()) {
                        // 更新headerview
                        addHeaderView(holder);
                    }
                }
            }

            // header bottom邊下的view
            View viewBottom = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, headerHeight);
            // 獲取view 在列表中的位置
            int positionBottom = recyclerView.getChildAdapterPosition(viewBottom);
            StickyHeader bottomHeaderItem = mHeaderAdapter.getHeaderItem(positionBottom);
            boolean isHeaderBottom = null != bottomHeaderItem && bottomHeaderItem.isHeader();
            int viewBottomTop = null == viewBottom ? 0 : viewBottom.getTop();
            if (isHeaderBottom && 0 <= viewBottomTop && viewBottomTop <= headerHeight) {
                mHeaderContainer.setHeaderTranslationY(viewBottomTop - headerHeight);
            } else {
                mHeaderContainer.setHeaderTranslationY(0);
            }

        } else if (dy <= 0) {
            int headerHeight = mHeaderContainer.getMeasuredHeight();
            View viewUnder = null;
            // dy 等於0時,取mHeaderContainer top邊下的item,通常是 刪除/添加 itme時觸發
            if (0 == dy) {
                viewUnder = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, 1);
            } else {
                viewUnder = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, headerHeight);
            }
            // 獲取view 在列表中的位置
            int position = recyclerView.getChildAdapterPosition(viewUnder);
            StickyHeader headerItem = mHeaderAdapter.getHeaderItem(position);
            boolean isHeader = null != headerItem && headerItem.isHeader();

            // header交換時機: viewUnder 是header 並且 viewUnder.getTop() > 0
            if (isHeader && viewUnder.getTop() > 0) {
                // 獲取當前position header的 前一個header
                Integer previousHeaderPosition = mHeaderContainer.getPreviousHeaderPosition(position);
                if (null != previousHeaderPosition
                        && previousHeaderPosition != mHeaderContainer.getHeaderPosition()) {

                    // 更新old header 記錄
                    mHeaderContainer.setOldHeaderPosition(mHeaderContainer.getHeaderPosition());
                    // 更新當前header 記錄
                    mHeaderContainer.setHeaderPosition(previousHeaderPosition);

                    RecyclerView.ViewHolder holder = getHolder(mHeaderContainer.getHeaderPosition());
                    if (null != holder && null == holder.itemView.getParent()) {
                        // 更新headerview
                        addHeaderView(holder);
                    }
                }
            }
            // header bottom邊下的view
            View viewBottom = recyclerView.findChildViewUnder(mHeaderContainer.getMeasuredWidth() * 0.5f, headerHeight);
            // 獲取view 在列表中的位置
            int positionBottom = recyclerView.getChildAdapterPosition(viewBottom);
            StickyHeader bottomHeaderItem = mHeaderAdapter.getHeaderItem(positionBottom);
            boolean isHeaderBottom = null != bottomHeaderItem && bottomHeaderItem.isHeader();
            int viewBottomTop = null == viewBottom ? 0 : viewBottom.getTop();
            if (isHeaderBottom && 0 <= viewBottomTop && viewBottomTop <= headerHeight) {
                mHeaderContainer.setHeaderTranslationY(viewBottomTop - headerHeight);
            } else {
                mHeaderContainer.setHeaderTranslationY(0);
            }
        }
        // 主要解決:mHeaderContainer 未完全初始化就已經添加headerView,此時headerView是沒有寬高信息的,需要重繪
        if (View.VISIBLE == mHeaderContainer.getVisibility()
                && (0 == mHeaderContainer.getHeight()
                || 0 == mHeaderContainer.getWidth())) {
            mHeaderContainer.requestLayout();
        }

    }

    private RecyclerView.ViewHolder getHolder(int headerPosition) {

        StickyHeader headerItem = mHeaderAdapter.getHeaderItem(headerPosition);
        if (null == headerItem || !headerItem.isHeader()) {
            return null;
        }

        // 獲取 header type
        int headerType = mHeaderAdapter.getHeaderType(headerPosition);

        // 從緩存中獲取 header 對應holder
        RecyclerView.ViewHolder holder = mHolderMap.get(headerType);
        if (null == holder) {
            // 創建holder
            holder = mHeaderAdapter.createHeaderViewHolder(mHeaderContainer, headerType);
            // 緩存 holder
            mHolderMap.put(headerType, holder);
        }

        // bind holder
        mHeaderAdapter.bindHeaderViewHolder(holder, mHeaderContainer.getHeaderPosition());

        return holder;
    }

    private void addHeaderView(RecyclerView.ViewHolder holder) {
        if (null == holder || null != holder.itemView.getParent()) {
            return;
        }

        mHeaderContainer.removeHeader();
        mHeaderContainer.addHeader(holder.itemView);

        Log.i(TAG, "add header View");
    }

    private BaseHeaderAdapter mHeaderAdapter;

    public <Adapter extends BaseHeaderAdapter> void setHeaderAdapter(Adapter adapter) {
        this.mHeaderAdapter = adapter;
    }

    /**
     * 刪除條目,刷新header
     *
     * @param index
     */
    public void notifyPreviousRemove(int index) {
        // 是否是當前header
        boolean curHeader = mHeaderContainer.getHeaderPosition() == index;

        // 刷新 當前headerposition
        mHeaderContainer.notifyHeaderPoisitionRemove(index);
        // 刷新記錄的所有header
        mHeaderContainer.notifyPreviousRemove(index);

        // 刪除的條目是當前header,需要更新header 條目
        if (curHeader) {
            RecyclerView.ViewHolder holder = getHolder(mHeaderContainer.getHeaderPosition());
            if (null != holder && null == holder.itemView.getParent()) {
                addHeaderView(holder);
            }
        }
    }

    /**
     * 添加條目,刷新header
     *
     * @param index
     */
    public void notifyPreviousInsert(int index) {
        StickyHeader headerItem = mHeaderAdapter.getHeaderItem(index);
        // 是否插入header
        boolean isInsertHeader = null != headerItem && headerItem.isHeader();
        // 刷新 當前headerposition
        mHeaderContainer.notifyHeaderPoisitionInsert(index, isInsertHeader);
        // 刷新記錄的所有header
        mHeaderContainer.notifyPreviousInsert(index, isInsertHeader);
    }

    public interface BaseHeaderAdapter<VH extends RecyclerView.ViewHolder> {

        /**
         * 獲取header type
         *
         * @param position
         * @return
         */
        int getHeaderType(int position);

        /**
         * 創建header holder
         *
         * @param parent
         * @param headerType
         * @return
         */
        VH createHeaderViewHolder(ViewGroup parent, int headerType);

        /**
         * 綁定header view
         *
         * @param holder
         * @param position
         */
        void bindHeaderViewHolder(VH holder, int position);

        /**
         * 獲取header條目
         *
         * @param position
         * @param <T>
         * @return
         */
        <T extends StickyHeader> T getHeaderItem(int position);
    }
}
package com.pp.stickyheaderdemo.adapter;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;


/**
 * @author qing
 * 粘性頭部
 */
public class StickHeaderContainer extends ViewGroup {
    private final HeaderView mHeaderView;

    public StickHeaderContainer(@NonNull Context context) {
        this(context, null);
    }

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

    public StickHeaderContainer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mHeaderView = new HeaderView(getContext());
        mHeaderView.setLayoutParams(new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    }

    /**
     * 測量設置寬高信息
     * <p>
     * 這裏我們希望StickHeaderContainer只需要包裹住HeaderView
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int childCount = getChildCount();
        if (childCount > 1) {
            throw new IllegalArgumentException("StickHeaderContainer only allows one child.");
        }

        if (childCount == 0) {
           setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
            return;
        }

        // 獲取child
        View child = getChildAt(0);
        //  測量單個child(考慮外邊距)
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

        // 獲取child 的佈局參數(此時包含外邊距)
        MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();

        // 期望的寬高 (取child最大寬高)
        int desireWidth = child.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
        int desireHeight = child.getHeight() + layoutParams.topMargin + layoutParams.bottomMargin;

        // 考慮本身內邊距
        desireWidth += getPaddingLeft() + getPaddingRight();
        desireHeight += getPaddingTop() + getPaddingBottom();

        // 比較建議最小值和期望值,並取最大值
        desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
        desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

        // 設置最終測量值 (view.getMeasureWidth()和view.getMeasureHeight()的值就是從這裏確定的)
        setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (getChildCount() == 0) {
            return;
        }

        View child = getChildAt(0);
        MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();

        int cLeft = getPaddingLeft() + layoutParams.leftMargin;
        int cTop = getPaddingTop() + layoutParams.topMargin;
        int cRight = cLeft + child.getMeasuredWidth();
        int cBottom = cTop + child.getMeasuredHeight();
        /* 設置child在parent中的位置
            分別保存在child的  mLeft,mTop,mBottom,mRight中,view.getWidth()的值等於 mRight - mLeft,
            所以在執行完view.layout()後,view.getWidth()和view.getHeight();纔有值
            */
        child.layout(cLeft, cTop, cRight, cBottom);

    }

    /**
     * 重寫構造默認佈局參數方法
     * 因爲在onMeasure()中使用了 measureChildWithMargins(),這要求view的佈局參數是MarginLayoutParams
     *
     * @return
     */
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    public void putPreviousValue(int previousPosition) {
        // key:當前header  value: 當前header的前一個header
        mHeaderView.putPreviousValue(previousPosition);
    }

    public void notifyPreviousRemove(int index) {
        mHeaderView.notifyPreviousRemove(index);
    }

    public void notifyHeaderPoisitionRemove(int index) {
        mHeaderView.notifyHeaderPoisitionRemove(index);
    }

    public void notifyPreviousInsert(int index, boolean isInsertHeader) {
        mHeaderView.notifyPreviousInsert(index, isInsertHeader);
    }

    public void notifyHeaderPoisitionInsert(int index, boolean isInsertHeader) {
        mHeaderView.notifyHeaderPoisitionInsert(index, isInsertHeader);
    }

    public void addHeader(View header) {
        if (null == mHeaderView.getParent()) {
            addView(mHeaderView);
        }
        mHeaderView.addView(header);
    }

    public void removeHeader() {
        mHeaderView.removeAllViews();
    }

    public int getHeaderPosition() {
        return mHeaderView.getHeaderPosition();
    }

    public void setHeaderPosition(int headerPosition) {
        mHeaderView.setHeaderPosition(headerPosition);
    }

    public int getOldHeaderPosition() {
        return mHeaderView.getOldHeaderPosition();
    }

    public void setOldHeaderPosition(int oldHeaderPosition) {
        mHeaderView.setOldHeaderPosition(oldHeaderPosition);
    }

    public Integer getPreviousHeaderPosition(int headerPosition) {
        return mHeaderView.getPreviousHeaderPosition(headerPosition);
    }

    public void setHeaderTranslationY(float translationY) {
        mHeaderView.setTranslationY(translationY);
    }

    public void setHeaderTranslationX(float translationX) {
        mHeaderView.setTranslationX(translationX);
    }

    static class HeaderView extends FrameLayout {
        //  key:當前header  value: 當前header的前一個header
        private final SparseArray<Integer> headerPositionMap;

        private HeaderView(Context context) {
            super(context);
            headerPositionMap = new SparseArray<>();
        }

        public void putPreviousValue(int previousPosition) {
            // key:當前header  value: 當前header的前一個header
            headerPositionMap.put(getHeaderPosition(), previousPosition);
        }

        public Integer getPreviousHeaderPosition(int headerPosition) {
            return headerPositionMap.get(headerPosition);
        }

        /**
         * 記錄當前headerView對應數據源中的位置
         */
        private int headerPosition = -1;

        public int getHeaderPosition() {
            return headerPosition;
        }

        public void setHeaderPosition(int headerPosition) {
            this.headerPosition = headerPosition;
        }

        /**
         * 記錄 old  headerPosition
         */
        private int oldHeaderPosition = headerPosition;

        public int getOldHeaderPosition() {
            return oldHeaderPosition;
        }

        public void setOldHeaderPosition(int oldHeaderPosition) {
            this.oldHeaderPosition = oldHeaderPosition;
        }

        public void notifyHeaderPoisitionRemove(int index) {
            if (getHeaderPosition() < index) {
                // do nothing
            } else if (getHeaderPosition() == index) {
                setHeaderPosition(headerPositionMap.get(getHeaderPosition()));
            } else if (getHeaderPosition() > index) {
                setHeaderPosition(getHeaderPosition() - 1);
            }
        }

        public void notifyPreviousRemove(int index) {
            SparseArray<Integer> cloneMap = headerPositionMap.clone();
            headerPositionMap.clear();

            for (int i = 0; i < cloneMap.size(); i++) {
                int key = cloneMap.keyAt(i);
                Integer previousKey = cloneMap.valueAt(i);

                // index 小於 key和previousKey
                if (index < previousKey && index < key) {
                    if (previousKey == 0) {
                        headerPositionMap.put(key - 1, 0);
                    } else {
                        headerPositionMap.put(key - 1, previousKey - 1);
                    }
                    // index < previousKey
                } else if (index == previousKey) {
                    Integer deletedValue = cloneMap.get(previousKey);
                    headerPositionMap.put(key - 1, deletedValue);
                    // previousKey < index  < key
                } else if (previousKey < index && index < key) {
                    headerPositionMap.put(key - 1, previousKey);
                } else if (index == key) {
                    int valueIndex = cloneMap.indexOfValue(key);
                    if (-1 != valueIndex) {

                        int keyValue = cloneMap.keyAt(valueIndex);
                        headerPositionMap.put(keyValue, previousKey);
                        cloneMap.put(keyValue, previousKey);
                    }
                } else if (index > key) {
                    // do nothing
                }
            }
        }

        public void notifyPreviousInsert(int index, boolean isInsertHeader) {
            SparseArray<Integer> cloneMap = headerPositionMap.clone();
            headerPositionMap.clear();

            for (int i = 0; i < cloneMap.size(); i++) {
                int key = cloneMap.keyAt(i);
                Integer previousKey = cloneMap.valueAt(i);
                if (index > key && index > previousKey) {
                    headerPositionMap.put(key, previousKey);
                    if (isInsertHeader) {
                        headerPositionMap.put(index, key);
                    }
                } else if (previousKey < index && index < key) {
                    if (isInsertHeader) {
                        headerPositionMap.put(index, previousKey);
                        headerPositionMap.put(key + 1, index);
                    } else {
                        headerPositionMap.put(key + 1, previousKey);
                    }
                } else if (index < previousKey && index < key) {
                    headerPositionMap.put(key + 1, previousKey + 1);
                    if (isInsertHeader) {
                        // 獲取 previousKey的前一個header
                        Integer value = cloneMap.get(previousKey);
                        headerPositionMap.put(index, value);
                        headerPositionMap.put(previousKey + 1, index);
                    }
                }
            }
        }

        /**
         * 插入item刷新headerposition
         *
         * @param index
         * @param isInsertHeader
         */
        public void notifyHeaderPoisitionInsert(int index, boolean isInsertHeader) {
            if (index > getHeaderPosition()) {
                // do nothing
            } else if (index <= getHeaderPosition()) {
                setHeaderPosition(getHeaderPosition() + 1);
            }
        }
    }
}

 

/**
 * @author qing
 */
public interface StickyHeader {
    /**
     * 頭部類型
     *
     * @return
     */
    int getHeaderType();

    /**
     * 是否是header
     *
     * @return
     */
    boolean isHeader();
}

 

github: https://github.com/PPQingZhao/StickyHeaderDemo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章