tinkoff-mobile-tech / ScrollingPagerIndicator

Pager indicator inspired by Instagram. Lightweight and easy to set up.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to use with custom LayoutManger?

SamerDiab opened this issue · comments

Hi, i really like your library, and been wondering about using it with a custom layout manager, I am using recycleviewer with a custom layout manager, https://github.com/yarolegovich/DiscreteScrollView ,
Can someone please provide help?
Thank you.

Hi! I have written an attacher class for you based on RecyclerViewAttacher.

There is an example of usage:

indicator.attachToPager(discreteScrollView, new DiscreteScrollViewAttacher());

The only differences from RecyclerViewAttacher are:

  • new line indicator.setLooped(attachedAdapter instanceof InfiniteScrollAdapter);
  • checks for LinearLayoutManager were deleted.

The attacher class code:

public class DiscreteScrollViewAttacher implements ScrollingPagerIndicator.PagerAttacher<DiscreteScrollView> {

    private ScrollingPagerIndicator indicator;
    private RecyclerView recyclerView;
    private RecyclerView.LayoutManager layoutManager;
    private RecyclerView.Adapter<?> attachedAdapter;

    private RecyclerView.OnScrollListener scrollListener;
    private RecyclerView.AdapterDataObserver dataObserver;

    private final boolean centered;
    private final int currentPageLeftCornerX;

    private int measuredChildWidth;

    /**
     * Default constructor. Use this if current page in recycler is centered.
     * All pages must have the same width.
     * Like this:
     *
     * +------------------------------+
     * |---+  +----------------+  +---|
     * |   |  |     current    |  |   |
     * |   |  |      page      |  |   |
     * |---+  +----------------+  +---|
     * +------------------------------+
     */
    public DiscreteScrollViewAttacher() {
        currentPageLeftCornerX = 0; // Unused when centered
        centered = true;
    }

    /**
     * Use this constructor if current page in recycler isn't centered.
     * All pages must have the same width.
     * Like this:
     *
     * +-|----------------------------+
     * | +--------+  +--------+  +----|
     * | | current|  |        |  |    |
     * | |  page  |  |        |  |    |
     * | +--------+  +--------+  +----|
     * +-|----------------------------+
     *   | currentPageLeftCornerX
     *   |
     * @param currentPageLeftCornerX x coordinate of current view left corner relative to recycler view.
     */
    public DiscreteScrollViewAttacher(int currentPageLeftCornerX) {
        this.currentPageLeftCornerX = currentPageLeftCornerX;
        this.centered = false;
    }

    @Override
    public void attachToPager(@NonNull final ScrollingPagerIndicator indicator, @NonNull final DiscreteScrollView pager) {
        this.layoutManager = pager.getLayoutManager();
        this.recyclerView = pager;

        this.attachedAdapter = pager.getAdapter();
        this.indicator = indicator;

        dataObserver = new RecyclerView.AdapterDataObserver() {
            @Override
            public void onChanged() {
                indicator.setDotCount(attachedAdapter.getItemCount());
                updateCurrentOffset();
            }

            @Override
            public void onItemRangeChanged(int positionStart, int itemCount) {
                onChanged();
            }

            @Override
            public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
                onChanged();
            }

            @Override
            public void onItemRangeInserted(int positionStart, int itemCount) {
                onChanged();
            }

            @Override
            public void onItemRangeRemoved(int positionStart, int itemCount) {
                onChanged();
            }

            @Override
            public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
                onChanged();
            }
        };
        attachedAdapter.registerAdapterDataObserver(dataObserver);

        indicator.setLooped(attachedAdapter instanceof InfiniteScrollAdapter);
        indicator.setDotCount(attachedAdapter.getItemCount());
        updateCurrentOffset();

        scrollListener = new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                if (newState == RecyclerView.SCROLL_STATE_IDLE && isInIdleState()) {
                    int newPosition = findCompletelyVisiblePosition();
                    if (newPosition != RecyclerView.NO_POSITION) {
                        indicator.setDotCount(attachedAdapter.getItemCount());
                        if (newPosition < attachedAdapter.getItemCount()) {
                            indicator.setCurrentPosition(newPosition);
                        }
                    }
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                updateCurrentOffset();
            }
        };

        recyclerView.addOnScrollListener(scrollListener);
    }

    @Override
    public void detachFromPager() {
        attachedAdapter.unregisterAdapterDataObserver(dataObserver);
        recyclerView.removeOnScrollListener(scrollListener);
        measuredChildWidth = 0;
    }

    private void updateCurrentOffset() {
        final View leftView = findFirstVisibleView();
        if (leftView == null) {
            return;
        }

        int position = recyclerView.getChildAdapterPosition(leftView);
        if (position == RecyclerView.NO_POSITION) {
            return;
        }
        final int itemCount = attachedAdapter.getItemCount();

        // In case there is an infinite pager
        if (position >= itemCount && itemCount != 0) {
            position = position % itemCount;
        }

        final float offset = (getCurrentFrameLeft() - leftView.getX()) / leftView.getMeasuredWidth();

        if (offset >= 0 && offset <= 1 && position < itemCount) {
            indicator.onPageScrolled(position, offset);
        }
    }

    private int findCompletelyVisiblePosition() {
        for (int i = 0; i < recyclerView.getChildCount(); i++) {
            View child = recyclerView.getChildAt(i);
            if (child.getX() >= getCurrentFrameLeft() && child.getX() + child.getMeasuredWidth() <= getCurrentFrameRight()) {
                RecyclerView.ViewHolder holder = recyclerView.findContainingViewHolder(child);
                if (holder != null && holder.getAdapterPosition() != RecyclerView.NO_POSITION) {
                    return holder.getAdapterPosition();
                }
            }
        }
        return RecyclerView.NO_POSITION;
    }

    private boolean isInIdleState() {
        return findCompletelyVisiblePosition() != RecyclerView.NO_POSITION;
    }

    @Nullable
    private View findFirstVisibleView() {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
        int firstVisibleChildX = Integer.MAX_VALUE;

        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);

            // Default implementation change: use getX instead of helper
            int childStart = (int) child.getX();

            // if child is more to start than previous closest, set it as closest

            // Default implementation change:
            // Fix for any count of visible items
            // We make assumption that all children have the same width
            if (childStart + child.getMeasuredWidth() < firstVisibleChildX
                    && childStart + child.getMeasuredWidth() > getCurrentFrameLeft()) {
                firstVisibleChildX = childStart;
                closestChild = child;
            }
        }

        return closestChild;
    }

    private float getCurrentFrameLeft() {
        if (centered) {
            return (recyclerView.getMeasuredWidth() - getChildWidth()) / 2;
        } else {
            return currentPageLeftCornerX;
        }
    }

    private float getCurrentFrameRight() {
        if (centered) {
            return (recyclerView.getMeasuredWidth() - getChildWidth()) / 2 + getChildWidth();
        } else {
            return currentPageLeftCornerX + getChildWidth();
        }
    }

    private float getChildWidth() {
        if (measuredChildWidth == 0) {
            for (int i = 0; i < recyclerView.getChildCount(); i++) {
                View child = recyclerView.getChildAt(i);
                if (child.getMeasuredWidth() != 0) {
                    measuredChildWidth = child.getMeasuredWidth();
                    return measuredChildWidth;
                }
            }
        }
        return measuredChildWidth;
    }
}

Wanna see it in sample project.

I ended up writing my own scrolling page indicator, but i might revert back to this as the animation feels smoother, anyways thank you for the help, i will look into it and report back.

This implementation works perfectly! Much appreciated!