Endless RecyclerView with ProgressBar for pagination - android

I am using a RecyclerView and fetching objects from an API in batches of ten. For pagination, I use EndlessRecyclerOnScrollListener.
It's all working properly. Now all that's left is to add a progress spinner at the bottom of the list while the next batch of objects is fetched by the API. Here is a screenshot of the Google Play Store app, showing a ProgressBar in what is surely a RecyclerView:
The problem is, neither the RecyclerView nor the EndlessRecyclerOnScrollListener have built-in support for showing a ProgressBar at the bottom while the next batch of objects is being fetched.
I have already seen the following answers:
1. Put an indeterminate ProgressBar as footer in a RecyclerView grid.
2. Adding items to Endless Scroll RecyclerView with ProgressBar at bottom.
I am not satisfied with those answers (both by the same person). This involves shoehorning a null object into the data-set midway while the user is scrolling and then taking it out after the next batch is delivered. It looks like a hack that sidesteps the main problem which may or may not work properly. And it causes a bit of jarring and distortion in the list
Using SwipeRefreshLayout is not a solution here. SwipeRefreshLayout involves pulling from the top to fetch the newest items, and it does not show a progress view anyway.
Can someone please provide a good solution for this? I am interested in knowing how Google has implemented this for their own apps (the Gmail app has it too). Are there any articles where this is shown in detail? All answers & comments will be appreciated. Thank you.
Some other references:
1. Pagination with RecyclerView. (Superb overview ...)
2. RecyclerView header and footer. (More of the same ...)
3. Endless RecyclerView with ProgressBar at bottom.

HERE IS SIMPLER AND CLEANER APPROACH.
Implement Endless Scrolling from this Codepath Guide and then follow the following steps.
1. Add progress bar under the RecyclerView.
<android.support.v7.widget.RecyclerView
android:id="#+id/rv_movie_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:paddingBottom="50dp"
android:clipToPadding="false"
android:background="#android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</android.support.v7.widget.RecyclerView>
<ProgressBar
android:id="#+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
android:background="#android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
Here android:paddingBottom="50dp" and android:clipToPadding="false" are very important.
2. Get a reference to the progress bar.
progressBar = findViewById(R.id.progressBar);
3. Define methods to show and hide progress bar.
void showProgressView() {
progressBar.setVisibility(View.VISIBLE);
}
void hideProgressView() {
progressBar.setVisibility(View.INVISIBLE);
}

I implemented this on my old project, I did it as follows...
I've created an interface as the guys of your examples did
public interface LoadMoreItems {
void LoadItems();
}
Then I add added an addOnScrollListener() on my Adapter
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView,
int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
totalItemCount = linearLayoutManager.getItemCount();
lastVisibleItem = linearLayoutManager
.findLastVisibleItemPosition();
if (!loading
&& totalItemCount <= (lastVisibleItem + visibleThreshold)) {
//End of the items
if (onLoadMoreListener != null) {
onLoadMoreListener.LoadItems();
}
loading = true;
}
}
});
The onCreateViewHolder() is where I put the ProgressBar or not.
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
int viewType) {
RecyclerView.ViewHolder vh;
if (viewType == VIEW_ITEM) {
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.list_row, parent, false);
vh = new StudentViewHolder(v);
} else {
View v = LayoutInflater.from(parent.getContext()).inflate(
R.layout.progressbar_item, parent, false);
vh = new ProgressViewHolder(v);
}
return vh;
}
On my MainActivity that is where I put the LoadItems() to add the others items is :
mAdapter.setOnLoadMoreListener(new LoadMoreItems() {
#Override
public void LoadItems() {
DataItemsList.add(null);
mAdapter.notifyItemInserted(DataItemsList.size() - 1);
handler.postDelayed(new Runnable() {
#Override
public void run() {
// remove progress item
DataItemsList.remove(DataItemsList.size() - 1);
mAdapter.notifyItemRemoved(DataItemsList.size());
//add items one by one
//When you've added the items call the setLoaded()
mAdapter.setLoaded();
//if you put all of the items at once call
// mAdapter.notifyDataSetChanged();
}
}, 2000); //time 2 seconds
}
});
For more information I just followed this Github repository(Note: this is using AsyncTask maybe it's useful as my answer, since I did it manually not with data from API but it should work as well) also this post was helpful to me endless-recyclerview-with-progress-bar
Also I don't know if you named it but I also found this post infinite_scrolling_recyclerview, maybe it could also help to you.
If it's not what you are looking for, let me know and tell me what's wrong with this code and I'll try to modify it as your like.
Hope it helps.
EDIT
Since you don't want to remove an item... I found I guess one guy that removes the footer only on this post : diseño-android-endless-recyclerview.
This is for ListView but I know you can adapt it to RecyclerView he's not deleting any item he's just putting Visible/Invisible the ProgressBar take a look : detecting-end-of-listview
Also take a look to : this question android-implementing-progressbar-and-loading-for-endless-list-like-android

There is another way to do this.
First your adapter's getItemCount returns listItems.size() + 1
return VIEW_TYPE_LOADING in getItemViewType() for position >= listItems.size(). This way the loader will only be shown at the end of the recycler view list. The only problem with this solution is even after reaching the last page, the loader will be shown, so in order to fix that you store the x-pagination-total-count in the adapter, and
then you change the condition to return view type to
(position >= listItem.size())&&(listItem.size <= xPaginationTotalCount) .
I just came up with this idea now what do you think?

here is my workaround without adding a fake item (in Kotlin but simple):
in your adapter add:
private var isLoading = false
private val VIEWTYPE_FORECAST = 1
private val VIEWTYPE_PROGRESS = 2
override fun getItemCount(): Int {
if (isLoading)
return items.size + 1
else
return items.size
}
override fun getItemViewType(position: Int): Int {
if (position == items.size - 1 && isLoading)
return VIEWTYPE_PROGRESS
else
return VIEWTYPE_FORECAST
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
if (viewType == VIEWTYPE_FORECAST)
return ForecastHolder(LayoutInflater.from(context).inflate(R.layout.item_forecast, parent, false))
else
return ProgressHolder(LayoutInflater.from(context).inflate(R.layout.item_progress, parent, false))
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
if (holder is ForecastHolder) {
//init your item
}
}
public fun showProgress() {
isLoading = true
}
public fun hideProgress() {
isLoading = false
}
now you can easily call showProgress() before API call. and hideProgress() after API call was done.

I like the idea of adding a progress view holder to an adapter, but it tends to lead to some ugly logic manipulation to get what you want. The view holder approach forces you to guard against the additional footer item by fidgeting with the return values of getItemCount(), getItemViewType(), getItemId(position) and any kind of getItem(position) method that you may want to include.
An alternative approach is to manage the ProgressBar visibility at the Fragment or Activity level by showing or hiding the ProgressBar below the RecyclerView when loading starts and ends respectively. This can be achieved by including the ProgressBar directly in the view layout or by adding it to a custom RecyclerView ViewGroup class. This solution will generally lead to less maintenance and fewer bugs.
UPDATE: My suggestion poses a problem when you scroll the view back up while the content is loading. The ProgressBar will stick to the bottom of the view layout. This is probably not the behavior you want. For this reason, adding a progress view holder to your adapter is probably the best, functional solution. Just don't forget to guard your item accessor methods. :)

Another possible solution is to use the ConcatAdapter available from RecyclerView 1.2.0. The drawback is that this library version is yet in alpha.
Using this approach, separate adapter is used for progress indicator, concatenated with the main adapter.
val concatAdapter = ConcatAdapter(dataAdapter, progressAdapter)
progressAdapter should return 0 or 1 from getItemCount() method, depending on the loading state.
More info: https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a
And check the current stable version of recyclerview library, might be already in a stable version at the time of reading: https://developer.android.com/jetpack/androidx/releases/recyclerview
Another viable approach would be to use recycler view item decorations. Using this approach would also save from modifying the ViewHolders.
Animating the decorator is also possible, see for example: https://medium.com/mobile-app-development-publication/animating-recycler-view-decorator-9b15fa4b2c23
Basically, item decorator is added when loading indicator should be present with recyclerView.addItemDecoration() function, and then removed with recyclerView.removeItemDecoration(). Constantly invalidateItemDecorations() on the recyclerView while item decoration is shown to make animation run.
A third possibility would be to use Paging library from Google (part of Jetpack), header and footer adapters available from v3 (still in beta)
https://www.youtube.com/watch?v=1cwqGOku2a4

This solution is inspired by Akshar Patels solution on this page. I modified it a bit.
When loading the first items it looks nice to have the ProgressBar centered.
I didn't like the remaining empty padding at the bottom when there existed no more items to load. That has been removed with this solution.
First the XML:
<android.support.v7.widget.RecyclerView
android:id="#+id/video_list"
android:paddingBottom="60dp"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</android.support.v7.widget.RecyclerView>
<ProgressBar
android:id="#+id/progressBar2"
style="?android:attr/progressBarStyle"
android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_centerHorizontal="true"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
Then I added the following programmatically.
When first results been loaded, add this to your onScrollListener. It moves the ProgressBar from center to the bottom:
ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) loadingVideos.getLayoutParams();
layoutParams.topToTop = ConstraintLayout.LayoutParams.UNSET;
loadingVideos.setLayoutParams(layoutParams);
When no more items exist, remove the padding at the bottom like this:
recyclerView.setPadding(0,0,0,0);
Hide and show your ProgressBar as usual.

