15

I want to change my ListView I currently have over to use RecyclerView so I can make use of StaggeredGridLayoutManager but RecyclerView does not have the ability to add a header like ListView.

Usually with a ListView I set an empty view in the header and put the image below the listview and translate the bottom image with the scrolling of the list to create the Parallax effect.

So with out a header how can I create the same parallax effect with RecyclerView?

tyczj
  • 71,600
  • 54
  • 194
  • 296
  • Do you have access to a `onScroll` method? – Pedro Oliveira Oct 27 '14 at 15:44
  • @PedroOliveira yes I currently use `public void onScrolled(RecyclerView recyclerView, int dx, int dy) ` for other things – tyczj Oct 27 '14 at 15:45
  • `onScrolled` is different from `onScroll`. That method is called at the end of the scroll. `This will be called after the scroll has completed`. So you can't update a view and translate it with this method. – Pedro Oliveira Oct 27 '14 at 15:49
  • @PedroOliveira So what are you suggesting, obviously create a class that extends RecyclerView so i can override onScroll but what then? What I currently do is set a margin for the first 2 indexes for a grid with 2 columns and then translate using `dy` but then the grid gets all messed up when adding a single item to the grid because of the margins – tyczj Oct 27 '14 at 15:55
  • You can't override `onScroll` because there isn't such method in `RecyclerView`. That's currently the problem in implementing the parallax effect. Because other than that it should be easy to add a "header" to it. Just translate the childAt(0). But without a proper `onScroll` method I think that's difficult. – Pedro Oliveira Oct 27 '14 at 15:58
  • @PedroOliveira my only other idea was to set the RecyclerView to the height of the screen, move it in `onCreate` then intercept touch events for the recyclerview and translate the recycler up until it covers the "header" section that is below the recycler in the view hierarchy. then when its in position hand touch events back to the recycler but thats a hell of a lot of work. – tyczj Oct 27 '14 at 16:05
  • 1
    You can do that however `onTouch` events will not cover the fling of the scroll. So you will not get the correct behavior on the parallax effect. – Pedro Oliveira Oct 27 '14 at 16:20

4 Answers4

33

the easiest way to do it, is using below onScrollListener without relying on any library.

View view = recyclerView.getChildAt(0);
if(view != null && recyclerView.getChildAdapterPosition(view) == 0)  {
     view.setTranslationY(-view.getTop() / 2);// or use view.animate().translateY();
}

make sure your second viewHolder item has a background color to match the drawer/activity background. so the scrolling looks parallax.

Kosh
  • 6,140
  • 3
  • 36
  • 67
  • 4
    This works quite neat. +1 for small number of line of code. But `getChildAt(0)` on the recyclerView will return the first child on the visible part of the screen, which is not always the header/first view in the actual list. So, keep a reference to that view would be more efficient. – C-- Aug 24 '15 at 10:24
  • 2
    @SubinSebastian Definitely it will take the first child, it depends on you tho, how you want to use it. It can be based on positions u provide or know previously. The choices are unlimited :) – Kosh Aug 24 '15 at 10:26
  • 5
    To make sure it is the first adapter item just do the following check: if(view != null && recyclerView.getChildAdapterPosition(view) == 0) – Roberto B. Jan 13 '16 at 18:07
  • This is amazing!! – AmeyaB Feb 07 '18 at 23:02
26

So today I tried to archive that effect on a RecyclerView. I was able to do it but since the code is too much I will paste here my github project and I will explain some of the key points of the project.

https://github.com/kanytu/android-parallax-recyclerview

First we need to look at getItemViewType on the RecyclerView.Adapter class. This methods defines what type of view we're dealing with. That type will be passed on to onCreateViewHolder and there we can inflate different views. So what I did was: check if the position is the first one. If so then inflate the header, if not inflate a normal row.

I've added also a CustomRelativeLayout that clips the view so we don't have any trouble with the dividers and with the rows getting on top of the header.

From this point you seem to know the rest of the logic behind it.

