Android RecyclerView Adapter: notifyItemInserted and notifyItemMoved at index 0 not working - android

I have a RecyclerView with a horizontal linear layout manager declared like this:
RecyclerView graph = (RecyclerView) findViewById(R.id.graph);
RecyclerView.LayoutManager classManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
graph.setLayoutManager(classManager);
graph.addItemDecoration(new ComponentDecorator(this)); //Just sets a margin around each item
I have a method which inserts a placeholder view into the RecyclerView like this:
private void insertPlaceholder(int index) {
int placeholderIndex = getIndexOfPlaceholder(); //returns index of existing placeholder, -1 if none
//No need to do anything
if(placeholderIndex == index)
return;
if(placeholderIndex == -1) {
ClassGraphItem placeholder = new ClassGraphItem();
placeholder.setType(ClassGraphItem.PLACEHOLDER);
mItems.add(index, placeholder);
Print.log("notify item inserted at index", index);
notifyItemInserted(index);
}
else {
ClassGraphItem placeholder = mItems.get(placeholderIndex);
mItems.remove(placeholderIndex);
mItems.add(index, placeholder);
notifyItemMoved(placeholderIndex, index);
}
}
The placeholder is just an invisible view which simulates a space opening between two existing views:
private class PlaceholderViewHolder extends RecyclerView.ViewHolder {
public PlaceholderViewHolder(View itemView) {
super(itemView);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(mComponentWidth, 1);
itemView.setLayoutParams(params);
itemView.setVisibility(View.INVISIBLE);
}
}
When the inserted index is > 0, it works perfectly. However at index 0, either inserting a placeholder, or moving an existing placeholder to the 0 index does not work, specifically the RecyclerView doesn't animate to show the new item inserted at index 0. If I used notifyDataSetChanged() it does work. however that doesn't animate and isn't the effect I'm looking for. This seems like a bug to me, but I wanted to make sure there wasn't something else that was causing this issue.
I'm on the latest version of the recyclerview support library (24.2.1). Thanks!

I removed recycler.setHasFixedSize(true); and now it works. I have no idea why this would be related though.

This may happen because of setting recycler.setHasStableIds(true) and then using item's position to assign the getItemId in the adapter.
This will just update that one position and other items won't be shifted as they have should.

Related

Custom GridLayoutManager

I am thinking of a way to implement a view using RecyclerView like this image where the header row is on the left side and the content under that header on the right. Any ideas or pointers on how to go about this.
If someone else gets here looking for an answer/ solution, here's what I did. I used default GridLayoutManager using child RecyclerView in the parent RecyclerView. Parent RV is using GridLayoutManager while child RV uses LinearLayoutManager. For parent RV, using multiple viewType, position 0 or position %2 == 0 returns HEADER_TYPE while the rest returns CARD_TYPE. Using this
mGridLayoutManager = GridLayoutManager(activity, 3)
mGridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (enquiryAdapter.getItemViewType(position)) {
HEADER_TYPE -> 1
CARD_TYPE -> 2
else -> -1
}
}
}
recyclerView.layoutManager = mGridLayoutManager
I changed the spanSize. For position that returns CARD_TYPE, I inflate another RV which takes list to layout the content view. I am not sure this is the best solution but that seems to be working for me. I will gladly accept any solution working better/simpler than this.

RecyclerView scrolls to bottom when data is loaded