Try this simple code :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerview_cities"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#id/preogressbar"
/>
<ProgressBar
android:id="#+id/preogressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
/>
</RelativeLayout>
Make the progress bar visible when your list items already scrolled and hide when you get data from your service.

You can use layout_above tag in recycleView like this:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv"
android:layout_below="#+id/tv2"
android:layout_width="match_parent"
android:focusable="false"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_above="#+id/pb_pagination"/>
<ProgressBar
android:id="#+id/pb_pagination"
style="#style/Widget.AppCompat.ProgressBar"
android:layout_width="30dp"
android:layout_height="30dp"
android:indeterminate="true"
android:visibility="gone"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>

Add in Adapter
Insert a new item to the RecyclerView on a predefined position
public void insert(int position, JobModel data) {
joblist.add(position, data);
notifyItemInserted(position);
}.
public void updateList( ArrayList<JobModel> data) {
try {
for (int i = joblist.size(); i < data.size(); i++)
insert(i, data.get(i));
}catch (Exception e){} }.
call from activity when page==2
apiSaveJobsAdapter.updateList(joblist);

Different approach would be to start the API call inside onBindViewHolder and initialy place into items view some progress indicator. After call is finished, you update the view (hide progress and showing received data). For example with Picasso for image loading, onBindViewHolder method would look like this
#Override
public void onBindViewHolder(final MovieViewHolder holder, final int position) {
final Movie movie = items.get(position);
holder.imageProgress.setVisibility(View.VISIBLE);
Picasso.with(context)
.load(NetworkingUtils.getMovieImageUrl(movie.getPosterPath()))
.into(holder.movieThumbImage, new Callback() {
#Override
public void onSuccess() {
holder.imageProgress.setVisibility(View.GONE);
}
#Override
public void onError() {
}
});
}
As I see it, there are two cases which can appear:
where you download all items in light version with one call (e.g. the adapter knows immediately that he’ll have to deal with 40 pictures, but downloads it on demand —> case which I showed previously with Picasso)
where you are working with real lazy loading and you are asking server to give you additional chunk of data. In this case, first prerequisite is to have adequate response from server with necessary information. Fore example
{
"offset": 0,
"total": 100,
"items": [{items}]
}
There response means that you received first chunk of total 100 data. My approach would be something like this:
View
After getting first chunk of data (e.g. 10) add them into adapter.
RecyclerView.Adapter.getItemCount
As long as the current amount of available items is lower than total amount (e.g. available 10; total 100), in getItemCount method you will return items.size() + 1
RecyclerView.Adapter.getItemViewType
if total amount of data is greater than amount of available items in adapter and the position = items.size() (i.e. you’ve fictively added item in getItemCount method), as view type you return some progress-indicator. Otherwise you’ll return normal layout type
RecyclerView.Adapter.onCreateViewHolder
When you are asked to use progress-indicator view type, all you need to do is to ask your presenter to get additional chunk of items and update the adapter
So basically, this is approach where you don’t have to add/remove items from the list and where you have control over situation when lazy loading will be triggered.
Here is the code example:
public class ForecastListAdapter extends RecyclerView.Adapter<ForecastListAdapter.ForecastVH> {
private final Context context;
private List<Forecast> items;
private ILazyLoading lazyLoadingListener;
public static final int VIEW_TYPE_FIRST = 0;
public static final int VIEW_TYPE_REST = 1;
public static final int VIEW_TYPE_PROGRESS = 2;
public static final int totalItemsCount = 14;
public ForecastListAdapter(List<Forecast> items, Context context, ILazyLoading lazyLoadingListener) {
this.items = items;
this.context = context;
this.lazyLoadingListener = lazyLoadingListener;
}
public void addItems(List<Forecast> additionalItems){
this.items.addAll(additionalItems);
notifyDataSetChanged();
}
#Override
public int getItemViewType(int position) {
if(totalItemsCount > items.size() && position == items.size()){
return VIEW_TYPE_PROGRESS;
}
switch (position){
case VIEW_TYPE_FIRST:
return VIEW_TYPE_FIRST;
default:
return VIEW_TYPE_REST;
}
}
#Override
public ForecastVH onCreateViewHolder(ViewGroup parent, int viewType) {
View v;
switch (viewType){
case VIEW_TYPE_PROGRESS:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_progress, parent, false);
if (lazyLoadingListener != null) {
lazyLoadingListener.getAdditionalItems();
}
break;
case VIEW_TYPE_FIRST:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_first, parent, false);
break;
default:
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_rest, parent, false);
break;
}
return new ForecastVH(v);
}
#Override
public void onBindViewHolder(ForecastVH holder, int position) {
if(position < items.size()){
Forecast item = items.get(position);
holder.date.setText(FormattingUtils.formatTimeStamp(item.getDt()));
holder.minTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMin()));
holder.maxTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMax()));
}
}
#Override
public long getItemId(int position) {
long i = super.getItemId(position);
return i;
}
#Override
public int getItemCount() {
if (items == null) {
return 0;
}
if(items.size() < totalItemsCount){
return items.size() + 1;
}else{
return items.size();
}
}
public class ForecastVH extends RecyclerView.ViewHolder{
#BindView(R.id.forecast_date)TextView date;
#BindView(R.id.min_temperature)TextView minTemperature;
#BindView(R.id.max_temperature) TextView maxTemperature;
public ForecastVH(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
public interface ILazyLoading{
public void getAdditionalItems();
}}
Maybe this'll inspire you to make something that will suit your needs

Related

Layout behaving weird in RecyclerView

I am trying to achieve a chatting app type layout in RecylcerView. Now the only problem is that even though I have set width of View to wrap around text. Inside RecyclerView sometime small words take whole line and sometime the word is fine.
Green highlight is expected behavior and red is unexpected behavior.
This is the code of cardView
<androidx.cardview.widget.CardView
android:id="#+id/cardView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
app:cardBackgroundColor="#00BCD4"
app:cardCornerRadius="12dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.123">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/textMessageSent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxWidth="260dp"
android:paddingBottom="8dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="8dp"
android:text="oh noo"
android:textColor="#000000"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:maxLines="10" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
Here is Adapter code
public class ChatAdapter extends ListAdapter {
String reciever,sender;
public ChatAdapter(String from,String to)
{
super(DIFF_CALLBACK);
this.reciever = from;
this.sender = to;
}
private static final DiffUtil.ItemCallback<Chat> DIFF_CALLBACK = new DiffUtil.ItemCallback<Chat>() {
#Override
public boolean areItemsTheSame(#NonNull Chat oldItem, #NonNull Chat newItem) {
return oldItem.getId() == newItem.getId();
}
#Override
public boolean areContentsTheSame(#NonNull Chat oldItem, #NonNull Chat newItem) {
return oldItem.getSender().equals(newItem.getSender()) &&
oldItem.getReciever().equals(newItem.getReciever()) &&
oldItem.getDate().equals(newItem.getDate()) &&
oldItem.getMessageIdSent() == newItem.getMessageIdSent() &&
oldItem.getMessageRecieved().equals(newItem.getMessageRecieved()) &&
oldItem.getTime().equals(newItem.getTime());
}
};
private final int TEXT_MESSAGE_SENT = 0;
private final int TEXT_MESSAGE_RECIEVED = 1;
#Override
public int getItemViewType(int position) {
Chat currentChats = (Chat) getItem(position);
if(!currentChats.getMessageRecieved().equals(""))
{
return TEXT_MESSAGE_RECIEVED;
}
else if(!currentChats.getMessageSent().equals(""))
{
return TEXT_MESSAGE_SENT;
}
return -1;
}
#NonNull
#Override
public RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view;
if(viewType == TEXT_MESSAGE_SENT)
{
view = layoutInflater.inflate(R.layout.me_text_message,parent,false);
return new TextChatSenderViewHolder(view);
}
//if reciever is text
view = layoutInflater.inflate(R.layout.other_text_message,parent,false);
return new TextChatRecieverViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
Chat currentChat = (Chat) getItem(position);
switch (holder.getItemViewType())
{
case TEXT_MESSAGE_RECIEVED:
//bind REcievedText viewholder
TextChatRecieverViewHolder viewHolder = (TextChatRecieverViewHolder) holder;
if(!currentChat.getMessageRecieved().equals(""))
{
viewHolder.textMessageRecieved.setText(currentChat.getMessageRecieved());
viewHolder.textTimeMessageRecieved.setText(currentChat.getTime());
}
break;
case TEXT_MESSAGE_SENT:
TextChatSenderViewHolder viewHolder2 = (TextChatSenderViewHolder) holder;
if(!currentChat.getMessageSent().equals("")) {
viewHolder2.textMessageSent.setText(currentChat.getMessageSent());
viewHolder2.textTimeMessageSent.setText(currentChat.getTime());
}
break;
}
}
class TextChatSenderViewHolder extends RecyclerView.ViewHolder
{
TextView textMessageSent,textTimeMessageSent;
public TextChatSenderViewHolder(#NonNull View itemView) {
super(itemView);
textTimeMessageSent = itemView.findViewById(R.id.textTimeMessageSent);
textMessageSent = itemView.findViewById(R.id.textMessageSent);
}
}
class TextChatRecieverViewHolder extends RecyclerView.ViewHolder
{
TextView textMessageRecieved,textTimeMessageRecieved;
public TextChatRecieverViewHolder(#NonNull View itemView) {
super(itemView);
textTimeMessageRecieved = itemView.findViewById(R.id.textTimeMessageRecieved);
textMessageRecieved = itemView.findViewById(R.id.textMessageRecieved);
}
}
}
NOTE: I have noticed that layout changes when the message goes out of screen and you scroll back up.
You need to change your onCreateViewHolder, keep below lines in else
view = layoutInflater.inflate(R.layout.other_text_message,parent,false);
return new TextChatRecieverViewHolder(view);
Also keep layout width to
wrap_content
rather than
match_parent.
you've messed up recycling pattern (link for ListView but it describes pattern better than official doc imho) by overriding getItemViewType method and letting this method return -1. thats not any of view types, but onCreateViewHolder MUST return ViewHolder, so you are returning R.layout.other_text_message. if any message would have 0-length then this empty layout will show up on list
another problem related to wrong recycling implementation is that you are not always calling setText inside onBindViewHolder, because of inconsistent view types and if check (probably unnecessary, as itemType is then -1). when you understand how recycling works (above linked) you will notice that when you won't ALWAYS call setText in onBindViewHolder then recycled item may contain text from previous iteration (before scroll) and this duplicated text will show at this position (wrongly recycler view)
now your list item view have android:layout_width="match_parent" which may "remember" length before recycling, showing set after recycling, something is messing up in here. if you want text wrapping then TextView should have set android:layout_width="wrap_content". keeping android:layout_width="match_parent" (ContraintLayout) inside android:layout_width="wrap_content" parent (CardView) is very unefficient and may cause multiple redrawings/remeasurements, parent probably should also have android:layout_width="match_parent"
my advise to you is to getting rid of empty messages BEFORE setting them to adapter - drawing mechanism isn't a place for filtering inproper items (empty messages in your case) not intended to be drawn. return always some known view type and always fully handle it (onCreate... and onBind... methods), use at least else setText(""); for clearing text after previous item/text set for this view