The final result was:

enter image description here

EDIT:

If you need to insert something in adapter make sure you notify the correct position by adding 1 in the notifyItemChanged/Inserted method. For example:

public void addItem(String item, int position) {
    mData.add(position, item);
    notifyItemInserted(position + 1); //we have to add 1 to the notification position since we don't want to mess with the header
}

Another important edit I've done is the scroll logic. The mCurrentOffset system I was using didn't work with the item insertion since the offset will change if you add an item. So what I did was:

ViewHolder holder = findViewHolderForPosition(0);
if (holder != null)
  ((ParallaxRecyclerAdapter) getAdapter()).translateHeader(-holder.itemView.getTop() * 0.5f);

To test this I added a postDelayed Runnable, started the app, scrolled to the end, add the item in position 0, and scroll up again. The result was:

enter image description here

If anyone is looking for other parallax effects they can check my other repo:

https://github.com/kanytu/android-parallax-listview

Pedro Oliveira
  • 20,442
  • 8
  • 55
  • 82
  • its a little different with a grid though because you have multiple columns. also did you try adding an item to the adapter after initial population and calling `notifyItemInserted`? because that is another problem since it only calls `onBindViewHolder` for the inserted position so the inflated view for one of the cells is not what it should be – tyczj Oct 27 '14 at 19:22
  • also I was sort of using this question as a reference http://stackoverflow.com/questions/26448717/android-5-0-add-header-footer-to-a-recyclerview – tyczj Oct 27 '14 at 19:25
  • I didn't' test that. I left work already and I will test that tomorrow. I will keep this post updated if I change anything on the project. – Pedro Oliveira Oct 27 '14 at 19:26
  • Yeah I just went and tested again, when you insert something using that method and a positon of 0 `onCreateViewHolder` does not get called again only `onBindViewHolder` so the views will be messed up. using `notifyDatasetChanged` will make it work but you dont get the insert/remove animations since it redoes the adapter – tyczj Oct 27 '14 at 19:30
  • 2
    @tyczj That's because you can't add anything on position 0. Position 0 must be reserved for the header. If you insert it at position 1 you will not have any problems. – Pedro Oliveira Oct 28 '14 at 09:11
  • I have a problem,in some cases,the header background color gets lost. i have blue background color for header,but sometimes when i open the navigation drawer,the header color gets lost. Pedro,can you please help me on this? – Shofiqul Alam Mar 25 '15 at 11:17
  • @PedroOliveira in a grid layout the header is not added on top of the recycler view, but instead on the cell at position 0-0 in the grid. How can I put it on top? I'm mentioning that I'm using as a layout manager GridLayoutManager. – dorin May 14 '15 at 09:22
  • 1
    @PedroOliveira Is there a way to make it work with grid layout manager? When i try to put it on a grid it just puts the header in the first position of the grid, not at the header – Alex Fragotsis Dec 22 '16 at 09:32
  • The next answer by @k0sh should be the accepted one. This is doing way too many things. – AmeyaB Feb 07 '18 at 23:01
2

For kotlin, you may config the recycler view as below

//setting parallax effects
    recyclerView.addOnScrollListener(object :RecyclerView.OnScrollListener(){
        override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
            super.onScrolled(recyclerView, dx, dy)
            val view = recyclerView?.getChildAt(0)
            if (view != null && recyclerView?.getChildAdapterPosition(view) === 0) {
                val imageView = view.findViewById(R.id.parallaxImage)
                imageView.translationY = -view.top / 2f
            }
        }
    })
Kit
  • 2,370
  • 13
  • 11
0

This answer is for those curious about adding a parallax header to a GridLayoutManager or a StaggeredGridLayoutManager

You'll want to add the following code to your adapter in either onBindViewHolder or onCreateViewHolder

StaggeredGridLayoutManager.LayoutParams layoutParams =
                    (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
            layoutParams.setFullSpan(true);
Zach
  • 447
  • 6
  • 16