I have some weird issue with RecyclerView since I changed my app to load the data from Room database.
I have a screen (a Fragment) which displays a Profile for a user. Basically it's a GridLayout with the first item (row) displaying some info about the user and then for each row I'm displaying three images.
The code for the RecyclerView is :
private void initializeRecyclerView() {
if (listAdapter == null) {
listAdapter = new PhotosListAdapter(getActivity(), userProfileAccountId, false);
}
rvPhotosList.setNestedScrollingEnabled(true);
rvPhotosList.getItemAnimator().setChangeDuration(0);
GridLayoutManager layoutManager = new GridLayoutManager(getContext(), ProfileFragment.GRID_COLUMNS,
LinearLayoutManager.VERTICAL, false);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
// 3 spans for Header, 1 for photos
return listAdapter.isHeader(position) ? 3 : 1;
}
});
rvPhotosList.setLayoutManager(layoutManager);
int spacingInPixels = 1dp;
rvPhotosList.addItemDecoration(new PaddingItemDecoration(GRID_COLUMNS, spacingInPixels, true));
rvPhotosList.setAdapter(listAdapter);
}
and this is how I load data :
compositeDisposable.add(
repository.getAccount(accountId)
.doOnNext(account -> {
if (account != null && view != null) {
view.showCover(account.getCover());
view.showAccount(account);
}
})
.flatMap(s -> repository.getUserPosts(accountId))
.subscribe(posts -> {
if (view != null) {
view.showPosts(posts);
}
}, throwable -> {}));
Both calls return a Flowable, either a Flowable<Account> or a Flowable<Post> where Account and Post are Room's Entity classes.
The two methods showAccount() and showPosts() pass the previously mentioned entity classes to the adapter.
The problem is this : the moment the data are loaded it scrolls to the bottom of the screen (which is also the bottom of the RecyclerView).
I'm not sure why this happens. Is it because of Room's Flowable type?Cause previously when I was fetching the data from network and passing them to the adapter I didn't have this problem.
Edit : I'm using version 25.3.1 for RecyclerView
Edit 2 : This is how I'm updating my adapter class with Account and Post data :
public void addPhotos(List<Post> posts) {
dataset.addAll(posts);
notifyDataSetChanged();
}
public void addProfileItem(Account account) {
this.account = account;
notifyItemInserted(0);
}
Did you try this? Setting descendantFocusability property of parent RecyclerView to blocksDescendants.
What this will do is, it will no more focus on your dynamically loaded child views inside recycler view, as a result, the automatic scroll will not take place.
android:descendantFocusability="blocksDescendants"
Note: The property descendantFocusability can be used with other views as well like FrameLayout, RelativeLayout, etc. So if you set this property to your parent/root layout, it will no more scroll to bottom.
If you have some initial items, that should be at the bottom after new items loaded, then you need to give them new ids, if you don't want to recycler scrolls to the bottom.
It happens because the recycler remember that this items was visible to the user before update and after update he restore his state so that initial items at the bottom will be displayed
In my case this line was culprit,
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, SPAN_COUNT, GridLayoutManager.VERTICAL, true);
Specifically , last parameter which is setting reverseLayout attribute to true. Make it false and RecyclerView doesn't scroll to bottom of screen.

Android Recycleview- Show first item at bottom and scroll it from bottom to top for next items

