Making a parallax header in a RecyclerView

    Greetings!
    With the advent of material design, new elements also come. For example, RecyclerView appeared, which many already know. About him on the habr more than once wrote: tyts , tuts .

    It seems like using it is understandable, but you want more. Usually, when switching to new alternatives, something is missing. So I did not have enough of what is. I needed to make a parallax effect, like on Google Play on a specific application page. Implementations for ListView and ScrollView are available. I looked in the great and powerful, and all I found was this repository . The solution seems to be working, and people use it. However, I did not like its usability. And as usual, I decided to write my own.

    In general, I started with a simple one, namely the fact that you need to create an adapter that will support the header. The adapter should not have been different from the principles of a conventional adapter for RecyclerView.

    The RecyclerView.Adapter class has a public int getItemViewType (int position) method , which returns a type for each position, always returns 0 by default. It will help us.

    I must warn you right away that the resulting class will be abstract. And some methods, respectively, too.

    We redefine it as follows:

        public static final int TYPE_VIEW_HEADER = Integer.MAX_VALUE;
        private int sizeDiff = 1;
        @Override
        public final int getItemViewType(final int position)
        {
            if (position == 0 && enableHeader)
                return TYPE_VIEW_HEADER;
            return getMainItemType(position - sizeDiff);
        }
       protected abstract int getMainItemType(int position);
    

    The value TYPE_VIEW_HEADER was chosen as such in an attempt to avoid accidental hits in getMainItemType .
    Logically, you will have to implement the methods responsible for creating the View and displaying information on us, as well as several abstract methods.
    Hidden text
        @Override
        public final RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType)
        {
            if (viewType == TYPE_VIEW_HEADER)
                return onCreateHeaderViewHolder(parent);
            return onCreateMainViewHolder(parent, viewType);
        }
        protected abstract HeaderHolder onCreateHeaderViewHolder(final ViewGroup parent);
        protected abstract VH onCreateMainViewHolder(final ViewGroup parent, final int viewType);
        @Override
        public final void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position)
        {
            if (holder.getItemViewType() == TYPE_VIEW_HEADER)
            {
                onBindHeaderViewHolder((HeaderHolder) holder);
                return;
            }
            onBindMainViewHolder((VH) holder, position - sizeDiff);
        }
        protected abstract  void onBindHeaderViewHolder(final HH holder);
        protected abstract void onBindMainViewHolder(final VH holder, final int position);
        protected static class HeaderHolder extends RecyclerView.ViewHolder
        {
            public HeaderHolder(final View itemView)
            {
               super(itemView);
            }
        }
    



    Yes, casting certainly doesn’t look very nice, but I haven’t come up with a better way to leave it the same without them.

    Briefly about what the code does above. First, we give the desired type for in the getItemViewType method , then based on it, create the desired ViewHolder in onCreateViewHolder , and bind the data in onBindViewHolder by also checking viewType.

    What is written already provides functionality to easily make headers more common, but the title of the article promised more.

    Therefore, we continue.

    So, header is displayed, now let's make it move. But it should move in the opposite direction of the RecyclerView content movement.

    To do this, we need an auxiliary container that can move its contents by a given amount. This will be the inner class of our adapter.

    Code of this class
        private static class CustomWrapper extends FrameLayout
        {
            private int offset;
            public CustomWrapper(Context context)
            {
                super(context);
            }
            @Override
            protected void dispatchDraw(Canvas canvas)
            {
                canvas.clipRect(new Rect(getLeft(), getTop(), getRight(), getBottom() + offset));
                super.dispatchDraw(canvas);
            }
            public void setYOffset(int offset)
            {
                this.offset = offset;
                invalidate();
            }
        }
    


    Then we rewrite our HeaderHolder class so that it places the passed View into our miracle container:

    Updated HeaderHolder
        protected static class HeaderHolder extends RecyclerView.ViewHolder
        {
            public HeaderHolder(final View itemView)
            {
                super(new CustomWrapper(itemView.getContext()));
                final ViewGroup parent = (ViewGroup) itemView.getParent();
                if (parent != null)
                    parent.removeView(itemView);
                ((CustomWrapper) this.itemView).addView(itemView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                this.itemView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            }
        }
    


    Now it remains only to send the desired values ​​to CustomWrapper and we will be parallax. To do this, you need to subscribe to scroll events at RecyclerView. I used the inner class for this.

    Scrollrollistener
        private class ScrollListener extends RecyclerView.OnScrollListener
        {
            @Override
            public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy)
            {
                totalScroll += dy;
                if (customWrapper != null && !headerOutOfVisibleRange())
                {
                    doParallaxWithHeader(totalScroll);
                }
                changeVisibilityHeader();
            }      
            private void changeVisibilityHeader()
            {
                if (customWrapper != null)
                {
                    customWrapper.setVisibility(headerOutOfVisibleRange() ? View.INVISIBLE : View.VISIBLE);
                }
            }
            private boolean headerOutOfVisibleRange()
            {
                return totalScroll > getHeaderHeight();
            }
        }
    


    Everything is simple here. When scrolling, the onScrolled method is called. In it, we change the current position of the scroll and check if we can do anything with the header. If so, then do parallax. And when the header goes beyond the area of ​​the screen, then we stop doing all kinds of operations with it, because this is not necessary.

    And the last code insert
        private void doParallaxWithHeader(float offset)
        {
            float parallaxOffset = offset * SCROLL_SPEED;
            moveHeaderToOffset(parallaxOffset);
            if (parallaxListener != null && enableHeader)
                parallaxListener.onParallaxScroll(left);
            customWrapper.setYOffset(Math.round(parallaxOffset));
            notifyHeaderChanged();
        }
        private void moveHeaderToOffset(final float parallaxOffset)
        {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
            {
                customWrapper.setTranslationY(parallaxOffset);
            }
            else
            {
                TranslateAnimation anim = createTranslateAnimation(parallaxOffset);
                customWrapper.startAnimation(anim);
            }
        }
        private TranslateAnimation createTranslateAnimation(final float parallaxOffset)
        {
            TranslateAnimation anim = new TranslateAnimation(0, 0, parallaxOffset, parallaxOffset);
            anim.setFillAfter(true);
            anim.setDuration(0);
            return anim;
        }
        public final void notifyHeaderChanged()
        {
            notifyItemChanged(0);
        }
        public final void notifyMainItemChanged(final int position)
        {
            notifyItemChanged(position + sizeDiff);
        }
    


    I think it’s obvious that for the parallax effect you just need to reduce the “speed” of movement. For this, the coefficient SCROLL_SPEED is used. Then we simply move the header to the new received value - and that’s it.

    Using it is all very simple.

    Sources can be taken here ; an example there. This is all published to jCenter, so it connects in a single line in gradle.

    The bonus is the HeaderGridLayoutManager , the successor to the GridLayoutManager, which provides functionality with a header, and parallax also works there.

    There is also SpacesItemDecoration , which sets the desired distance between all RecyclerView elements. Not working with StaggeredGridLayoutManager yet .

    Look like that's it. Thanks for attention.

    Also popular now: