scrollBy doesn't work properly in nested recyclerview - android

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!!

Related

Find first completely visible element in recyclerview

I have a layout in which it has parallax effect. So this are the elements in it -
AppBarLayout
CollapsingToolbarLayout inside AppBarLayout
Toolbar inside CollapsingToolbarLayout
RecyclerView
All this views are within CoordinatorLayout. Now I require to find out what is the first completely visible item of RecyclerView. Normally I used following logic to get it -
int firstVisibleItem = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
But here I am getting lots of 1 when even 0th position is not completely visible.
getChildAt begins at the first visible position, not at the position of the adapter.
Here is the resulting code.
int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition();
View v = layoutManager.getChildAt(0);
if (firstVisiblePosition > 0 && v != null) {
int offsetTop = v.getTop();
chatAdapter.notifyDataSetChanged();
if (firstVisiblePosition - 1 >= 0 && chatAdapter.getItemCount() > 0) {
layoutManager.scrollToPositionWithOffset(firstVisiblePosition - 1, offsetTop);
}
}
I found it myself. As I am using AppBarLayout, I need to check it out whether particular view is available on screen at that particular scroll or not.
I did is:
#Override
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
View v = recyclerView.getLayoutManager().getChildAt(1);
int offset = 0;
if (v != null) {
offset = v.getTop();
}
if ((verticalOffset * -1) >= offset) {
layoutBuy.setVisibility(View.GONE);
} else {
layoutBuy.setVisibility(View.VISIBLE);
}
}
I used recyclerView.getLayoutManager().getChildAt(1); because I wanted to work around with that particular position which is 1.
Since, vertical offset becomes minus values while scrolling I multiplied it by -1. Then just checked whether offset and top of the view which I am looking for is same or not.
Thus while using parallax effect in a screen and at that same time one needs to check which view is visible in RecyclerView, needs the logic as mentioned above.

How to determine column position in staggered grid layout manager

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...

RecyclerView scrolls to end after smoothScrollToPosition(0)

I have RecyclerView with LinearLayoutManager (Vertical, not inverted). When I insert or move item to 0 position, such method is invoked:
private void scrollToStart() {
int firstVisiblePosition = ((LinearLayoutManager) recentList.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
boolean notScrolling = recentList.getScrollState() == RecyclerView.SCROLL_STATE_IDLE;
if (firstVisiblePosition < 2 && notScrolling) {
recentList.scrollToPosition(0);
}
}
But sometimes it smooth scrolls to the end of list.
(I've seen such behaviour in some apps like Instagram. It looks like list scrolls to top and then starts to move in the opposite direction.)
If I replace smoothScrollToPosition with simple scrollToPosition, it behaves as it should.
How to prevent this scroll?
In my case the fact items are recyclable was the cause to it. Try to place this.setIsRecyclable(false); inside of your ViewHolder

Find out only completely visible list items in android- list view

I want to find out the position or ids related to a ListView's items: only those ones which are completely visible on the screen.
Using listview.getFirstVisibleposition and listview.getLastVisibleposition takes partial list items into account.
I followed a little bit similar approach as suggested by Rich, to suit my requirement which was to fetch completely visible items on screen when List View is scrolled every time.
This is what i did
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//Loop to get tids of all completely visible List View's item scrolled on screen
for (int listItemIndex = 0; listItemIndex <= getListView().getLastVisiblePosition() - getListView().getFirstVisiblePosition(); listItemIndex++) {
View listItem = getListView().getChildAt(listItemIndex);
TextView tvNewPostLabel = (TextView) listItem.findViewById(R.id.tvNewPostLabel);
if (tvNewPostLabel != null && tvNewPostLabel.getVisibility() == View.VISIBLE) {
int listTid = (int) tvNewPostLabel.getTag();
if (listItem.getBottom() < getListView().getHeight()) {//If List View's item is not partially visible
listItemTids.add(listTid);
}
}
}
}
I have not tried this, but here are the pieces of the framework that I believe will get you to what you're looking for (at least this is what I'd try first)
As you've stated, you should get the last visible position from the list view using ListView.getLastVisiblePosition()
You can then access the View representing this position using ListView.getChildAt(position)
You now have a reference to the view, which you can call a combination of View.getLocationOnScreen(location) and View.getHeight()
Also call View.getLocationOnScreen(location) and View.getHeight() on the ListView. y + height of the View should be less than or equal to y + height of the ListView if it is fully visible.

Animation for a GridView

Anybody knwos, how I can animate my GridView after deleting 1 entry? I want something like this:
The first is my normal gridview with 8 items. If I'm deleting one item, I want, that a animate starts and the coloumn of the deleted item will slide up and fill the free place.
Anybody an idea?
Thanks for help :)
One of possible ways to achieve that can be the following set of steps in given order:
1) Save left/top positions of all grid children before deletion. One thing to note here is that the left/top positions should be mapped not to the child position in gridview (which will change after deletion), but to the itemId in adapter which is going to be same for each child even after deletion. To do so, adapter should support stableIds;
2) Delete grid child from adapter and call notifyDataSetChanged();
3) Right after previous step add OnPreDrawListener to the gridview. That is good place to initialize animations, as the layout is ready but not yet drawn to screen. Now we can access all children's coordinates after deletion. On the first onPreDraw() callback, remove listener and calculate diffX and diffY values of each child. And set translationX, translationY values to diffX and diffY and start animating each child to 0 translation value.
Pseudo code might look like this:
// Step 1
saveTopLeftPositions(gridView);
// Step 2
adapter.removeAt(deletedPosition);
adapter.notifyDataSetChanged();
// Step 3
gridView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
gridView.getViewTreeObserver().removeOnPreDrawListener(this);
int firstVisiblePos = grid.getFirstVisiblePosition();
int childCount = grid.getChildCount();
for (int i=0; i<childCount; i++) {
View child = grid.getChildAt(i);
long itemId = adapter.getItemId(i + firstVisiblePos);
float diffX = child.getX() - getOldXPos(itemId);
float diffY = child.getY() - getOldYPos(itemId);
child.setTranslationX(diffX);
child.setTranslationY(diffY);
child.animate().translationX(0).translationY(0);
}
return false;
}
});

Categories

Resources