I have a recyclerview with several items. I need to show the items starting from bottom i.e. first item should be displayed at bottom and after scrolling from bottom to top, further items will get displayed.
For this what I tried is,
linearlayoutManager.setStackFromEnd(true);
This helped to start the items from bottom. But when there are several items, more than the size to fit into the screen at the same time then the first item goes moving at the top as the item count increases.
I also tried with
linearlayoutManager.scrollToPosition(0);
This didn't change anything.
Here is an image showing what I am looking for.
There are few more items in the list above which are still not visible.
Here how it looks When these items are scrolled,
.
I have checked view hierarchy of this view in the image which only shows a recyclerview and the items within it. And the recyclerview's height is match_parent i.e. the top space with no items showing another view underneath of the recyclerview which is a google map.
I just need to know how I can achieve this feature of the recyclerview without using more extra views.
Any help would be greatly appreciated..!!
Try to init your Linear layout as follow
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, true);
The third boolean indicates that it should reverse the items or not.
But remember that your datas should be sorted from newest to oldest also.
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true);
linearLayoutManager.setStackFromEnd(true);
linearLayoutManager.setReverseLayout(true);
Then once your data is populated
adapter.notifyDataSetChanged();
recyclerView.post(new Runnable() {
#Override
public void run() {
recyclerView.smoothScrollToPosition(0);
}
});
Try using a reversed LinearLayoutManager:
LinearLayoutManager linearlayoutManager = new LinearLayoutManager(context, VERTICAL, true);
write this line in xml to start list from bottom
android:stackFromBottom="true"
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:stackFromBottom="true">
</android.support.v7.widget.RecyclerView>
and second thing reverse your array list before add in adapter
Add these attributes to your recyclerview (XML)
android:paddingTop="500dp"
android:clipToPadding="false"
You can also set it in the code like this:
recyclerView.setPadding(0, 500, 0, 0);
recyclerView.setClipToPadding(false);
This is for different screen sizes
int height = recyclerView.getHeight()
recyclerView.setPadding(0, height - 200, 0, 0);
recyclerView.setClipToPadding(false);
If you don't know how to get screen width, height and convert dp to px
so here are the links: How to get screen width and height and Converting pixels to dp
Seems like what you want is to have a separate view type in your recycler adapter that doesn't show anything but is the height of the map. This would allow you to 'scroll up' from the bottom where the list starts and continue seeing different views.
A simple example:
public class ScreenBottomRecyclerAdapter extends RecyclerView.Adapter {
final int VIEW_TYPE_MAP = 0;
final int VIEW_TYPE_ROW = 1;
#Override
public int getItemCount() {
return dataset.length + 1;
}
#Override
public int getItemViewType(int position) {
if (position == 0) {
return VIEW_TYPE_MAP;
} else {
return VIEW_TYPE_ROW;
}
}
#Override
public RecyclerView.ViewHolder createViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_MAP: return new ViewHolder(null); // This is an empty recycler row so you can see and interact with the map, it will need to be the height of the map so you'll probably want to make a new view type that the same height but doesn't show anything and doesn't intercept touches
case VIEW_TYPE_ROW: return new RowViewHolder(someInflatedView); // This will be the rows you are already creating
}
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case VIEW_TYPE_ROW: holder.update(position - 1) // Do your normal update here, but remember that your data will be off by 1 for the map view row in the recycler
}
}
}
Use below code to reverse list:
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), RecyclerView.VERTICAL, false);
Important: Set layout manager which have property reverse layout to true
layoutManager.setReverseLayout(true);
RecyclerView.setLayoutManager(layoutManager);
This code will display reverse list which you may have assigned to RecyclerView

Sync al horizontal RecyclerView inside vertical list view

Hi I have vertical list (Custom ArrayAdapter) where each element has a horizontal RecyclerView, to give me a table like format.
What I want to do is, if one row is scrolled horizontally, rest rows too should be. I tried putting them just inside horizontal scroll view and various tricks available, but got no luck.
If possible, I would also like this synchronization keeping first element fixed.
Make an instance of RecyclerView.OnScrollListener. This instance will be common to all your horizontal recycler views. When onScrolled( ) gets called, scroll all the views except the one on which this got called.
Adding more to Little Child's answer, it was trickier than expected, I had to create a custom listener that was in my main Activity that was called by onScroll. This is because onScroll was in a getView method and did not get access to other views in listview, but my mainactivty could access them.
In my listadapter, I added
public interface RecycleListener {
public void onScroll(int scroll_x);
}
Then in getView
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
listener.onScroll(recyclerView.computeHorizontalScrollOffset());
}
});
and finally in MainActivity
dayAdapter.setRecycleListener(new DayAdapter.RecycleListener() {
int busy = 0;
#Override
public void onScroll(int scroll_x)
{
if(busy == 1) return;
busy = 1;
for(int i = 0;i<days.size();i++)
{
View view = lv.getChildAt(i);
if(view == null) continue;
RecyclerView r = (RecyclerView) view.findViewById(R.id.timetable_row_recycler);
LinearLayoutManager layoutManager = ( LinearLayoutManager) r.getLayoutManager();
layoutManager.scrollToPositionWithOffset(0,0-scroll_x);
}
busy = 0;
}
});
Note I found out that you can easily get scroll position by using int scroll_x = recyclerView.computeHorizontalScrollOffset() however there is no direct method to appy it back. The method layoutManager.scrollToPosition(i) scrolls to ith element and not the scroll position, so the only correct possible way is
layoutManager.scrollToPositionWithOffset(0,0-scroll_x);
or (in case of custom layoutmanager)
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(0,0-scroll_x);
We basically apply negative offset to the 0th position, so instead of scrolling to offset before 0th, it scrolls to our desired scroll_x.
UPDATE
this worked but caused a lot of lag, so later I ended up switching to android.widget.TableLayout, and inserted rows (android.widget.TableRow) like I was trying with recyclerview.

RecyclerView StaggeredGridLayoutManager reordering issue

