📜 ⬆️ ⬇️

Make the parallax header in RecyclerView

Greetings
With the advent of material design come new items. For example, RecyclerView has appeared, which is already known to many. About him on Habré wrote more than once: tyts , tuts .

It seems it is understandable how to use it, but you want more. Usually, when switching to new alternatives, something is missing. So I did not have what it is. It took me a parallax effect, like in Google Play on the page of a specific application. Implementations for ListView and ScrollView are available. I searched in the great and mighty, and all I found was this repository . The solution seems to be working, and the 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, namely with the need to create an adapter that will support the header. The adapter should not be different from the principles of the normal adapter for RecyclerView.

The class RecyclerView.Adapter has a method public int getItemViewType (int position) , which the type returns for each position, by default it always returns 0. It will help us.
')
Immediately warn you, the resulting class will be abstract. And some methods, respectively, too.

Override 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 TYPE_VIEW_HEADER value is chosen as such in an attempt to avoid accidental hits in getMainItemType .
According to the logic, further it is necessary to implement the methods responsible for creating the View and displaying information on it, 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 <HH extends HeaderHolder> 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, coercion of types certainly does not look very nice, but I did not think of a better way to leave it in the same form without them.

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

What is written already provides functionality to easily do the usual header, but the title of the article promised more.

Therefore, we continue.

So, the 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 specified amount. This will be the internal class of our adapter.

The code for this class itself
  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 class HeaderHolder so that it places the passed View in our wonder 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 necessary values ​​to CustomWrapper and we will be parallax. To do this, you need to subscribe to scroll events in RecyclerView. I used the inner class for this.

ScrollListener
  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(); } } 


It's simple. When scrolling, the onScrolled method is called. In it, we change the current position of the scroll and check if we can do something with the header. If yes, then do parallax. And when the header goes out of the screen area, then we stop doing all sorts of operations with it, so there is no need for that.

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 is obvious that for the effect of the paralax, you just need to reduce the "speed" of movement. To do this, use the coefficient SCROLL_SPEED. Then we simply move the header to the new received value - and that's it.

To use this all is quite simple.

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

The bonus is HeaderGridLayoutManager , the heir of GridLayoutManager, which provides the functionality with the header, and the parallax works there too.

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

Look like that's it. Thanks for attention.

Source: https://habr.com/ru/post/250689/


All Articles