Inserting RecyclerView items at zero position - always stay scrolled to top

I have a pretty standard RecyclerView with a vertical LinearLayoutManager. I keep inserting new items at the top and I'm calling notifyItemInserted(0).
I want the list to stay scrolled to the top; to always display the 0th position.
From my requirement's point of view, the LayoutManager behaves differently based on the number of items.
While all items fit on the screen, it looks and behaves as I expect: The new item always appears on top and shifts everything below it.
However, as soon as the no. of items exceeds the RecyclerView's bounds, new items are added above the currently visible one, but the visible items stay in view. The user has to scroll to see the newest item.
This behavior is totally understandable and fine for many applications, but not for a "live feed", where seeing the most recent thing is more important than "not distracting" the user with auto-scrolls.
I know this question is almost a duplicate of Adding new item to the top of the RecyclerView... but all of the proposed answers are mere workarounds (most of them quite good, admittedly).
I'm looking for a way to actually change this behavior. I want the LayoutManager to act exactly the same, no matter the number of items. I want it to always shift all of the items (just like it does for the first few additions), not to stop shifting items at some point, and compensate by smooth-scrolling the list to the top.
Basically, no smoothScrollToPosition, no RecyclerView.SmoothScroller. Subclassing LinearLayoutManager is fine. I'm already digging through its code, but without any luck so far, so I decided to ask in case someone already dealt with this. Thanks for any ideas!
EDIT: To clarify why I'm dismissing answers from the linked question: Mostly I'm concerned about animation smoothness.
Notice in the first GIF where ItemAnimator is moving other items while adding the new one, both fade-in and move animations have the same duration. But when I'm "moving" the items by smooth scrolling, I cannot easily control the speed of the scroll. Even with default ItemAnimator durations, this doesn't look as good, but in my particular case, I even needed to slow down the ItemAnimator durations, which makes it even worse:
Although I wrote this answer and this is the accepted solution, I suggest a look at the other later answers to see if they work for you before attempting this.
When an item is added to the top of the RecyclerView and the item can fit onto the screen, the item is attached to a view holder and RecyclerView undergoes an animation phase to move items down to display the new item at the top.
If the new item cannot be displayed without scrolling, a view holder is not created so there is nothing to animate. The only way to get the new item onto the screen when this happens is to scroll which causes the view holder to be created so the view can be laid out on the screen. (There does seem to be an edge case where the view is partially displayed and a view holder is created, but I will ignore this particular instance since it is not germane.)
So, the issue is that two different actions, animation of an added view and scrolling of an added view, must be made to look the same to the user. We could dive into the underlying code and figure out exactly what is going on in terms of view holder creation, animation timing, etc. But, even if we can duplicate the actions, it can break if the underlying code changes. This is what you are resisting.
An alternative is to add a header at position zero of the RecyclerView. You will always see the animation when this header is displayed and new items are added to position 1. If you don't want a header, you can make it zero height and it will not display. The following video shows this technique:
This is the code for the demo. It simply adds a dummy entry at position 0 of the items. If a dummy entry is not to your liking, there are other ways to approach this. You can search for ways to add headers to RecyclerView.
(If you do use a scrollbar, it will misbehave as you can probably tell from the demo. To fix this 100%, you will have to take over a lot of the scrollbar height and placement computation. The custom computeVerticalScrollOffset() for the LinearLayoutManager takes care of placing the scrollbar at the top when appropriate. (Code was introduced after video taken.) The scrollbar, however, jumps when scrolling down. A better placement computation would take care of this problem. See this Stack Overflow question for more information on scrollbars in the context of varying height items.)
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private TheAdapter mAdapter;
private final ArrayList<String> mItems = new ArrayList<>();
private int mItemCount = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
LinearLayoutManager layoutManager =
new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) {
#Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
if (findFirstCompletelyVisibleItemPosition() == 0) {
// Force scrollbar to top of range. When scrolling down, the scrollbar
// will jump since RecyclerView seems to assume the same height for
// all items.
return 0;
} else {
return super.computeVerticalScrollOffset(state);
}
}
};
recyclerView.setLayoutManager(layoutManager);
for (mItemCount = 0; mItemCount < 6; mItemCount++) {
mItems.add(0, "Item # " + mItemCount);
}
// Create a dummy entry that is just a placeholder.
mItems.add(0, "Dummy item that won't display");
mAdapter = new TheAdapter(mItems);
recyclerView.setAdapter(mAdapter);
}
#Override
public void onClick(View view) {
// Always at to position #1 to let animation occur.
mItems.add(1, "Item # " + mItemCount++);
mAdapter.notifyItemInserted(1);
}
}
TheAdapter.java
class TheAdapter extends RecyclerView.Adapter<TheAdapter.ItemHolder> {
private ArrayList<String> mData;
public TheAdapter(ArrayList<String> data) {
mData = data;
}
#Override
public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
if (viewType == 0) {
// Create a zero-height view that will sit at the top of the RecyclerView to force
// animations when items are added below it.
view = new Space(parent.getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0));
} else {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item, parent, false);
}
return new ItemHolder(view);
}
#Override
public void onBindViewHolder(final ItemHolder holder, int position) {
if (position == 0) {
return;
}
holder.mTextView.setText(mData.get(position));
}
#Override
public int getItemViewType(int position) {
return (position == 0) ? 0 : 1;
}
#Override
public int getItemCount() {
return mData.size();
}
public static class ItemHolder extends RecyclerView.ViewHolder {
private TextView mTextView;
public ItemHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.textView);
}
}
}
activity_main.xml
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toTopOf="#+id/button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:text="Button"
android:onClick="onClick"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>
list_item.xml
<LinearLayout
android:id="#+id/list_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<View
android:id="#+id/box"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:background="#android:color/holo_green_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#id/box"
app:layout_constraintTop_toTopOf="parent"
tools:text="TextView" />
</LinearLayout>
I'm also displaying a live feed of items, and when a new item is added, it's before the first item. But in the recycler view, I need to scroll up to see the new item.
To solve the problem, add to the Adapter a RecyclerView.AdapterDataObserver() and override the onItemRangeInserted(). When new data is added, check if the data is on at position 0, and the recycler view was on top (You don't want to autoscroll to first position if you were scrolling in the list).
Exemple :
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
#Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if (positionStart == 0 && positionStart == layoutManager.findFirstCompletelyVisibleItemPosition()) {
layoutManager.scrollToPosition(0);
}
}
});
This solution worked fine for me, with different type of Adapter, like ListAdapter and PagedListAdapter.
I firstly wanted to use a similar implementation of the accepted solution, where you add a dumb first item, but in PagedListAdapter it's impossible as list are immutable.
This worked for me:
val atTop = !recycler.canScrollVertically(-1)
adapter.addToFront(item)
adapter.notifyItemInserted(0)
if (atTop) {
recycler.scrollToPosition(0)
}
The only solution that worked for me was to reverse the recycler's layout by calling setReverseLayout() and setStackFromEnd() on its LinearLayoutManager.
This might sound stupid, but the way RecyclerView handles adding items at the end of the list is what you need at the top. The only downsize of this is that you'd have to reverse your list and start adding items to the end instead.
Use adapter.notifyDataSetChanged() instead of adater.notifyItemInserted(0). This will scroll recylerView to zero position if current scroll position is one(old zero).

