Recently I've started working on custom scroll for RecyclerView but I've encountered a problem. I did header parallax and it worked great, but then I wanted to make rows expand when you scroll. For this method I needed to use recyclerView.computeVerticalScrollOffset() it works fine for the first two items in the list but then it falls apart.
Here is an example of my code:
FrameLayout main = (FrameLayout) holder.itemView;
if (main != null) {
int height = calculateHeight(offset, (position + i));
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) main.getLayoutParams();
lp.height = height;
main.setLayoutParams(lp);
}
So I have a function that calculates the height based on the position in layout and offset of the recyclerview. When the second item reaches Y == 0 the offset drops for half (i.e. it drops from 1200 to 600 and then it goes on but the calculations are all wrong)
Any ideas on how to get a stable offset value or how to change it with something that is consistent with recyclerView position.
EDIT:
OK, I can describe the problem step by step:
Element at position(0) of the adapter has a height of 1200
All other elements have the default height of 300
When user starts to scroll down, the element at position(1) starts to expand from 300 to 1200 height
--------- This is where the code starts to crack... -----------
When element at position(1) with the new height of 1200 reaches the top of the screen (when getTop() == 0) offset here is around 900.
When this same element starts to get off the screen (user is scrolling down) the offset at the point where top screen and this element collides (0,0) breaks down by the half, so offset is now 450 when it should be 900, and then it goes from 450 on, but my element at position(1) is now only half the size it should be...
OK I created a workaround for this solution and I am posting this as answer, if someone needs it in the future.
Calculate your own offset:
public int offsetForVerticalScrolling(RecyclerView recyclerView) {
LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager();
int position = llm.findFirstVisibleItemPosition();
ViewHolder mViewHolder = (ViewHolder) recyclerView.findViewHolderForAdapterPosition(position);
View item = mViewHolder.itemView;
int y = (int) item.getY();
if (y < 0) y *= -1;
if (position == 0) return y;
else {
int offset = y;
for (int i = 0; i < position; i++) {
//Add your previous item heights to offset
}
return offset;
}
}
Related
I have a RecyclerView + LinearLayoutManger which is using an adapter that holds chat messages. I limit the number of chat messages to the most 100 recent. This issue is that when I remove the older chats, the scroll position of the chats in the recyclerview changes because index 0 was removed. I began writing the code below:
int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition();
View v = layoutManager.getChildAt(firstVisiblePosition);
if (firstVisiblePosition > 0 && v != null) {
int offsetTop = //need to get the view offset here;
chatAdapter.notifyDataSetChanged();
if (firstVisiblePosition - 1 >= 0 && chatAdapter.getItemCount() > 0) {
layoutManager.scrollToPositionWithOffset(firstVisiblePosition - 1, offsetTop);
}
}
I thought it would be easy to get the visible offset of the first visible item position. Ex. if the first visible view is 300dp but only the last 200dp is visible, I would like to get the 100 offset.
This way I could use scrollToPositionWithOffset(firstVisiblePosition - 1, offsetTop).
Am I missing something here? This seems like it would be an easy problem to figure out, but I haven't seen any methods that would support this.
#Blackbelt. Thank you for getting me on the right track.
The offset that I needed was actually just v.getTop();
My real problem was in getChildAt(). Apparently getChildAt begins at the first visible position, not at the position of the adapter. The documentation is poorly written in this case.
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);
}
}
Here is a picture of what I'm trying to do:
So, I want to resize a single cell while dragging resize anchors (black quads) which are ImageViews. To do this I attached custom onTouchListener to them that does next in MotionEvent.ACTION_MOVE:
calculate drag offset
set cell height/width based on this offset
reposition anchor point by changing it's layout params
The outcome of this is that the cell resizes but there is some king of flicker, more like shaking left/right or up/down, by some small offset.
My guess is that the problem comes when it catches move event, then I manualy change position of anchor and then, when it catches move event again it doesn't handle that change well...or something
I have an idea to put invisible ImageViews under each anchor and do resize based on their movement but do not move that anchor while draging. Then when I relese it, it lines up with coresponding visible anchor. But this is more hacking than solution :)
And finally, does anubody know why is this happening?
EDIT:
Here is the code where I'm handlign move event:
float dragY = event.getRawY() - resizePreviousPositionY;
LinearLayout.LayoutParams componentParams = (LinearLayout.LayoutParams)selectedComponent.layout.getLayoutParams();
LinearLayout parrent = (LinearLayout)selectedComponent.layout.getParent();
View topSibling = parrent.getChildAt(parrent.indexOfChild(selectedComponent.layout) - 1);
LinearLayout.LayoutParams topSiblingParams = (LinearLayout.LayoutParams)topSibling.getLayoutParams();
if(dragY > 0 && selectedComponent.layout.getHeight() > 100 ||
dragY < 0 && topSibling.getHeight() > 100)
{
componentParams.height = selectedComponent.layout.getHeight() - (int)dragY;
topSiblingParams.height = topSibling.getHeight() + (int)dragY;
//bottomSiblingParams.height = bottomSibling.getHeight();
selectedComponent.layout.setLayoutParams(componentParams);
repositionResizeAnchors(selectedComponent);
}
resizePreviousPositionY = event.getRawY();
and here is where I reposition it:
if(((LinearLayout)viewHolder.layout.getParent()).getOrientation() == LinearLayout.VERTICAL)
{
leftMargin = ((LinearLayout)viewHolder.layout.getParent()).getLeft() +
viewHolder.layout.getLeft() + viewHolder.layout.getWidth()/2;
}
else
{
leftMargin = viewHolder.layout.getLeft() + viewHolder.layout.getWidth()/2;
}
topMargin = viewHolder.layout.getTop();
params = (RelativeLayout.LayoutParams)resizeTop.getLayoutParams();
params.leftMargin = leftMargin - dpToPx(resizeAnchorRadius/2);
params.topMargin = topMargin - dpToPx(resizeAnchorRadius/2);
resizeTop.setLayoutParams(params);
I'm doing almost the same as here (https://github.com/kmshack/Android-ParallaxHeaderViewPager), but with RecyclerView in tabs.
RecyclerView in one tab has items with big height and the first one is not always fully visible under header.
At some point I call RecyclerView's computeVerticalScrollOffset() (that calls StaggeredGridLayoutManager's method) and it returns completely wrong values. If I change header height to make first item fully visible I'm getting right values.
Is there any known solution/fix for this?
P.S. I also use LinearLayoutManager and always get right values even if first item is not fully visible under header
I believe the error is with the findFirstVisiblePosition() and findLastVisiblePosition() used in the computeScrollOffset() function arguments. Even if I use almost equal row height computeVerticalScrollOffset() returns really weird numbers.
If we replace them with the recyclerView.getChildAt(0) and recyclerView.getChildAt(recyclerView.getChildCount() - 1) that actually works.
So we can write our own function:
View firstItemView = recyclerView.getChildAt(0);
View lastItemView = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
int firstItem = recyclerView.getChildLayoutPosition(firstItemView);
int lastItem = recyclerView.getChildLayoutPosition(lastItemView);
int itemsBefore = firstItem;
int laidOutArea = recyclerView.getLayoutManager().getDecoratedBottom(lastItemView) - recyclerView.getLayoutManager().getDecoratedTop(firstItemView);
int itemRange = lastItem - firstItem + 1;
float avgSizePerRow = (float) laidOutArea / itemRange;
int offset = (int) (itemsBefore * avgSizePerRow + recyclerView.getLayoutManager().getPaddingTop() - recyclerView.getLayoutManager().getDecoratedTop(firstItemView));
Let's follow the source code of RecyclerView:
#Override
public int computeVerticalScrollOffset() {
return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0;
}
mLayout is an instance of LayoutManager. In our case, it should belong to StaggeredGridLayoutManager. Let's check there:
#Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
return computeScrollOffset(state);
}
then here:
private int computeScrollOffset(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
ensureOrientationHelper();
return ScrollbarHelper.computeScrollOffset(state, mPrimaryOrientation,
findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled, true)
, findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled, true),
this, mSmoothScrollbarEnabled, mShouldReverseLayout);
}
And finally we go to ScrollbarHelper.computeScrollOffset:
/**
* #param startChild View closest to start of the list. (top or left)
* #param endChild View closest to end of the list (bottom or right)
*/
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled, boolean reverseLayout) {
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
endChild == null) {
return 0;
}
final int minPosition = Math.min(lm.getPosition(startChild),
lm.getPosition(endChild));
final int maxPosition = Math.max(lm.getPosition(startChild),
lm.getPosition(endChild));
final int itemsBefore = reverseLayout
? Math.max(0, state.getItemCount() - maxPosition - 1)
: Math.max(0, minPosition);
if (!smoothScrollbarEnabled) {
return itemsBefore;
}
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) -
orientation.getDecoratedStart(startChild));
final int itemRange = Math.abs(lm.getPosition(startChild) -
lm.getPosition(endChild)) + 1;
final float avgSizePerRow = (float) laidOutArea / itemRange;
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
- orientation.getDecoratedStart(startChild)));
}
And we should focus on last statement. This method returns the offset calculated by items * avgHeight, which isn't accurate when our items have variety of height.
As a result, when we use a LinearLayoutManager of GridLayoutManager, since height of each row is confirmed, computeVerticalScrollOffset() will return correct distance. However, unfortunately, when we use StaggeredGridLayoutManager, we cannot get accurate scroll offset by it.
P.S I know the reason why it isn't correct, but I don't know how to get correct scroll distance. If anyone knows, please visit: How to get correct scroll distance of RecyclerView when using a StaggeredGridLayoutManager? and post your answer.
I am trying to build my own grid view functions - extending on the GridView.
The only thing I cannot solve is how to get the current scroll position of the GridView.
getScrollY() does always return 0, and the onScrollListener's parameters are just a range of visible child views, not the actual scroll position.
This does not seem very difficult, but I just can't find a solution in the web.
Anybody here who have an idea?
I did not find any good solution,
but this one is at least able to maintain the scroll position kind of pixel-perfectly:
int offset = (int)(<your vertical spacing in dp> * getResources().getDisplayMetrics().density);
int index = mGrid.getFirstVisiblePosition();
final View first = container.getChildAt(0);
if (null != first) {
offset -= first.getTop();
}
// Destroy the position through rotation or whatever here!
mGrid.setSelection(index);
mGrid.scrollBy(0, offset);
By that you can not get an absolute scroll position, but a visible item + displacement pair.
NOTES:
This is meant for API 8+.
You can get with mGrid.getVerticalSpacing() in API 16+.
You can use mGrid.smoothScrollToPositionFromTop(index, offset) in API 11+ instead of the last two lines.
Hope that helps and gives you an idea.
On Gingerbread, GridView getScrollY() works in some situations, and in some doesn't. Here is an alternative based on the first answer. The row height and the number of columns have to be known (and all rows must have equal height):
public int getGridScrollY()
{
int pos, itemY = 0;
View view;
pos = getFirstVisiblePosition();
view = getChildAt(0);
if(view != null)
itemY = view.getTop();
return YFromPos(pos) - itemY;
}
private int YFromPos(int pos)
{
int row = pos / m_numColumns;
if(pos - row * m_numColumns > 0)
++row;
return row * m_rowHeight;
}
The first answer also gives a good clue on how to pixel-scroll a GridView. Here is a generalized solution, which will scroll a GridView equivalent to scrollTo(0, scrollY):
public void scrollGridToY(int scrollY)
{
int row, off, oldOff, oldY, item;
// calc old offset:
oldY = getScrollY(); // getGridScrollY() will not work here
row = oldY / m_rowHeight;
oldOff = oldY - row * m_rowHeight;
// calc new offset and item:
row = scrollY / m_rowHeight;
off = scrollY - row * m_rowHeight;
item = row * m_numColumns;
setSelection(item);
scrollBy(0, off - oldOff);
}
The functions are implemented inside a subclassed GridView, but they can be easily recoded as external.
I am trying to build my own grid view functions - extending on the GridView.
The only thing I cannot solve is how to get the current scroll position of the GridView.
getScrollY() does always return 0, and the onScrollListener's parameters are just a range of visible child views, not the actual scroll position.
This does not seem very difficult, but I just can't find a solution in the web.
Anybody here who have an idea?
I did not find any good solution,
but this one is at least able to maintain the scroll position kind of pixel-perfectly:
int offset = (int)(<your vertical spacing in dp> * getResources().getDisplayMetrics().density);
int index = mGrid.getFirstVisiblePosition();
final View first = container.getChildAt(0);
if (null != first) {
offset -= first.getTop();
}
// Destroy the position through rotation or whatever here!
mGrid.setSelection(index);
mGrid.scrollBy(0, offset);
By that you can not get an absolute scroll position, but a visible item + displacement pair.
NOTES:
This is meant for API 8+.
You can get with mGrid.getVerticalSpacing() in API 16+.
You can use mGrid.smoothScrollToPositionFromTop(index, offset) in API 11+ instead of the last two lines.
Hope that helps and gives you an idea.
On Gingerbread, GridView getScrollY() works in some situations, and in some doesn't. Here is an alternative based on the first answer. The row height and the number of columns have to be known (and all rows must have equal height):
public int getGridScrollY()
{
int pos, itemY = 0;
View view;
pos = getFirstVisiblePosition();
view = getChildAt(0);
if(view != null)
itemY = view.getTop();
return YFromPos(pos) - itemY;
}
private int YFromPos(int pos)
{
int row = pos / m_numColumns;
if(pos - row * m_numColumns > 0)
++row;
return row * m_rowHeight;
}
The first answer also gives a good clue on how to pixel-scroll a GridView. Here is a generalized solution, which will scroll a GridView equivalent to scrollTo(0, scrollY):
public void scrollGridToY(int scrollY)
{
int row, off, oldOff, oldY, item;
// calc old offset:
oldY = getScrollY(); // getGridScrollY() will not work here
row = oldY / m_rowHeight;
oldOff = oldY - row * m_rowHeight;
// calc new offset and item:
row = scrollY / m_rowHeight;
off = scrollY - row * m_rowHeight;
item = row * m_numColumns;
setSelection(item);
scrollBy(0, off - oldOff);
}
The functions are implemented inside a subclassed GridView, but they can be easily recoded as external.