回收者视图中的可堆叠粘性标头

时间:2019-07-02 20:24:39

标签: android android-recyclerview android-adapter

我正在尝试使用LinearLayoutManager或Item Decorator来实现粘性头,但是我希望粘性头彼此堆叠,而不是新的将旧的头移出视图。 有没有一种干净的方法可以做到这一点?

以下是我目前正在使用的一些代码:

public class StickyHeadersLinearLayoutManager<T extends RecyclerView.Adapter & StickyHeaders> extends LinearLayoutManager {
    private T mAdapter;

    private float mTranslationX;
    private float mTranslationY;

    // Header positions for the currently displayed list and their observer.
    private List<Integer> mHeaderPositions = new ArrayList<>(0);
    private RecyclerView.AdapterDataObserver mHeaderPositionsObserver = new HeaderPositionsAdapterDataObserver();

    // Sticky header's ViewHolder and dirty state.
    private View mStickyHeader;
    private int mStickyHeaderPosition = RecyclerView.NO_POSITION;

    private int mPendingScrollPosition = RecyclerView.NO_POSITION;
    private int mPendingScrollOffset = 0;

    public StickyHeadersLinearLayoutManager(Context context) {
        super(context);
    }

    public StickyHeadersLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

    /**
     * Offsets the vertical location of the sticky header relative to the its default position.
     */
    public void setStickyHeaderTranslationY(float translationY) {
        mTranslationY = translationY;
        requestLayout();
    }

    /**
     * Offsets the horizontal location of the sticky header relative to the its default position.
     */
    public void setStickyHeaderTranslationX(float translationX) {
        mTranslationX = translationX;
        requestLayout();
    }

    /**
     * Returns true if {@code view} is the current sticky header.
     */
    public boolean isStickyHeader(View view) {
        return view == mStickyHeader;
    }

    @Override
    public void onAttachedToWindow(RecyclerView view) {
        super.onAttachedToWindow(view);
        setAdapter(view.getAdapter());
    }

