I'm trying to play with Parallax and getting some weird bugs, wondering if anyone can add some input to it. The only app I've seen implement parallax effectively is soundcloud. It's quite subtle, but each item has an image background and it has he parallax effect as you scroll.
I've created a custom RecyclerView to handle this, here is what I have so far:
public class ParallaxScrollListener extends RecyclerView.OnScrollListener {
private float scrollSpeed = 0.5f;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = layoutManager.findFirstVisibleItemPosition();
int visibleCount = Math.abs(firstVisible - layoutManager.findLastVisibleItemPosition());
Matrix imageMatrix;
float tempSpeed = -100;
if (dy > 0) {
tempSpeed = scrollSpeed;
} else if (dy < 0) {
tempSpeed = -scrollSpeed;
}
for (int i = firstVisible; i < (firstVisible + visibleCount); i++) {
ImageView imageView = ((MyClass.MyAdapter.MyViewHolder) recyclerView.getLayoutManager().findViewByPosition(i).getTag()).image;
if (imageView != null) {
imageMatrix = imageView.getImageMatrix();
imageMatrix.postTranslate(0, tempSpeed);
imageView.setImageMatrix(imageMatrix);
imageView.invalidate();
}
}
}
In my RecyclerView Adapter's onBindView I have the following as well:
Matrix matrix = viewHolder.image.getImageMatrix();
matrix.postTranslate(0, 0);
viewHolder.image.setImageMatrix(matrix);
viewHolder.itemView.setTag(viewHolder);
Finally inside the onViewRecycled method I have the following:
#Override
public void onViewRecycled(MyViewHolder viewHolder) {
super.onViewRecycled(viewHolder);
if (viewHolder.image != null) {
viewHolder.image.setScaleType(ImageView.ScaleType.MATRIX);
Matrix matrix = viewHolder.image.getImageMatrix();
// this is set manually to show to the center
matrix.reset();
viewHolder.image.setImageMatrix(matrix);
}
}
I been working with this code on Github to get the idea
So the parallax works, but but views in my RecyclerView move as well. I have a CardView beneath the image and it moves, creating big gaps between each item. Scrolling is what causes this, the more the scroll up and down the bigger the gaps get, and the images get smaller as the parallax moves them out of their bounds.
I've tried messing with the numbers like scrollSpeed in the OnScrollListener but while it reduces the bug it also reduces the parallax.
Has anyone got any ideas on how I can achieve a bug free parallax effect on each item in my RecyclerView? I feel like I'm getting somewhere with this but it's still very buggy and I don't know what the next step is.
P.s I've tried looking at 3rd party libraries but they all seem to only use header parallax like the CoordinatorLayout, I haven't found any that do it just on each item in a list.
I'm hoping this question gets a good discussion going even if I don't solve my problem because Parallax seems to be underused in Android and there's very little around about it.
Thanks for you time, appreciate any help.
I managed to get Parallax working with this library: https://github.com/yayaa/ParallaxRecyclerView
For anyone doing this themselves, it's still a good thing to play and see how it works.
Similar concept to my code but it actually works! haha.
You are on the right track. You have to use a ScrollListener. Furtermore, you have to access RecyclerViews LayoutManager and iterate over all items that are visible and set translateY according to the amount of pixels scrolled.
The things get a little bit more complicated, because you can't use recyclerView.getChildAt(pos) because LayoutManager is responsible to layout elements and they might be in different order in the LayoutManager than getChildAt(pos).
So the algorithm basically should look like this (pseudo code, assuming LinearLayoutManager is used):
for (int i = layoutManager.findFirstVisibleItemPosition(); i <= layoutmanager.findLastVisibleItemPosition; i++){
// i is the adapter position
ViewHolder vh = recyclerView.findViewHolderForAdapterPosition(i);
vh.imageView.setTranslationY( computedParalaxOffset ); // assuming ViewHolder has a imageView field on which you want to apply the parallax effect
}
Related
I have a vertically scrolling RecyclerView with horizontally scrolling inner RecyclerViews just like this.
With this implementation, users can scroll each horizontal recyclerview synchronously. However, when a user scroll vertically to the parent recyclerView, a new horizontal recyclerview which has just attached on window doesn't display on same scroll x position. This is normal. Because it has just created.
So, I had tried to scroll to the scrolled position before it was displayed. Just like this:
Note: this is in adapter of the parent recyclerview whose orientation is vertical.
#Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
CellColumnViewHolder viewHolder = (CellColumnViewHolder) holder;
if (m_nXPosition != 0) {
// this doesn't work properly
viewHolder.m_jRecyclerView.scrollBy(m_nXPosition, 0);
}
}
As you can see, scrollBy doesn't effect for row 10, row 11, row 12 and row 13 After that, I debugged the code to be able find out find out what's happening. When I set scroll position using scrollBy, childCount() return zero for row 10, row 11, row 12 and row 13 So they don't scroll. But why ? and Why others work ?
How can I fix this ?
Is onViewAttachedToWindow right place to scroll new attached recyclervViews ?
Note: I have also test scrollToPosition(), it doesn't get any problem like this. But I can't use it at my case. Because users can scroll to the any x position which may not the exact position. So I need to set scroll position using x value instead of the position.
Edit: You can check The source code
I found a solution that is use scrollToPositionWithOffset method instead using scrollBy. Even if both of two scroll another position, they have really different work process in back side.
For example: if you try to use scrollBy to scroll any pixel position and your recyclerView had not been set any adapter which means there is no any data to display and so it has no any items yet, then scrollBy doesn't work. RecyclerView uses its layoutManager's scrollBy method. So in my case, I am using LinearLayoutManager to the horizontal recyclerViews.
Lets see what it's doing :
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
mLayoutState.mRecycle = true;
ensureLayoutState();
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
}
return 0;
}
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
As you can see scrollBy ignores the scroll intentions if there is no any child at that time.
if (getChildCount() == 0 || dy == 0) {
return 0;
}
On the other hand scrollToPosition can work perfectly even if there is no any set data yet.
According to the Pro RecyclerView slide, the below sample works perfectly. However you can not do that with scrollBy.
void onCreate(SavedInstanceState state) {
....
mRecyclerView.scrollToPosition(selectedPosition);
mRecyclerView.setAdapter(myAdapter);
}
As a result, I have changed little thing to use scrollToPositionWithOffset().
Before this implementation I was calculating the exact scroll x position as a pixel.
After that, when the scroll came idle state, calculating the first complete visible position to the first parameter of the scrollToPositionWithOffset().
For second parameter which is the offset, I am getting the value using view.getLeft() function which helps to get left position of this view relative to its parent.
And it works perfectly!!
I am coding an Android application for a school project.
I have implemented a RecyclerView with horizontal scroll using LinearLayoutManager.HORIZONTAL.
I would like to make it such that a specific element, for example element n, is in the middle of the set of currently visible elements, like so:
I have searched around, and found this link: https://www.reddit.com/r/androiddev/comments/3o0xzw/how_do_you_scroll_to_an_item_with_recyclerview_to/
The solution involved taking half the sum of layoutManager.findFirstVisibleItemPosition() and layoutManager.findLastVisibleItemPosition() and adding it to the position of the element you want to be in the middle.
However, these two methods always returned RecyclerView.NO_POSITION when I used this horizontally scrolling RecyclerView, hence half the sum would be 0, and it would render the calculation useless.
Here is my code:
RecyclerView selectDay = (RecyclerView) findViewById(R.id.lv_selectDay);
daySelectorAdapter adapter = new daySelectorAdapter(this, arrayList);
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
selectDay.setAdapter(adapter);
selectDay.setLayoutManager(layoutManager);
int firstVisible = layoutManager.findFirstVisibleItemPosition();
Log.d("FIRSTVISIBLE", String.valueOf(firstVisible)); // returns -1
int lastVisible = layoutManager.findLastVisibleItemPosition();
Log.d("LASTVISIBLE", String.valueOf(lastVisible)); // also -1
int halfScreenOffset = (lastVisible - firstVisible) / 2;
Log.d("HALFOFFSET", String.valueOf(halfScreenOffset));
layoutManager.scrollToPositionWithOffset((15 + halfScreenOffset), 0);
May I know if anyone has a workaround to this problem? Thanks in advance.
EDIT: When I tried implementing a onScrollListener and cast recyclerView.getLayoutManager() to LinearLayoutManager, these methods worked perfectly fine. May I know why?
EDIT 2:
I used this solution right here: RecyclerView smoothScroll to position in the center. android, However why do the methods return NO_POSITION?
I’m using a staggered recycler view layout for a list of photos. I want the spacing on the sides to be zero while still having space between the two columns. I’m using an item decoration sub class to get the spacing seen in the attached photo. I know I have control over the left and right spacing but the problem is that I never know which column the photo is in. It seems like the staggered layout manager does some of its own reordering. I've tried using getChildAdapterPosition but it seems to return the position in the data source array and not the actual position of the photo in the layout. Any idea how I should approach this?
I managed to get it working. In my case, I don't need any borders on the left or right edges of the screen. I just need borders in the middle and bottom. The solution is to get the layout parameters of the view that are of type StaggeredGridLayoutManager.LayoutParams. In those parameters you can get the spanIndex that tells you on which index the view is. So if you have a spanCount of 2, the left view will have a spanIndex of 0 and the right view will have a spanIndex of 1.
Here is my code, maybe it help you.
public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpaceItemDecoration(int space) {
this.space = space;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
int spanIndex = lp.getSpanIndex();
if (position > 0) {
if (spanIndex == 1) {
outRect.left = space;
} else {
outRect.right = space;
}
outRect.bottom = space * 2;
}
}
}
In my case, firstly I have to get the position, since on the index 0 I have a header View, which doesn't have any borders. After that, I get the span index and depending on it I set the borders that I need on that View. And finally I set the bottom border on every View.
so the one solution I was able to use was with an item decorator but it definitely is a little weird/hacky feeling.
Basically you'll adjust the outer rectangle of the item based on its column position (or something similar). My understanding is that the outer rectangle is more or less the spacing you want to change. Give the code below a try, obviously you'll need to make your own adjustments and logic to 'calculate' which column the item is on but this should be enough to figure it out, hopefully:
recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int left = outRect.left;
int right = outRect.right;
int top = outRect.top;
int bottom = outRect.bottom;
int idx = parent.getChildPosition(view);
int perRow = gridLayoutManager.getSpanCount();
int adj = blahh... // some adjustment
if (idx < itemsPerRow) {
// on first row, adjust top if needed
}
if(idx % perRow == 0){
// on first column, adjust. Left magically adjusts bottom, so adjust it too...
left += adj;
bottom -= adj;
}
if(idx % itemsPerRow == perRow - 1){
// on last column, adjust. Right magically adjusts bottom, so adjust it too...
right += adjustment;
bottom -= adjustment;
}
outRect.set(left, top, right, bottom);
}
});
Again this is hacky and takes some trial and error to get right.
Another solution I have tried with some success is to define different views for the different columns. In your case the columns would have views with different, negative margins, on the left and right to get the effect you want.
As a side note, I assume you are using an elevation on the card view. One thing I've noticed is that if the card view does NOT have elevation and instead you handle it yourself (yeah, i know, isn't the point to not handle elevation yourself) much of this difficulty goes away and things start to behave, likely because of the elevation/shadow calculations. But anyway... Hope this is at least somewhat helpful...
Actually I don't know how it properly called - overlay, parallax or slideUP, whatever, I have an Activity called "cafe details" which presents a Container(LinearLayout) with header information (name, min price, delivery time min e.t.c) and other container (ViewPager) which contains a ExpandableListView with something information (menus&dishes) and all I want to do is slide up my Viewpager when scrolls listview to scpecific Y position to cover(or overlay) header information.
A similar effect (but with parallax that I don't need to use) looks like this
I can detect when user scrolling listview down or up but how I can move container with ViewPager to overlay other container? Please give me ideas, regards.
UPD
I have tried a huge number of ways how to implement it and all of them unfortunately are not suitable. So now I have come to next variant - add scroll listener to ListView, calculate scrollY position of view and then based on that move the viewpager on y axis by calling setTranslationY();
Here is some code
1) ViewPager's fragment
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
#Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {
if (getActivity() != null) {
((MainActivity) getActivity()).resizePagerContainer(absListView);
}
}
});
2) MainActivity
//object fields
int previousPos;
private float minTranslation;
private float minHeight;
<--------somewhere in onCreate
minTranslation = - (llVendorDescHeaders.getMeasuredHeight()+llVendorDescNote.getMeasuredHeight());
//llVendorDescHeaders is linearLayout with headers that should be hidden
//lVendorDescNote is a textView on green background;
minHeight = llVendorDescriptionPagerContainer.getMeasuredHeight();
//llVendorDescriptionPagerContainer is a container which contains ViewPager
--------->
public void resizePagerContainer(AbsListView absListView){
final int scrollY = getScrollY(absListView);
if (scrollY != previousPos) {
final float translationY = Math.max(-scrollY, minTranslation);
llVendorDescriptionPagerContainer.setTranslationY(translationY);
previousPos = scrollY;
}
}
private int getScrollY(AbsListView view) {
View child = view.getChildAt(0);
if (child == null) {
return 0;
}
int firstVisiblePosition = view.getFirstVisiblePosition();
int top = child.getTop();
return -top + firstVisiblePosition * child.getHeight() ;
}
This simple solution unfortunately has a problem - it is blinking and twitching (I don't know how to call it right) when scrolls slowly. So instead setTranslationY() I've used an objectAnimator:
public void resizePagerContainer(AbsListView absListView){
.............
ObjectAnimator moveAnim = ObjectAnimator.ofFloat(llVendorDescriptionPagerContainer, "translationY", translationY);
moveAnim.start();
..............
}
I don't like this solution because 1) anyway it does resize viewpager with delay, not instantly 2) I don't think that is good idea to create many ObjectAnimator's objects every time when I scroll my listView.
Need your help and fresh ideas. Regards.
I'm assuming that you are scrolling the top header (the ImageView is a child of the header) based on the scrollY of the ListView/ScrollView, as shown below:
float translationY = Math.max(-scrollY, mMinHeaderTranslation);
mHeader.setTranslationY(translationY);
mTopImage.setTranslationY(-translationY / 3); // For parallax effect
If you want to stick the header/image to a certain dimension and continue the scrolling without moving it anymore, then you can change the value of mMinHeaderTranslation to achieve that effect.
//change this value to increase the dimension of header stuck on the top
int tabHeight = getResources().getDimensionPixelSize(R.dimen.tab_height);
mHeaderHeight = getResources().getDimensionPixelSize(R.dimen.header_height);
mMinHeaderTranslation = -mHeaderHeight + tabHeight;
The code snippets above are in reference to my demo but I think it's still general enough for you.
If you're interested you can check out my demo
https://github.com/boxme/ParallaxHeaderViewPager
Have you tried CoordinatorLayout from this new android's design support library? It looks like it's what you need. Check this video from 3:40 or this blog post.
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?
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.
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:
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:
If anyone is looking for other parallax effects they can check my other repo:
https://github.com/kanytu/android-parallax-listview
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
}
}
})
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);