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.
Related
Background
I work on an app that has a RecyclerView which you can scroll up and down however you wish.
The data items are loaded from the server, so if you are about to reach the bottom or the top, the app gets new data to show there.
To avoid weird scrolling behavior, and staying on the current item, I use 'DiffUtil.Callback' , overriding 'getOldListSize', 'getNewListSize', 'areItemsTheSame', 'areContentsTheSame'.
I've asked about this here, since all I get from the server is a whole new list of items, and not the difference with the previous list.
The problem
The RecyclerView doesn't have only data to show. There are some special items in it too:
Since Internet connection might be slow, there is a header item and a footer item in this RecyclerView, which just have a special Progress view, to show you've reached the edge and that it will get loaded soon.
The header and footer always exist in the list, and they are not received from the server. It's purely a part of the UI, just to show things are about to be loaded.
Thing is, just like the other items, it needs to be handled by DiffUtil.Callback, so for both areItemsTheSame and areContentsTheSame, I just return true if the old header is the new header, and the old footer is the new footer:
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
when {
oldItem.itemType != newItem.itemType -> return false
oldItem.itemType == ItemType.TYPE_FOOTER || oldItem.itemType == AgendaItem.TYPE_HEADER -> return true
...
}
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return when {
oldItem.itemType == ItemType.TYPE_FOOTER || oldItem.itemType == ItemType.TYPE_HEADER -> true
...
}
}
}
Seems right? Well it's wrong. If the user is at the top of the list, showing the header, and the list gets updated with new items, the header will stay at the top, meaning the previous items you've seen will get pushed away by the new ones.
Example:
Before: header, 0, 1, 2, 3, footer
After: header, -3, -2, -1, 0, 1, 2, 3, footer
So if you stayed on the header, and the server sent you the new list, you still see the header, and below the new items, without seeing the old ones. It scrolls for you instead of staying on the same position .
Here's a sketch showing the issue. The black rectangle shows the visible part of the list.
As you can see, before loading, the visible part has the header and some items, and after loading it still has the header and some items, but those are new items that pushed away the old ones.
I need the header to be gone on this case, because the real content is below it. Instead of the area of the header, it might show other items (or a part of them) above it, but the visible position of the current items should stay where they are.
This issue only occurs when the header is shown, at the top of the list. In all other cases it works fine, because only normal items are shown at the top of the visible area.
What I've tried
I tried to find how to set DiffUtil.Callback to ignore some items, but I don't think such a thing exists.
I was thinking of some workarounds, but each has its own disadvantages:
A NestedScrollView (or RecyclerView) which will hold the header&footer and the RecyclerView in the middle, but this might cause some scrolling issues, especially due to the fact I already have a complex layout that depends on the RecyclerView (collapsing of views etc...).
Maybe in the layout of the normal items, I could put the layout of the header and footer too (or just the header, because this one is the problematic one). But this is a bad thing for performance as it inflates extra views for nothing. Plus it requires me to toggle hiding and viewing of the new views within.
I could set a new ID for the header each time there is an update from the server, making it as if the previous header is gone, and there is a totally new header at the top of the new list. However, this might be risky in the case of no real updates of the list at the top, because the header will be shown as if it's removed and then re-added.
The questions
Is there a way to solve this without such workarounds?
Is there a way to tell DiffUtil.Callback : "these items (header&footer) are not real items to scroll to, and these items (the real data items) should be" ?
I will try to explain what I see as a solution to your problem:
Step 1: Remove all the code for FOOTER and HEADER views.
Step 2: Add these methods that add and remove dummy model items in adapter based on the user scroll direction:
/**
* Adds loader item in the adapter based on the given boolean.
*/
public void addLoader(boolean isHeader) {
if (!isLoading()) {
ArrayList<Model> dataList = new ArrayList<>(this.oldDataList);
if(isHeader) {
questions.add(0, getProgressModel());
else {
questions.add(getProgressModel());
setData(dataList);
}
}
/**
* Removes loader item from the UI.
*/
public void removeLoader() {
if (isLoading() && !dataList.isEmpty()) {
ArrayList<Model> dataList = new ArrayList<>(this.oldDataList);
dataList.remove(getDummyModel());
setData(questions);
}
}
public MessageDetail getChatItem() {
return new Model(0, 0, 0, "", "", "")); // Here the first value is id which is set as zero.
}
And here is your rest of the adapter logic that you need to decide if the item is a loader item or an actual data item:
#Override
public int getItemViewType(int position) {
return dataList.get(position).getId() == 0 ? StaticConstants.ItemViewTypes.PROGRESS : StaticConstants.ItemViewTypes.CONTENT;
}
According to the view type, you can add a progress bar view holder in your adapter.
Step 3: use these methods in data loading logic:
While making the API call in onScrolled() method of recyclerView, you need to add a loader item before the api call and then remove it after the api call. Use the given adapter methods above. The coded in onScrolled should look a little like this:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy < 0) { //This is top scroll, so add a loader as the header.
recyclerViewAdapter.addLoader(true);
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (!recyclerViewAdapter.isLoading(true)) {
if (linearLayoutManager.findFirstCompletelyVisibleItemPosition() <= 2) {
callFetchDataApi();
}
}
}
} else {
if (!recyclerViewAdapter.isLoading(false)) {
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() >= linearLayoutManager.getItemCount() - 2) {
callFetchDataApi();
}
}
});
Now after the api call gives you the data you need. Simply remove the added loader from the list like this:
private void onGeneralApiSuccess(ResponseModel responseModel) {
myStreamsDashboardAdapter.removeLoader();
if (responseModel.getStatus().equals(SUCCESS)) {
// Manage your pagination and other data loading logic here.
dataList.addAll(responseModel.getDataList());
recyclerViewAdapter.setData(dataList);
}
}
And lastly, you need to avoid any scroll during data loading operation is add a logic method for that is isLoading() method. which is used in the code of method onScrolled():
public boolean isLoading(boolean isFromHeader) {
if (isFromHeader) {
return dataList.isEmpty() || dataList.get(0).getId() == 0;
} else {
return dataList.isEmpty() || dataList.get(dataList.size() -1).getId() == 0;
}
}
Let me know if you don't understand any of this.
I think for now, the solution I took will suffice. It's a bit weird, but I think it should work:
The header item gets a new id each time the list is different in its first real item. The footer always have the same id, because it's ok for it to move in the current way it works. I don't even need to check that its id is the same. The check of areItemsTheSame is as such for them:
oldItem.agendaItemType == AgendaItem.TYPE_HEADER -> return oldItem.id == newItem.id
oldItem.agendaItemType == AgendaItem.TYPE_FOOTER -> return true
This way, if the header belongs to a new list data, old one will be removed, and new one will be at the top.
It's not the perfect solution, as it doesn't really push the original header to be at the top, and theoretically it makes us "kinda" have 2 headers at the same time (one being removed and one being added) but I think it's good enough.
Also, for some reason, I can't use notifyItemChanged on the header and footer in case only they get updated (internet connection changes its state, so need to change the header&footer alone). Only notifyDataSetChanged works for some reason.
Still, if there is a more official way, could be nice to know.
I can use view actions to perform click on individual list items
onView(withId(R.id.rv_recycler_view))
.perform(actionOnItemAtPosition(0, click()));
However, I want to return the number of list items, and use a loop to click on each list item without writing a separate pice of code for each item.
How can I return the number of list items the recycler view contains? Maybe I need to access the adapter variable through the activity directly?
Thanks
I would stub out the dependency so that you can control your test environment, i.e. If you feed in 3 items, you should expect that three items are displayed. You can check this with the custom action by nenick How to count RecyclerView items with Espresso
public class CustomRecyclerViewAssertions {
//Asserts the number of items in a RecyclerView
public static ViewAssertion AssertItemCount(final int expectedCount) {
return new ViewAssertion() {
#Override
public void check(View view, NoMatchingViewException noViewFoundException) {
if (noViewFoundException != null) {
throw noViewFoundException;
}
RecyclerView recyclerView = (RecyclerView) view;
RecyclerView.Adapter adapter = recyclerView.getAdapter();
assertThat(adapter.getItemCount(), is(expectedCount));
}
};
}
}
which is called by using
onView(withId(R.id.recyclerView)).check(new RecyclerViewItemCountAssertion(3));
Once you know how many items are present, you can use:
onView(withId(R.id.recyclerView)).perform(
actionOnItemAtPosition<RecyclerView.ViewHolder>(1, click()))
As the recyclerView is showing multiple instances of the same view, I wouldn't waste time duplicating code to click each item
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
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.
In my app I am using recycler view.I want to show and hide view on particular condition.But when I scroll recycler views I am not getting expected behaviour.When I Visible a view it gets visible for other rows as well randomly.
What I understand is when it recycles it reuses view and when previous view when it gets recycled it finds the visibility of that view.How can I hide view on particular condition? here is my adapter code
#Override
public void onBindViewHolder(UrduRhymesViewHolder holder, int position) {
RhymesModel current = mUrduRhymesList.get(position);
AppUtility.setCustomFont(mContext, holder.tvUrduRhymesName, Constants.HANDLEE_REGULAR);
holder.tvUrduRhymesName.setText(current.getRhymeName());
holder.ivUrduRhymesLogo.setImageUrl(current.getThumbnailUrl(), mImageRequest);
int status = AppUtility.getFavouriteStatus(mContext, current.getRhymeName(), new UrduRhymesDb(mContext));
if (status == 0)
holder.btnFavourite.setBackgroundResource(R.mipmap.btn_star_unactive);
else
holder.btnFavourite.setBackgroundResource(R.mipmap.btn_star);
ProgressbarDetails progressbarDetails = ProgressbarDetails.getProgressDetail(current.getRhymeName());
if (progressbarDetails == null) {
progressbarDetails = new ProgressbarDetails();
progressbarDetails.prgProgressBar = holder.pbRhymeDownload;
progressbarDetails.download_btn_settings = holder.downloadButtonLayout;
} else {
progressbarDetails.prgProgressBar = holder.pbRhymeDownload;
progressbarDetails.download_btn_settings = holder.downloadButtonLayout;
holder.pbRhymeDownload.setProgress(progressbarDetails.progress);
}
ProgressbarDetails.addUpdateProgressDetail(current.getRhymeName(), progressbarDetails);
if (progressbarDetails != null && progressbarDetails.isDownloading) {
Log.e("test","downloading foe position "+position );
holder.downloadButtonLayout.setBackgroundResource(R.mipmap.btn_download);
holder.pbRhymeDownload.setVisibility(View.VISIBLE);
holder.pbRhymeDownload.setProgress(progressbarDetails.progress);
} else {
Log.e("test","should not be visible for position "+position);
holder.pbRhymeDownload.setVisibility(View.GONE);
}
}
Here progressbarDetails.isDownloading (having value true) is the criteria when I want to show my view but is else clause it is not hiding my view
Edit: Here ProgressbarDetails (Singleton )is a class keeping reference of every row of adapter's progress bar.
No direct way of hiding and unhiding recylerview childitems.
Solution:
Let us assume that the recyclerview adapter is ArrayList
Now make another arraylist (temp_list)
Scenarios:
Hide: iterate through your adapter items and remove the ones that you want to hide. Put each of these into temp_list. After iteration is over, call notifyDataSetChanged()
Show: iterate through your temp_list items and remove the ones that you want to show. Put each of these into adapter. After iteration is over, call notifyDataSetChanged()
You should add a flag in your viewHolder that indicates if this view should be displayed or not . and check this flag every time in the onBindViewHolder.
because the recyclerView reuses the same views you should make a decision depending on something special for every view in you viewHolder.
do u mean when your data has been changed?
and your layout want to change ?
adapter.notifyDataSetChanged();