RecyclerView inside NestedScrollView causes RecyclerView to inflate all elements

I'm having an issue with placing a RecyclerView inside a NestedScrollView, which causes ALL elements of the RecyclerView's adapter to be rendered.
This is a rather large issue, as the lists that the RecyclerView is showing can contain several hundred elements.
This is at the moment causing quite a lot of lag (obviously) as it has to render all views at once, and can't reuse any already inflated views as the RecyclerView normally does.
This is my current XML (Removed some bloat to minimize it):
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="90dp">
<!-- Some content -->
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Some more content -->
</LinearLayout>
<!-- Product list -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="never"/>
</LinearLayout>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
This is my onCreateView() from the Fragment that is inflating the view containing the NestedScrollView and RecyclerView:
#Override
public View onCreateView(LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.category_content_fragment, container, false);
ButterKnife.bind(this, root);
List<Product> products = new ArrayList<>(); //This is populated by other means, not relevant to the issue
productsRecyclerView.setNestedScrollingEnabled(false);
productsRecyclerView.setHasFixedSize(true);
productsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
ProductsContentAdapter productsContentAdapter = new ProductsContentAdapter(products);
productsRecyclerView.setAdapter(productsContentAdapter);
return root;
}
I have seen this post about the issue:
How to put RecyclerView inside NestedScrollView?
But it doesn't mention a final fix to the issue sadly.
To clarify:
The RecyclerView scrolls perfectly, it shows at the correct time, but the issue is that it renders ALL of its children instantly, meaning possible several hundreds of elements, even though the screen only shows 5-6 at a time at max.
Please feel free to ask questions if more information is needed.
------- EDIT -------
After many failed attempts of other solutions, i ended up using Jeeva Nandhan's solution.
Prior to asking this question i knew that was a possible solution, but i had 11 different possible views that needed to fit into the RecyclerView, so i would've liked to avoid it.
After using different ViewTypes, it worked perfectly. I was afraid it would be very inefficient due to the high amount of ViewTypes, but it's buttery smooth.
I too have come across this issue... This is because both scrollview and RecyclerView are different in loading data, since the ScrollView acts as the parent in this case and we are using the below line in our code.
setNestedScrollingEnabled(false);
This will make the scroll slow and hang issue based on the Recyclerview data.
One way which I have used to solve this issue is adding header to the Recyclerview..
Here I'll explain it clearly.
lets assume this recyclerview is in our activity.
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
The adapter class will be like this, where we will add the header
public class SampleAdapter extends RecyclerView.Adapter {
private final int BODY = 1;
private final int HEADER = 2;
private List<String> data = null;
SampleAdapter(List<String> data) {
this.data = data;
}
#Override
public int getItemViewType(int position) {
if (position == 0) {
return HEADER;
}
return BODY;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
switch (viewType) {
case HEADER:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_header_layout, parent, false);
return new HeaderViewHolder(view);
default:
//Setting the Body view...
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_details, parent, false);
return new BodyViewHolder(view);
}
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof BodyViewHolder) {
//Set the body content....
if (data != null && data.size() > 0) {
/** Since we have added one cell for header,
* we need to decrement the position and render the body view.
*
*/
int bodyPosition = position - 1;
}
} else if (holder instanceof HeaderViewHolder) {
//Set the header content...
}
}
#Override
public int getItemCount() {
//Sice we are going to add header, we are supposed increase the count by one...
return data.size() + 1;
}
}
by this there is no need for NestedScrollView and all the view will work in RecyclerView behavior...
Hope this is helpful :)
If you have large amount of data to display,show only some numbers of data first time than on scroll use loadMoreListener to get next data.