    @Override
    public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
        super.onAdapterChanged(oldAdapter, newAdapter);
        setAdapter(newAdapter);
    }

    @SuppressWarnings({"unchecked", "ConstantConditions"})
    private void setAdapter(RecyclerView.Adapter adapter) {
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mHeaderPositionsObserver);
        }

        if (adapter instanceof StickyHeaders) {
            mAdapter = (T) adapter;
            mAdapter.registerAdapterDataObserver(mHeaderPositionsObserver);
            mHeaderPositionsObserver.onChanged();
        } else {
            mAdapter = null;
            mHeaderPositions.clear();
        }
    }

    @Override
    public Parcelable onSaveInstanceState() {
        SavedState ss = new SavedState();
        ss.superState = super.onSaveInstanceState();
        ss.pendingScrollPosition = mPendingScrollPosition;
        ss.pendingScrollOffset = mPendingScrollOffset;
        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (state instanceof SavedState) {
            SavedState ss = (SavedState) state;
            mPendingScrollPosition = ss.pendingScrollPosition;
            mPendingScrollOffset = ss.pendingScrollOffset;
            state = ss.superState;
        }

        super.onRestoreInstanceState(state);
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachStickyHeader();
        int scrolled = super.scrollVerticallyBy(dy, recycler, state);
        attachStickyHeader();

        if (scrolled != 0) {
            updateStickyHeader(recycler, false);
        }

        return scrolled;
    }

    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachStickyHeader();
        int scrolled = super.scrollHorizontallyBy(dx, recycler, state);
        attachStickyHeader();

        if (scrolled != 0) {
            updateStickyHeader(recycler, false);
        }

        return scrolled;
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachStickyHeader();
        super.onLayoutChildren(recycler, state);
        attachStickyHeader();

        if (!state.isPreLayout()) {
            updateStickyHeader(recycler, true);
        }
    }

    @Override
    public void scrollToPosition(int position) {
        scrollToPositionWithOffset(position, INVALID_OFFSET);
    }

    @Override
    public void scrollToPositionWithOffset(int position, int offset) {
        scrollToPositionWithOffset(position, offset, true);
    }

    private void scrollToPositionWithOffset(int position, int offset, boolean adjustForStickyHeader) {
        // Reset pending scroll.
        setPendingScroll(RecyclerView.NO_POSITION, INVALID_OFFSET);

        // Adjusting is disabled.
        if (!adjustForStickyHeader) {
            super.scrollToPositionWithOffset(position, offset);
            return;
        }

        // There is no header above or the position is a header.
        int headerIndex = findHeaderIndexOrBefore(position);
        if (headerIndex == -1 || findHeaderIndex(position) != -1) {
            super.scrollToPositionWithOffset(position, offset);
            return;
        }

        // The position is right below a header, scroll to the header.
        if (findHeaderIndex(position - 1) != -1) {
            super.scrollToPositionWithOffset(position - 1, offset);
            return;
        }

        // Current sticky header is the same as at the position. Adjust the scroll offset and reset pending scroll.
        if (mStickyHeader != null && headerIndex == findHeaderIndex(mStickyHeaderPosition)) {
            int adjustedOffset = (offset != INVALID_OFFSET ? offset : 0) + mStickyHeader.getHeight();
            super.scrollToPositionWithOffset(position, adjustedOffset);
            return;
        }

        // Remember this position and offset and scroll to it to trigger creating the sticky header.
        setPendingScroll(position, offset);
        super.scrollToPositionWithOffset(position, offset);
    }

    @Override
    public int computeVerticalScrollExtent(RecyclerView.State state) {
        detachStickyHeader();
        int extent = super.computeVerticalScrollExtent(state);
        attachStickyHeader();
        return extent;
    }

    @Override
    public int computeVerticalScrollOffset(RecyclerView.State state) {
        detachStickyHeader();
        int offset = super.computeVerticalScrollOffset(state);
        attachStickyHeader();
        return offset;
    }

    @Override
    public int computeVerticalScrollRange(RecyclerView.State state) {
        detachStickyHeader();
        int range = super.computeVerticalScrollRange(state);
        attachStickyHeader();
        return range;
    }

    @Override
    public int computeHorizontalScrollExtent(RecyclerView.State state) {
        detachStickyHeader();
        int extent = super.computeHorizontalScrollExtent(state);
        attachStickyHeader();
        return extent;
    }

    @Override
    public int computeHorizontalScrollOffset(RecyclerView.State state) {
        detachStickyHeader();
        int offset = super.computeHorizontalScrollOffset(state);
        attachStickyHeader();
        return offset;
    }

    @Override
    public int computeHorizontalScrollRange(RecyclerView.State state) {
        detachStickyHeader();
        int range = super.computeHorizontalScrollRange(state);
        attachStickyHeader();
        return range;
    }

    @Override
    public PointF computeScrollVectorForPosition(int targetPosition) {
        detachStickyHeader();
        PointF vector = super.computeScrollVectorForPosition(targetPosition);
        attachStickyHeader();
        return vector;
    }

    @Override
    public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler,
                                    RecyclerView.State state) {
        detachStickyHeader();
        View view = super.onFocusSearchFailed(focused, focusDirection, recycler, state);
        attachStickyHeader();
        return view;
    }

    private void detachStickyHeader() {
        if (mStickyHeader != null) {
            detachView(mStickyHeader);
        }
    }

    private void attachStickyHeader() {
        if (mStickyHeader != null) {
            attachView(mStickyHeader);
        }
    }

    /**
     * Updates the sticky header state (creation, binding, display), to be called whenever there's a layout or scroll
     */
    private void updateStickyHeader(RecyclerView.Recycler recycler, boolean layout) {
        int headerCount = mHeaderPositions.size();
        int childCount = getChildCount();
        if (headerCount > 0 && childCount > 0) {
            // Find first valid child.
            View anchorView = null;
            int anchorIndex = -1;
            int anchorPos = -1;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
                if (isViewValidAnchor(child, params)) {
                    anchorView = child;
                    anchorIndex = i;
                    anchorPos = params.getViewAdapterPosition();
                    break;
                }
            }
            if (anchorView != null && anchorPos != -1) {
                int headerIndex = findHeaderIndexOrBefore(anchorPos);
                int headerPos = headerIndex != -1 ? mHeaderPositions.get(headerIndex) : -1;
                int nextHeaderPos = headerCount > headerIndex + 1 ? mHeaderPositions.get(headerIndex + 1) : -1;

                // Show sticky header if:
                // - There's one to show;
                // - It's on the edge or it's not the anchor view;
                // - Isn't followed by another sticky header;
                if (headerPos != -1
                        && (headerPos != anchorPos || isViewOnBoundary(anchorView))
                        && nextHeaderPos != headerPos + 1) {
                    // Ensure existing sticky header, if any, is of correct type.
                    if (mStickyHeader != null
                            && getItemViewType(mStickyHeader) != mAdapter.getItemViewType(headerPos)) {
                        // A sticky header was shown before but is not of the correct type. Scrap it.
                        scrapStickyHeader(recycler);
                    }

                    // Ensure sticky header is created, if absent, or bound, if being laid out or the position changed.
                    if (mStickyHeader == null) {
                        createStickyHeader(recycler, headerPos);
                    }
                    if (layout || getPosition(mStickyHeader) != headerPos) {
                        bindStickyHeader(recycler, headerPos);
                    }

                    // Draw the sticky header using translation values which depend on orientation, direction and
                    // position of the next header view.
                    View nextHeaderView = null;
                    if (nextHeaderPos != -1) {
                        nextHeaderView = getChildAt(anchorIndex + (nextHeaderPos - anchorPos));
                        // The header view itself is added to the RecyclerView. Discard it if it comes up.
                        if (nextHeaderView == mStickyHeader) {
                            nextHeaderView = null;
                        }
                    }
                    mStickyHeader.setTranslationX(getX(mStickyHeader, nextHeaderView));
                    mStickyHeader.setTranslationY(getY(mStickyHeader, nextHeaderView));
                    return;
                }
            }
        }

        if (mStickyHeader != null) {
            scrapStickyHeader(recycler);
        }
    }

    /**
     * Creates {@link RecyclerView.ViewHolder} for {@code position}, including measure / layout, and assigns it to
     * {@link #mStickyHeader}.
     */
    private void createStickyHeader(@NonNull RecyclerView.Recycler recycler, int position) {
        View stickyHeader = recycler.getViewForPosition(position);

        // Setup sticky header if the adapter requires it.
        if (mAdapter != null) {
            mAdapter.setupStickyHeaderView(stickyHeader);
        }

        // Add sticky header as a child view, to be detached / reattached whenever LinearLayoutManager#fill() is called,
        // which happens on layout and scroll (see overrides).
        addView(stickyHeader);
        measureAndLayout(stickyHeader);

        // Ignore sticky header, as it's fully managed by this LayoutManager.
        ignoreView(stickyHeader);

        mStickyHeader = stickyHeader;
        mStickyHeaderPosition = position;
    }

    /**
     * Binds the {@link #mStickyHeader} for the given {@code position}.
     */
    private void bindStickyHeader(@NonNull RecyclerView.Recycler recycler, int position) {
        // Bind the sticky header.
        recycler.bindViewToPosition(mStickyHeader, position);
        mStickyHeaderPosition = position;
        measureAndLayout(mStickyHeader);

        // If we have a pending scroll wait until the end of layout and scroll again.
        if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
            final ViewTreeObserver vto = mStickyHeader.getViewTreeObserver();
            vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    vto.removeOnGlobalLayoutListener(this);

                    if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
                        scrollToPositionWithOffset(mPendingScrollPosition, mPendingScrollOffset);
                        setPendingScroll(RecyclerView.NO_POSITION, INVALID_OFFSET);
                    }
                }
            });
        }
    }

    /**
     * Measures and lays out {@code stickyHeader}.
     */
    private void measureAndLayout(View stickyHeader) {
        measureChildWithMargins(stickyHeader, 0, 0);
        if (getOrientation() == RecyclerView.VERTICAL) {
            stickyHeader.layout(getPaddingLeft(), 0, getWidth() - getPaddingRight(), stickyHeader.getMeasuredHeight());
        } else {
            stickyHeader.layout(0, getPaddingTop(), stickyHeader.getMeasuredWidth(), getHeight() - getPaddingBottom());
        }
    }

    /**
     * Returns {@link #mStickyHeader} to the {@link RecyclerView}'s {@link RecyclerView.RecycledViewPool}, assigning it
     * to {@code null}.
     *
     * @param recycler If passed, the sticky header will be returned to the recycled view pool.
     */
    private void scrapStickyHeader(@Nullable RecyclerView.Recycler recycler) {
        View stickyHeader = mStickyHeader;
        mStickyHeader = null;
        mStickyHeaderPosition = RecyclerView.NO_POSITION;

        // Revert translation values.
        stickyHeader.setTranslationX(0);
        stickyHeader.setTranslationY(0);

        // Teardown holder if the adapter requires it.
        if (mAdapter != null) {
            mAdapter.teardownStickyHeaderView(stickyHeader);
        }

        // Stop ignoring sticky header so that it can be recycled.
        stopIgnoringView(stickyHeader);

        // Remove and recycle sticky header.
        removeView(stickyHeader);
        if (recycler != null) {
            recycler.recycleView(stickyHeader);
        }
    }

    /**
     * Returns true when {@code view} is a valid anchor, ie. the first view to be valid and visible.
     */
    private boolean isViewValidAnchor(View view, RecyclerView.LayoutParams params) {
        if (!params.isItemRemoved() && !params.isViewInvalid()) {
            if (getOrientation() == RecyclerView.VERTICAL) {
                if (getReverseLayout()) {
                    return view.getTop() + view.getTranslationY() <= getHeight() + mTranslationY;
                } else {
                    return view.getBottom() - view.getTranslationY() >= mTranslationY;
                }
            } else {
                if (getReverseLayout()) {
                    return view.getLeft() + view.getTranslationX() <= getWidth() + mTranslationX;
                } else {
                    return view.getRight() - view.getTranslationX() >= mTranslationX;
                }
            }
        } else {
            return false;
        }
    }

    /**
     * Returns true when the {@code view} is at the edge of the parent {@link RecyclerView}.
     */
    private boolean isViewOnBoundary(View view) {
        if (getOrientation() == RecyclerView.VERTICAL) {
            if (getReverseLayout()) {
                return view.getBottom() - view.getTranslationY() > getHeight() + mTranslationY;
            } else {
                return view.getTop() + view.getTranslationY() < mTranslationY;
            }
        } else {
            if (getReverseLayout()) {
                return view.getRight() - view.getTranslationX() > getWidth() + mTranslationX;
            } else {
                return view.getLeft() + view.getTranslationX() < mTranslationX;
            }
        }
    }

    /**
     * Returns the position in the Y axis to position the header appropriately, depending on orientation, direction and
     * {@link android.R.attr#clipToPadding}.
     */
    private float getY(View headerView, View nextHeaderView) {
        if (getOrientation() == RecyclerView.VERTICAL) {
            float y = mTranslationY;
            if (getReverseLayout()) {
                y += getHeight() - headerView.getHeight();
            }
            if (nextHeaderView != null) {
                if (getReverseLayout()) {
                    y = Math.max(nextHeaderView.getBottom(), y);
                } else {
                    y = Math.min(nextHeaderView.getTop() - headerView.getHeight(), y);
                }
            }
            return y;
        } else {
            return mTranslationY;
        }
    }

    /**
     * Returns the position in the X axis to position the header appropriately, depending on orientation, direction and
     * {@link android.R.attr#clipToPadding}.
     */
    private float getX(View headerView, View nextHeaderView) {
        if (getOrientation() != RecyclerView.VERTICAL) {
            float x = mTranslationX;
            if (getReverseLayout()) {
                x += getWidth() - headerView.getWidth();
            }
            if (nextHeaderView != null) {
                if (getReverseLayout()) {
                    x = Math.max(nextHeaderView.getRight(), x);
                } else {
                    x = Math.min(nextHeaderView.getLeft() - headerView.getWidth(), x);
                }
            }
            return x;
        } else {
            return mTranslationX;
        }
    }

    /**
     * Finds the header index of {@code position} in {@code mHeaderPositions}.
     */
    private int findHeaderIndex(int position) {
        int low = 0;
        int high = mHeaderPositions.size() - 1;
        while (low <= high) {
            int middle = (low + high) / 2;
            if (mHeaderPositions.get(middle) > position) {
                high = middle - 1;
            } else if (mHeaderPositions.get(middle) < position) {
                low = middle + 1;
            } else {
                return middle;
            }
        }
        return -1;
    }

    /**
     * Finds the header index of {@code position} or the one before it in {@code mHeaderPositions}.
     */
    private int findHeaderIndexOrBefore(int position) {
        int low = 0;
        int high = mHeaderPositions.size() - 1;
        while (low <= high) {
            int middle = (low + high) / 2;
            if (mHeaderPositions.get(middle) > position) {
                high = middle - 1;
            } else if (middle < mHeaderPositions.size() - 1 && mHeaderPositions.get(middle + 1) <= position) {
                low = middle + 1;
            } else {
                return middle;
            }
        }
        return -1;
    }

    /**
     * Finds the header index of {@code position} or the one next to it in {@code mHeaderPositions}.
     */
    private int findHeaderIndexOrNext(int position) {
        int low = 0;
        int high = mHeaderPositions.size() - 1;
        while (low <= high) {
            int middle = (low + high) / 2;
            if (middle > 0 && mHeaderPositions.get(middle - 1) >= position) {
                high = middle - 1;
            } else if (mHeaderPositions.get(middle) < position) {
                low = middle + 1;
            } else {
                return middle;
            }
        }
        return -1;
    }

    private void setPendingScroll(int position, int offset) {
        mPendingScrollPosition = position;
        mPendingScrollOffset = offset;
    }

    /**
     * Handles header positions while adapter changes occur.
     * <p>
     * This is used in detriment of {@link RecyclerView.LayoutManager}'s callbacks to control when they're received.
     */
    private class HeaderPositionsAdapterDataObserver extends RecyclerView.AdapterDataObserver {
        @Override
        public void onChanged() {
            // There's no hint at what changed, so go through the adapter.
            mHeaderPositions.clear();
            int itemCount = mAdapter.getItemCount();
            for (int i = 0; i < itemCount; i++) {
                if (mAdapter.isStickyHeader(i)) {
                    mHeaderPositions.add(i);
                }
            }

            // Remove sticky header immediately if the entry it represents has been removed. A layout will follow.
            if (mStickyHeader != null && !mHeaderPositions.contains(mStickyHeaderPosition)) {
                scrapStickyHeader(null);
            }
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            // Shift headers below down.
            int headerCount = mHeaderPositions.size();
            if (headerCount > 0) {
                for (int i = findHeaderIndexOrNext(positionStart); i != -1 && i < headerCount; i++) {
                    mHeaderPositions.set(i, mHeaderPositions.get(i) + itemCount);
                }
            }

            // Add new headers.
            for (int i = positionStart; i < positionStart + itemCount; i++) {
                if (mAdapter.isStickyHeader(i)) {
                    int headerIndex = findHeaderIndexOrNext(i);
                    if (headerIndex != -1) {
                        mHeaderPositions.add(headerIndex, i);
                    } else {
                        mHeaderPositions.add(i);
                    }
                }
            }
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            int headerCount = mHeaderPositions.size();
            if (headerCount > 0) {
                // Remove headers.
                for (int i = positionStart + itemCount - 1; i >= positionStart; i--) {
                    int index = findHeaderIndex(i);
                    if (index != -1) {
                        mHeaderPositions.remove(index);
                        headerCount--;
                    }
                }

                // Remove sticky header immediately if the entry it represents has been removed. A layout will follow.
                if (mStickyHeader != null && !mHeaderPositions.contains(mStickyHeaderPosition)) {
                    scrapStickyHeader(null);
                }

                // Shift headers below up.
                for (int i = findHeaderIndexOrNext(positionStart + itemCount); i != -1 && i < headerCount; i++) {
                    mHeaderPositions.set(i, mHeaderPositions.get(i) - itemCount);
                }
            }
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            // Shift moved headers by toPosition - fromPosition.
            // Shift headers in-between by -itemCount (reverse if upwards).
            int headerCount = mHeaderPositions.size();
            if (headerCount > 0) {
                if (fromPosition < toPosition) {
                    for (int i = findHeaderIndexOrNext(fromPosition); i != -1 && i < headerCount; i++) {
                        int headerPos = mHeaderPositions.get(i);
                        if (headerPos >= fromPosition && headerPos < fromPosition + itemCount) {
                            mHeaderPositions.set(i, headerPos - (toPosition - fromPosition));
                            sortHeaderAtIndex(i);
                        } else if (headerPos >= fromPosition + itemCount && headerPos <= toPosition) {
                            mHeaderPositions.set(i, headerPos - itemCount);
                            sortHeaderAtIndex(i);
                        } else {
                            break;
                        }
                    }
                } else {
                    for (int i = findHeaderIndexOrNext(toPosition); i != -1 && i < headerCount; i++) {
                        int headerPos = mHeaderPositions.get(i);
                        if (headerPos >= fromPosition && headerPos < fromPosition + itemCount) {
                            mHeaderPositions.set(i, headerPos + (toPosition - fromPosition));
                            sortHeaderAtIndex(i);
                        } else if (headerPos >= toPosition && headerPos <= fromPosition) {
                            mHeaderPositions.set(i, headerPos + itemCount);
                            sortHeaderAtIndex(i);
                        } else {
                            break;
                        }
                    }
                }
            }
        }

        private void sortHeaderAtIndex(int index) {
            int headerPos = mHeaderPositions.remove(index);
            int headerIndex = findHeaderIndexOrNext(headerPos);
            if (headerIndex != -1) {
                mHeaderPositions.add(headerIndex, headerPos);
            } else {
                mHeaderPositions.add(headerPos);
            }
        }
    }

    public static class SavedState implements Parcelable {
        private Parcelable superState;
        private int pendingScrollPosition;
        private int pendingScrollOffset;

        public SavedState() {
        }

        public SavedState(Parcel in) {
            superState = in.readParcelable(SavedState.class.getClassLoader());
            pendingScrollPosition = in.readInt();
            pendingScrollOffset = in.readInt();
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel parcel, int i) {
            parcel.writeParcelable(superState, i);
            parcel.writeInt(pendingScrollPosition);
            parcel.writeInt(pendingScrollOffset);
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}

0 个答案:

没有答案