I'm trying to display three (at least that's the case I have an issue with) items in a RecyclerView with a StaggeredGridLayoutManager with two columns. The first item is spanned across the two rows. Here's how it looks like:
Now, I'm moving the item "Item 2" to top. Here's the code I call, in the adapter (it's a sample I wrote to demonstrate the issue I have in a more complex project):
private int findById(int id) {
for (int i = 0; i < items.size(); ++i) {
if (items.get(i).title.equals("Item " + id)) {
return i;
}
}
return -1;
}
// Moving the item "Item 2" with id = 2 and position = 0
public void moveItem(int id, int position) {
final int idx = findById(id);
final Item item = items.get(idx);
if (position != idx) {
items.remove(idx);
items.add(position, item);
notifyItemMoved(idx, position);
//notifyDataSetChanged();
}
}
After that, the array is fine: [Item 2, Item 1, Item 3]. However, the view is far from fine:
If I touch the RecyclerView (enough to trigger the overscroll effect if there's not enough items to scroll), Item 2 move to the left, where I expected to see it in the first place (with a nice animation):
As you maybe saw in the code, I tried to replace notifyItemMoved(idx, position) by a call to notifyDataSetChanged(). It works, but the change is not animated.
I wrote a complete sample to demonstrate this and put it on GitHub. It's nearly minimal (there are options to move the item and toggle their spanning).
I don't see what I can be doing wrong. Is this a bug with StaggeredGridLayoutManager? I would like to avoid notifyDataSetChanged() as I would like to keep consistency regarding the animations.
Edit: after some digging, there's no need for a fully-spanned item to show the issue. I removed the full-span. When I try to move Item 2 to position 0, it doesn't move: Item 1 goes after it, and Item 3 is moved on the right, so I have: empty cell, Item 2, new line, Item 1, Item 3. I still have the correct layout after a scroll.
What's more interesting is that I don't have the issue with a GridLayoutManager. I need a full-span item so it's not a solution, but I guess it's indeed a bug in the StaggeredGridLayoutManager…
I don't have a complete answer, but I can point you to both a workaround and the bug report (that I believe is related).
The trick to updating the layout so that it looks like your second screenshot, is to call invalidateSpanAssignments() on the StaggeredGridLayoutManger (sglm) after you've called notifyItemMoved(). The "challenge" is that if you call it immediately after nIM(), it won't run. If you delay the call for a few ms, it will. So, in your referenced code for MainActivity, I've made your sglm a private field:
private StaggeredGridLayoutManager sglm;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
adapter = new Adapter();
recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
sglm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(sglm);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setAdapter(adapter);
}
And down in the switch block, reference it in a handler:
case R.id.move_sec_top:
adapter.moveItem(2, 0);
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
sglm.invalidateSpanAssignments();
}
}, 100);
return true;
The result is that your animation still runs, the layout ends up the way you want it. This is a real kludge, but it does work. I believe this is the same bug that I found and reported at the following link:
https://code.google.com/p/android/issues/detail?id=93156
While my "symptom" and required call were different, the underlying issue seems to be identical.
Good luck!
EDIT: No need to postDelayed, simply posting will do the trick:
case R.id.move_sec_top:
adapter.moveItem(2, 0);
new Handler().post(new Runnable() {
#Override
public void run() {
sglm.invalidateSpanAssignments();
}
});
return true;
My original theory was that the call was blocked until the layout pass was over, but I believe that is not the case. Instead, I now think that if you call invalidateSpanAssignments() immediately, it actually executes too soon (before the layout changes have completed). So, the post above (without delay) simply adds the call to the end of the rendering queue where it happens after the layout.
Well I have done this way.
StaggeredGridLayoutManager gaggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
gaggeredGridLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
recyclerView.setLayoutManager(gaggeredGridLayoutManager);
dataList = YourDataList (Your Code for Arraylist);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerAdapter = new DataAdapter(dataList, recyclerView);
recyclerView.setAdapter(recyclerAdapter);
// Magic line
recyclerView.addOnScrollListener(new ScrollListener());
Create class for Custom RecyclerView Scroll Listener.
private class ScrollListener extends RecyclerView.OnScrollListener {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
gaggeredGridLayoutManager.invalidateSpanAssignments();
}
}
Hope this will help you.

Categories

Resources