RecyclerView add EmptyView

I'm trying to implement an EmptyView on my RecyclerView Adapter but I'm not getting any result.
I've followed this tutorial and this tip, but noone worked for me.
I've implemented:
if (viewType == EMPTY_VIEW) {
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.empty_view, parent, false);
EmptyViewHolder evh = new EmptyViewHolder(v);
return evh;
}
v = LayoutInflater.from(parent.getContext()).inflate(R.layout.data_row, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
But it doesn't let me compile because they are differents ViewHolder, because I've created two ViewHolder classes but they extends Recycler.ViewHolder so I don't get it...
I'm trying to do this because I've got a SearchView and I want when the list is empty it shows an EmptyView, I've got it doing it programmatically but I prefer to add like a layout because I don't know that much how to put TextViews and Buttons programmatically.
Also if I put
return dataList.size() > 0 ? dataList.size() : 1;
It gives to me error because index is 0.
I've debugged the viewType and always is 1, then it won't join the if condition...
Deep on Android I found this :
/**
* Return the view type of the item at <code>position</code> for the purposes
* of view recycling.
*
* <p>The default implementation of this method returns 0, making the assumption of
* a single view type for the adapter. Unlike ListView adapters, types need not
* be contiguous. Consider using id resources to uniquely identify item view types.
*
* #param position position to query
* #return integer value identifying the type of the view needed to represent the item at
* <code>position</code>. Type codes need not be contiguous.
*/
public int getItemViewType(int position) {
return 0;
}
But the thing is that no changes the value.
EDIT
I almost done it, I did this :
#Override
public int getItemViewType(int position) {
return list.size() > 0 ? list.size() : 1;
}
But sometimes it returns 0 when the size() is 0... I don't get it, I'm using this SearchView, and sometimes when I type a letter that doesn't matches with any item of the list it doesn't show and sometimes it does...
Also other thing that happens is that when the layout popups it shows on the left of the screen when I put that is on center, but I think it's problem with RecyclerView because the layout puts inside of it.
RecyclerView layout :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="#dimen/activity_vertical_margin"
android:paddingLeft="#dimen/activity_horizontal_margin"
android:paddingRight="#dimen/activity_horizontal_margin"
android:paddingTop="#dimen/activity_vertical_margin"
android:id="#+id/rtpew"
android:layout_centerInParent="true"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/linearpew">
<android.support.v7.widget.RecyclerView
android:id="#+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
</RelativeLayout>
And this is my emptylayout :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/ImageViewSearchFail"
android:src="#drawable/sadface"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_gravity="center"
android:textSize="#dimen/15dp"
android:layout_marginTop="4dp"
android:text="foo"
android:layout_below="#+id/ImageViewSearchFail"
android:layout_centerHorizontal="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/ButtonAddEntity"
android:text="foo"
android:background="?android:selectableItemBackground"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
The other way that I thought is to implement it programmatically as follow :
#Override
public boolean onQueryTextChange(String query) {
final ArrayList<List> filteredModelList = filter(mModel, query);
mAdapter.animateTo(filteredModelList);
rv.scrollToPosition(0);
if(query.isEmpty()){
//Here
}
return true;
}
And :
private ArrayList<List> filter(ArrayList<List> models, String query) {
query = query.toLowerCase();
final ArrayList<List> filteredModelList = new ArrayList<List>();
for (List model : models) {
final String text = model.getRedName().toLowerCase();
if (text.contains(query)) {
filteredModelList.add(model);
}
}
if (filteredModelList.size()<0) {
//HERE
}
else{
//Delete the views added
}
return filteredModelList;
}
PROBLEMS
-I only add the view using the #Jimeux answer but I'd like to do this on the Adapter, I got it, but not always shows the view even if the list is empty.
-At the time to put the emptyview.xml it puts inside of the RecyclerView then since I've put all of this xml at the center it shows on the right. I've tried to add the xml programmatically but it's like a chaos....
Since you need to handle two different kind of views, it would be easier to use an intermediate list of business object for more easily binding them with views. Idea is to have a kind of placeholder in your list for representing empty state. Defining an intermediate layer is extremely useful in this sense for allowing you to consider eventual changes to be applied to your list in future (e.g. adding you element types). Moreover in this way you can more clearly separate your business model from ui representation (for example you can implement methods returning ui settings based on internal status of model objects).
You can proceed as follows:
Define a dedicated abstract type for List items (e.g. ListItem) to wrap your business objects. Its implementation could be something like this:
public abstract class ListItem {
public static final int TYPE_EMPTY = 0;
public static final int TYPE_MY_OBJ = 1;
abstract public int getType();
}
Define a class for each of your List element type:
public class EmptyItem extends ListItem {
#Override
public int getType() {
return TYPE_EMPTY;
}
}
public class MyObjItem extends ListItem {
private MyObj obj;
public ContactItem(MyObj obj) {
this.obj = obj;
}
public MyObj getMyObj() {
return obj;
}
// here you can also add methods for simplify
// objects rendering (e.g. get background color
// based on your object internal status)
#Override
public int getType() {
return TYPE_MY_OBJ;
}
}
Create your list.
List<ListItem> mItems = new ArrayList<>();
if (dataList != null && dataList.size() > 0) {
for (MyObj obj : dataList) {
mItems.add(new MyObjItem(obj));
}
} else {
mItems.add(new EmptyItem());
}
This is the most important part of code. You have many options for creating this list. You can do it inside your RecyclerView Adapter or outside, but it's extremely important to properly handle eventual modifications to it. This is essential for exploiting Adapter notify methods. For example, if you create list within the Adapter, it should probably provide also methods for adding or removing your model items. For example:
public void addObj(MyObj obj) {
if (mItems.size() == 1 && mItems.get(0).getType() == ListItem.EMPTY_TYPE) {
mItems.clear();
}
mItems.add(new MyObjItem(obj));
notifyDataSetChanged();
}
Define an adapter for your RecyclerView, working on List defined at point 3. Here what is important is to override getItemViewType method as follows:
#Override
public int getItemViewType(int position) {
return mItems.get(position).getType();
}
Moreover, type of ViewHolder should be RecyclerView.ViewHolder (unless you decide to create an intermediate class even in this case).
Then you need to have two layouts and ViewHolder for empty and business obj items. Adapter methods should take care of this accordingly:
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ListItem.TYPE_EMPTY) {
View itemView = mLayoutInflater.inflate(R.layout.empty_layout, parent, false);
return new EmptyViewHolder(itemView);
} else {
View itemView = mLayoutInflater.inflate(R.layout.myobj_layout, parent, false);
return new MyObjViewHolder(itemView);
}
}
#Override
public void onBindViewHolder(final RecyclerView.ViewHolder viewHolder, final int position) {
int type = getItemViewType(position);
if (type == ListItem.TYPE_EMPTY) {
EmptyItem header = (EmptyItem) mItems.get(position);
EmptyViewHolder holder = (EmptyViewHolder) viewHolder;
// your logic here... probably nothing to do since it's empty
} else {
MyObjItem event = (MyObjItem) mItems.get(position);
MyObjViewHolder holder = (MyObjViewHolder) viewHolder;
// your logic here
}
}
Of course, as I wrote at the beginning you don't need to strictly define intermediate types for ui representation (EmptyItem and MyObjItem). You can even just use MyObj type and create a specific configuration for it that represent an empty placeholder. This approach is probably not the best in case in future you need to make your logic more complex by including for example new list item types.
Follow the below steps one by one
1). Since you have two types of views for your RecyclerView item, your adapter declaration should look like this a generic one
public class YourAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
and your ViewHolders for both listview item and empty view should extend RecyclerView.ViewHolder like this
static class ListItemViewHolder extends RecyclerView.ViewHolder {
public ListItemViewHolder(View itemView) {
super(itemView);
// initialize your views here for list items
}
}
static class EmptyViewViewHolder extends RecyclerView.ViewHolder {
public EmptyViewViewHolder(View itemView) {
super(itemView);
// initialize your views here for empty list
}
}
2). You have to Override getItemCount() and getItemViewType()
#Override
public int getItemCount() {
return yourList.size() > 0 ? yourList.size() : 1;// if size of your list is greater than 0, you will return your size of list otherwise 1 for the empty view.
}
#Override
public int getItemViewType(int position) {
if (yourList.size() == 0) {
return VIEW_TYPE_EMPTY;
}
return position;
}
3). Your onCreateViewHolder() will look alike this now
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_EMPTY) {
return new EmptyViewViewHolder(mLayoutInflater
.inflate(R.layout.empty_view_layout, parent, false));
} else {
return new ListItemViewHolder(mLayoutInflater
.inflate(R.layout.row_list_item, parent, false));
}
}
4). Same check you have to apply in your onBindViewHolder() as well
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == VIEW_TYPE_EMPTY) {
EmptyViewViewHolder emptyViewViewHolder = (EmptyViewViewHolder) holder;
// set values for your empty views
} else {
ListItemViewHolder listItemViewHolder = (ListItemViewHolder) holder;
// set values for your list items
}
}
5). At last Override your SearcView.setOnQueryTextListener()
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
#Override
public boolean onQueryTextSubmit(String query) {
return false;
}
#Override
public boolean onQueryTextChange(String newText) {
currentSearchKeyword = newText.trim();
if(currentSearchKeyword.iseEmpty()){
yourList.clear();
yourAdapter.notifyDataSetChanged();
}else{
// there will two cases again 1). If your currentSearchKeyword matchces with list results, add that items to your list and notify your adapter. 2) If the currentSearchKeyword doesn't matched with list results, clear your list and notify your adapter;
}
return false;
}
});
Hope it will help you, let me know if any issues.
The compilation error probably results because of you extending RecyclerView.Adapter with your main ViewHolder as the generic argument.
You should make it like
YourAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
And then cast your ViewHolders appropriately (you can reuse getViewType(position) here). Be sure to switch the ViewHolder type in your methods as well.
If I were you, I wouldn't put the empty view in the adapter at all. Put it under your linearpew layout that's holding the RecyclerView and hide/show it as your data changes. You can easily add a loading view, error view, etc. with this setup too.
Here's a bit of simplified code from one of my apps to give you some ideas. #Bind comes from Butter Knife if you're not familiar with it. You may also want to check out Jake Wharton's u2020 project for more RecyclerView ideas.
//fragment_layout.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="#+id/content">
</FrameLayout>
<include layout="#layout/status_views" />
</RelativeLayout>
//status_views.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<LinearLayout style="#style/ListStatusView"
android:id="#+id/empty_view"/>
<LinearLayout style="#style/ListStatusView"
android:id="#+id/error_view"/>
<LinearLayout style="#style/ListStatusView"
android:id="#+id/loading_view"
android:padding="30dp"/>
</LinearLayout>
//MyFragment.java
#Bind(R.id.content) protected ViewGroup contentView;
#Bind(R.id.loading_view) protected ViewGroup loadingView;
#Bind(R.id.empty_view) protected ViewGroup emptyView;
#Bind(R.id.error_view) protected ViewGroup errorView;
#Bind({R.id.loading_view, R.id.error_view, R.id.empty_view, R.id.content})
protected List<ViewGroup> stateViews;
protected void activateView(View view) {
for (ViewGroup vg : stateViews)
vg.setVisibility(View.GONE);
view.setVisibility(VISIBLE);
}
#Override
public void onActivityCreated(#Nullable Bundle state) {
super.onActivityCreated(state);
if (state == null) {
activateView(loadingView);
loadData();
} else if (data.isEmpty())
activateView(emptyView);
else
activateView(contentView);
}
Edit: Here's a simplified version without Butter Knife.
private ViewGroup contentView;
private ViewGroup emptyView;
#Override
protected void onCreate(#Nullable Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_main);
contentView = (ViewGroup) findViewById(R.id.content_view);
emptyView = (ViewGroup) findViewById(R.id.empty_view);
}
#Override
public boolean onQueryTextChange(String query) {
final ArrayList<List> filteredModelList = filter(mModel, query);
mAdapter.animateTo(filteredModelList);
rv.scrollToPosition(0);
if(query.isEmpty()){
contentView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
} else {
contentView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
}
return true;
}
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rtpew"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true">
<LinearLayout
android:id="#+id/content_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="#+id/rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<RelativeLayout android:id="#+id/empty_view">
<ImageView android:src="#drawable/sadface"/>
<TextView android:text="foo"/>
<Button android:id="#+id/ButtonAddEntity"/>
</RelativeLayout>
</RelativeLayout>
Here is what you can try:
1. Replace
EmptyViewHolder evh = new EmptyViewHolder(v);
with
RecyclerView.ViewHolder evh = new EmptyViewHolder(v);
This is probably why the compilation fails.
2. Replace
#Override
public int getItemViewType(int position) {
return list.size() > 0 ? list.size() : 1;
}
with
#Override
public int getItemViewType(int position) {
return list.get(position) != null ? 1 : 0;
}
For this to work, you must insert a null object whenever you want to show an EmptyView:
int progressPosition = list.size();
list.add(null);
adapter.notifyItemInserted(progressPosition);
and remove the null object when you want to hide the EmptyView:
int progressPosition = existingList.size() - 1;
existingList.remove(progressPosition);
adapter.notifyItemRemoved(progressPosition);
Also, you must modify your onCreateViewHolder() method as follows:
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == 1) {
// inflate your default ViewHolder here ...
} else {
// inflate the EmptyViewHolder here
}
}
I believe we have discussed this before ... see this question for a detailed discussion on this.
3. Instead of using a SearchView, consider using an AutoCompleteTextView with a Filter. This may be easier to integrate with your RecyclerView's Adapter. See this answer for an example of this.
I will update this answer as I understand your question better ... do try this and update me.

RecyclerView blinking after notifyDatasetChanged()

I have a RecyclerView which loads some data from API, includes an image url and some data, and I use networkImageView to lazy load image.
#Override
public void onResponse(List<Item> response) {
mItems.clear();
for (Item item : response) {
mItems.add(item);
}
mAdapter.notifyDataSetChanged();
mSwipeRefreshLayout.setRefreshing(false);
}
Here is implementation for Adapter:
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int position) {
if (isHeader(position)) {
return;
}
// - get element from your dataset at this position
// - replace the contents of the view with that element
MyViewHolder holder = (MyViewHolder) viewHolder;
final Item item = mItems.get(position - 1); // Subtract 1 for header
holder.title.setText(item.getTitle());
holder.image.setImageUrl(item.getImg_url(), VolleyClient.getInstance(mCtx).getImageLoader());
holder.image.setErrorImageResId(android.R.drawable.ic_dialog_alert);
holder.origin.setText(item.getOrigin());
}
Problem is when we have refresh in the recyclerView, it is blincking for a very short while in the beginning which looks strange.
I just used GridView/ListView instead and it worked as I expected. There were no blincking.
configuration for RecycleView in onViewCreated of my Fragment:
mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
// use this setting to improve performance if you know that changes
// in content do not change the layout size of the RecyclerView
mRecyclerView.setHasFixedSize(true);
mGridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
mGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
return mAdapter.isHeader(position) ? mGridLayoutManager.getSpanCount() : 1;
}
});
mRecyclerView.setAdapter(mAdapter);
Anyone faced with such a problem? what could be the reason?
Try using stable IDs in your RecyclerView.Adapter
setHasStableIds(true) and override getItemId(int position).
Without stable IDs, after notifyDataSetChanged(), ViewHolders usually assigned to not to same positions. That was the reason of blinking in my case.
You can find a good explanation here.
According to this issue page ....it is the default recycleview item change animation... You can turn it off.. try this
recyclerView.getItemAnimator().setSupportsChangeAnimations(false);
Change in latest version
Quoted from Android developer blog:
Note that this new API is not backward compatible. If you previously
implemented an ItemAnimator, you can instead extend
SimpleItemAnimator, which provides the old API by wrapping the new
API. You’ll also notice that some methods have been entirely removed
from ItemAnimator. For example, if you were calling
recyclerView.getItemAnimator().setSupportsChangeAnimations(false),
this code won’t compile anymore. You can replace it with:
ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
This simply worked:
recyclerView.getItemAnimator().setChangeDuration(0);
I have the same issue loading image from some urls and then imageView blinks.
Solved by using
notifyItemRangeInserted()
instead of
notifyDataSetChanged()
which avoids to reload those unchanged old datas.
try this to disable the default animation
ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
this the new way to disable the animation since android support 23
this old way will work for older version of the support library
recyclerView.getItemAnimator().setSupportsChangeAnimations(false)
In Kotlin you can use 'class extension' for RecyclerView:
fun RecyclerView.disableItemAnimator() {
(itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}
// sample of using in Activity:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// ...
myRecyclerView.disableItemAnimator()
// ...
}
Kotlin solution:
(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Recyclerview uses DefaultItemAnimator as it's default animator.
As you can see from the code below, they change the alpha of the view holder upon item change:
#Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
...
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
...
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null) {
....
ViewCompat.setAlpha(newHolder.itemView, 0);
}
...
return true;
}
I wanted to retain the rest of the animations but remove the "flicker" so I cloned DefaultItemAnimator and removed the 3 alpha lines above.
To use the new animator just call setItemAnimator() on your RecyclerView:
mRecyclerView.setItemAnimator(new MyItemAnimator());
Assuming mItems is the collection that backs your Adapter, why are you removing everything and re-adding? You are basically telling it that everything has changed, so RecyclerView rebinds all views than I assume the Image library does not handle it properly where it still resets the View even though it is the same image url. Maybe they had some baked in solution for AdapterView so that it works fine in GridView.
Instead of calling notifyDataSetChanged which will cause re-binding all views, call granular notify events (notify added/removed/moved/updated) so that RecyclerView will rebind only necessary views and nothing will flicker.
Try this in Kotlin
binding.recyclerView.apply {
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
In my case, neither any of above nor the answers from other stackoverflow questions having same problems worked.
Well, I was using custom animation each time the item gets clicked, for which I was calling notifyItemChanged(int position, Object Payload) to pass payload to my CustomAnimator class.
Notice, there are 2 onBindViewHolder(...) methods available in RecyclerView Adapter.
onBindViewHolder(...) method having 3 parameters will always be called before onBindViewHolder(...) method having 2 parameters.
Generally, we always override the onBindViewHolder(...) method having 2 parameters and the main root of problem was I was doing the same,
as each time notifyItemChanged(...) gets called, our onBindViewHolder(...) method will be called, in which I was loading my image in ImageView using Picasso, and this was the reason it was loading again regardless of its from memory or from internet. Until loaded, it was showing me the placeholder image, which was the reason of blinking for 1 sec whenever I click on the itemview.
Later, I also override another onBindViewHolder(...) method having 3 parameters. Here I check if the list of payloads is empty, then I return the super class implementation of this method, else if there are payloads, I am just setting the alpha value of the itemView of holder to 1.
And yay I got the solution to my problem after wasting a one full day sadly!
Here's my code for onBindViewHolder(...) methods:
onBindViewHolder(...) with 2 params:
#Override
public void onBindViewHolder(#NonNull RecyclerAdapter.ViewHolder viewHolder, int position) {
Movie movie = movies.get(position);
Picasso.with(context)
.load(movie.getImageLink())
.into(viewHolder.itemView.posterImageView);
}
onBindViewHolder(...) with 3 params:
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position, #NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
} else {
holder.itemView.setAlpha(1);
}
}
Here's the code of method I was calling in onClickListener of viewHolder's itemView in onCreateViewHolder(...):
private void onMovieClick(int position, Movie movie) {
Bundle data = new Bundle();
data.putParcelable("movie", movie);
// This data(bundle) will be passed as payload for ItemHolderInfo in our animator class
notifyItemChanged(position, data);
}
Note: You can get this position by calling getAdapterPosition() method of your viewHolder from onCreateViewHolder(...).
I have also overridden getItemId(int position) method as follows:
#Override
public long getItemId(int position) {
Movie movie = movies.get(position);
return movie.getId();
}
and called setHasStableIds(true); on my adapter object in activity.
Hope this helps if none of the answers above work!
In my case there was a much simpler problem, but it can look/feel very much like the problem above. I had converted an ExpandableListView to a RecylerView with Groupie (using Groupie's ExpandableGroup feature). My initial layout had a section like this:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/hint_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/white" />
With layout_height set to "wrap_content" the animation from expanded group to collapsed group felt like it would flash, but it was really just animating from the "wrong" position (even after trying most of the recommendations in this thread).
Anyway, simply changing layout_height to match_parent like this fixed the problem.
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/hint_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white" />
Hey #Ali it might be late replay. I also faced this issue and solved with below solution, it may help you please check.
LruBitmapCache.java class is created to get image cache size
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import com.android.volley.toolbox.ImageLoader.ImageCache;
public class LruBitmapCache extends LruCache<String, Bitmap> implements
ImageCache {
public static int getDefaultLruCacheSize() {
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
return cacheSize;
}
public LruBitmapCache() {
this(getDefaultLruCacheSize());
}
public LruBitmapCache(int sizeInKiloBytes) {
super(sizeInKiloBytes);
}
#Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
#Override
public Bitmap getBitmap(String url) {
return get(url);
}
#Override
public void putBitmap(String url, Bitmap bitmap) {
put(url, bitmap);
}
}
VolleyClient.java singleton class [extends Application] added below code
in VolleyClient singleton class constructor add below snippet to initialize the ImageLoader
private VolleyClient(Context context)
{
mCtx = context;
mRequestQueue = getRequestQueue();
mImageLoader = new ImageLoader(mRequestQueue,getLruBitmapCache());
}
I created getLruBitmapCache() method to return LruBitmapCache
public LruBitmapCache getLruBitmapCache() {
if (mLruBitmapCache == null)
mLruBitmapCache = new LruBitmapCache();
return this.mLruBitmapCache;
}
Hope its going to help you.
Try to use the stableId in recycler view. The following article briefly explains it
https://medium.com/#hanru.yeh/recyclerviews-views-are-blinking-when-notifydatasetchanged-c7b76d5149a2
I had similar issue and this worked for me
You can call this method to set size for image cache
private int getCacheSize(Context context) {
final DisplayMetrics displayMetrics = context.getResources().
getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
// 4 bytes per pixel
final int screenBytes = screenWidth * screenHeight * 4;
return screenBytes * 3;
}
for my application, I had some data changing but I didn't want the entire view to blink.
I solved it by only fading the oldview down 0.5 alpha and starting the newview alpha at 0.5. This created a softer fading transition without making the view disappear completely.
Unfortunately because of private implementations, I couldn't subclass the DefaultItemAnimator in order to make this change so I had to clone the code and make the following changes
in animateChange:
ViewCompat.setAlpha(newHolder.itemView, 0); //change 0 to 0.5f
in animateChangeImpl:
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { //change 0 to 0.5f
Using appropriate recyclerview methods to update views will solve this issue
First, make changes in the list
mList.add(item);
or mList.addAll(itemList);
or mList.remove(index);
Then notify using
notifyItemInserted(addedItemIndex);
or
notifyItemRemoved(removedItemIndex);
or
notifyItemRangeChanged(fromIndex, newUpdatedItemCount);
Hope this will help!!
In my case I used SwipeRefresh and RecycleView with viewmodel binding and faced with blinking. Solved with ->
Use submitList() to keep the list updated
because DiffUtils have done the job, otherwise the list reloading entirely
refer to CodeLab https://developer.android.com/codelabs/kotlin-android-training-diffutil-databinding#4
when using livedata I solved it with diffUtil
This is how I combined diffUtill and databinding with my adapter
class ItemAdapter(
private val clickListener: ItemListener
) :
ListAdapter<Item, ItemAdapter.ViewHolder>(ItemDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(clickListener, getItem(position)!!)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
class ViewHolder private constructor(val binding: ListItemViewBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
clickListener: ItemListener,
item: Item) {
binding.item = item
binding.clickListener = clickListener
binding.executePendingBindings()
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = ListItemViewBinding
.inflate(layoutInflater, parent, false)
return ViewHolder(view)
}
}
}
class ItemDiffCallback :
DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.itemId == newItem.itemId
}
override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
return newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
}
}
class ItemListener(val clickListener: (item: Item) -> Unit) {
fun onClick(item: Item) = clickListener(item)
}
In my case (shared element transition between one image to a higher resolution image), I added some delay to the item decorator in order to make it less noticeable:
(yourRv.itemAnimator as? DefaultItemAnimator)?.changeDuration = 2000
My own issue was a very specific problem which I'm going to share some insight for here, though I don't yet fully understand it.
Basically I had a re-usable fragment with RecyclerView, so that I could have a menu with nested menus. Pick an item in the RecyclerView, and it opens another fragment with more options. All facilitated using the JetPack navigation component and data binding using LiveData in the layout xml.
Anyway, here's how I fixed my issue of items flickering when the RecyclerView changed (although it's worth bearing in mind, this was only the appearance as it was a 'new' RecyclerView each time). To update the LiveData list of items (some viewmodels representing objects for the menu items, in my case) I was using LiveData.value = new items. Changing it to postValue(new items) fixed the issue, though I'm not yet sure why.
I've read up the difference between value (setValue in Java) and PostValue, and I understand they're to do with using the main thread or background threading, and the latter only gets applied one time on the main thread when it's ready. But other than that, I'm not sure why this fixed flickering in my RecyclerView. Maybe someone has some insight? In any case, hopefully this will help someone facing a similar problem to me.
for me recyclerView.setHasFixedSize(true); worked

Categories

Resources